callsight adds entry/exit timing hooks to a C or C++ project at compile
time, with zero edits to its sources. One trace.config decides
which files, folders, or call subtrees get hooks — and everything you exclude emits
no hook at all, so it costs exactly zero at runtime.
A real trace of the bundled matrixlab workload — 1,000,000 events across
26 threads, exported with callsight analyze --format folded and rendered
as a flame graph.
Compile-time selection, a lock-free per-thread runtime, exact per-call timing, and a report you can read in the terminal, in a browser, or in a flame graph.
Hooks come from -finstrument-functions at compile time. Your source
tree is never touched — adopting callsight adds a config file and a build include,
nothing else.
Excluded code is not compiled with hooks, so it does not check a flag, take a branch, or touch a buffer. Runtime filters can't match that — the instruction simply isn't there.
Every call is counted and timed: calls, inclusive, self and max time per function, matched per thread. No statistical blur, no missed rare-but-slow paths.
--format folded feeds flamegraph.pl and speedscope directly;
--format json gives you the whole report — counters, rows, per-thread
timing — for your own tooling.
On a constrained target the runtime writes into a shared-memory ring; a tiny C client ships it ZSTD-compressed over TCP. The traced process does no disk or network I/O, and a full ring drops events rather than stalling your workload.
callsight ui walks the whole loop — browse a project, build the
config from checkboxes, compile, run, and read the hotspot table. No root, one
command.
Four stages. The only one that touches your build is the first, and it is driven
entirely by trace.config.
callsight flags turns trace.config plus your source
list into -finstrument-functions and the matching exclude lists. The
Make and CMake integrations call it for you on every build.
The compiler emits __cyg_profile_func_enter/exit calls. The runtime
appends 32-byte events to a thread-local buffer — no locks, no malloc, no I/O on
the hot path — and stays inert unless TRACE_ENABLE=1.
Buffers flush to trace.<pid>.<tid>.bin, or into a POSIX
shared-memory ring that the trace_stream client drains and forwards to
callsight serve on your workstation.
callsight analyze streams the events, matches enter/exit per thread,
resolves addresses through addr2line — static functions
included — and prints, exports, or serves the report.
The streaming path (cyan) is optional — without it, buffers flush straight to trace files next to your binary.
A call-heavy program generates millions of events per second. Every tracer has to answer "how do I record less?" — callsight answers it in the compiler, before a single instruction is emitted.
# trace.config
include src/network/ # only this subsystem
exclude src/network/crc.c # except the chatty helper
exclude-func log_printf # and this one, by name
# or select one task's whole call subtree:
include-func handle_request # + everything it calls
include-func resolves the call graph statically from your sources,
so naming one entry point selects exactly its subtree — explore it first with
callsight select src/ --function handle_request.
$ callsight scan . --config trace.config
35 sources: 34 instrumented, 1 excluded
excluded: src/utils/rng.c
$ callsight select src/ --function workload_sort
workload_sort: 31 functions across 6 files
heapsort
mergesort
qs_partition
…
# add to trace.config:
include-func workload_sort
Sort by self time for hot leaves, by inclusive time
for the slow high-level operation, by calls to find your next
exclusion. unmatched_exits=0 means the trace is clean.
$ callsight analyze traces/ --exe bin/matrixlab.instr --top 5
events=1000000 threads=26 functions=139 span=48.7ms unmatched_exits=0 unclosed_enters=136
== TOP BY SELF TIME ==
calls incl_ms self_ms max_ms function (first location)
272 529.382 529.382 9.983 timer_sleep_us (src/utils/timer.c:38)
127048 52.470 52.470 6.643 qs_swap (src/sort/quicksort.c:5)
57284 366.418 35.370 5.738 fft_recursive (src/signal/fft.c:63)
104870 33.523 33.523 10.017 stats_running_push (src/stats/statistics.c:84)
3195 60.699 30.238 18.485 qs_partition (src/sort/quicksort.c:23)
trace.config.Adopt an existing project without changing a line of its code.
Stdlib-only Python core; the extras are optional.
$ uv tool install callsight # or 'callsight[ui]' for the web UI
Copies the hook runtime and the build wiring, writes a starter
trace.config, and prints the snippet for your build system.
$ cd /path/to/your/project
$ callsight init .
The instrumented profile is separate from your normal build, and stays inert until you ask for a trace.
$ make instrument
$ TRACE_ENABLE=1 TRACE_MAX=1000000 ./bin/yourapp.instr
$ cmake -DCALLSIGHT_INSTRUMENT=ON -B build-instr
$ cmake --build build-instr
$ TRACE_ENABLE=1 TRACE_MAX=1000000 ./build-instr/yourapp
Terminal tables, a flame graph, or JSON for your own tooling.
$ callsight analyze traces/ --exe ./bin/yourapp.instr --top 20
$ callsight analyze traces/ --exe ./bin/yourapp.instr --format folded > out.folded
Reach for perf first when you want a cheap statistical profile of a whole
system. Reach for callsight when you need exact per-call timing for a chosen
subsystem — and zero cost for everything else.
| Tool | Granularity | Selection | Needs |
|---|---|---|---|
| callsight | Every entry/exit, exact timing | Compile time, from one config file — excluded code emits no hook at all | Rebuild with GCC |
| uftrace | Same mechanism, richer live TUI and replay | Mostly runtime filters (-F/-N), so filtered functions still pay for the hook |
Rebuild (-pg / -finstrument-functions) |
perf record |
Sampled, statistical | None needed | No rebuild; often root or perf_event_paranoid |
gprof (-pg) |
Sampled + call counts | None | Rebuild; single-threaded accounting |
| Clang XRay | Entry/exit with runtime patching | Per-function attributes and lists | Rebuild, Clang only |
-finstrument-functions-exclude-* flags it builds on are GCC-only
(LLVM #15627).
Clang can instrument everything — a config with no include/exclude
directives — and callsight detects the toolchain and tells you up front instead of
letting the build fail one file at a time.
Two commands to adopt, and nothing in your source tree changes.