Open source · MIT · Linux

Trace every call you care about.
Pay nothing for the rest.

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.

$uv tool install callsight
license MIT PyPI compilers languages overhead
Flame graph of a multi-threaded C workload: thread_main splits into workload_matrix, workload_signal, workload_stats, workload_crypto and workload_sort, with the recursive fft and quicksort towers clearly visible

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.

What it does

One config file, the whole tracing pipeline.

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.

Zero source edits

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.

Selection that is free

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.

Exact, not sampled

Every call is counted and timed, so p50, p99 and max are measurements rather than estimates — the rare slow call a sampling profiler would miss entirely is right there in the table.

Exports that fit your tools

folded for flamegraph.pl and speedscope, chrome for a real timeline in Perfetto, callers for hot call sites, and json plus callsight diff to fail a build on a regression.

It cannot fill your disk

Capture is bounded by default, rotates in segments, stops before the filesystem does — and says so in the report. TRACE_MODE=summary aggregates in-process instead: hours of execution, kilobytes of output, exact counts.

Streams off the device

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.

A browser front end

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.

How it works

Compile, run, analyze.

Four stages. The only one that touches your build is the first, and it is driven entirely by trace.config.

01 · COMPILE

Selection becomes flags

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.

02 · RUN

Hooks fill per-thread buffers

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.

03 · COLLECT

Files, or a stream

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.

04 · ANALYZE

Symbols and hotspots

callsight analyze streams the events, matches enter/exit per thread, resolves addresses through addr2linestatic functions included — and prints, exports, or serves the report.

COMPILE TIME · YOUR BUILD your sources never modified trace.config what gets hooks -finstrument- functions + exclude lists RUNTIME · TRACED PROCESS __cyg_profile hooks 32-byte events per-thread buffer lock-free, no malloc trace.*.bin local files shm ring drops, never blocks ANALYSIS HOST trace_stream zstd · raw TCP callsight serve writes trace files callsight analyze · web UI · flame graph calls · inclusive · self · max, per thread

The streaming path (cyan) is optional — without it, buffers flush straight to trace files next to your binary.

The idea

Event volume is the whole game.

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.

Write what you want traced

# 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.

See what it selects, before building

$ 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
Why compile-time? A runtime filter still pays for the hook: the call happens, the check runs, the branch is predicted. Compile-time exclusion removes the call site entirely. Running wide once and then excluding the chatty leaf helpers typically cuts event volume by 10–100×.
The output

A report you can act on.

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=850059 threads=24 functions=84 span=6.6ms unmatched_exits=0 unclosed_enters=127

== TOP BY SELF TIME ==
     calls      incl_ms      self_ms       p50       p99       max  function (first location)
        12       13.170       13.170  983.04us    1.97ms    1.97ms  timer_sleep_us (src/utils/timer.c:38)
     16111       11.098        6.518      71ns    3.84us    1.59ms  qs_partition (src/sort/quicksort.c:23)
    358383        4.738        4.738       8ns      14ns  352.74us  qs_swap (src/sort/quicksort.c:5)
         4        2.335        2.230  180.22us    1.70ms    1.79ms  matrix_multiply_blocked (src/matrix/matrix_multiply.c:24)
       384        1.450        1.450    4.61us    5.63us    7.75us  matrix_lu_solve (src/matrix/matrix_decomp.c:48)

qs_swap normally finishes in 8 ns and once took 352 µs. A mean hides both numbers; a sampling profiler would almost certainly never see that call.

The callsight web UI hotspot table: sortable columns for calls, inclusive ms, self ms and max ms, with resolved function names and source locations
The same report in the browserSortable columns, resolved source locations, and the clean-trace check.
The callsight config builder: a checkbox pane of 35 source files beside a searchable pane of 315 functions, generating trace.config
Build the config by clickingFiles and functions enumerated with ctags, filtered live, written out as trace.config.
Quick start

Four commands in.

Adopt an existing project without changing a line of its code.

1

Install

Stdlib-only Python core; the extras are optional.

$ uv tool install callsight        # or 'callsight[ui]' for the web UI
2

Adopt

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 .
3

Build and run

The instrumented profile is separate from your normal build, and stays inert until you ask for a trace. callsight run traces it and reports in one step.

$ make instrument
$ callsight run -- ./bin/yourapp.instr
$ cmake -DCALLSIGHT_INSTRUMENT=ON -B build-instr
$ cmake --build build-instr
$ callsight run -- ./build-instr/yourapp
4

Analyze

Terminal tables, a flame graph, a Perfetto timeline, 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
$ callsight analyze traces/ --exe ./bin/yourapp.instr --format chrome > trace.json
Where it fits

How it compares.

Reach for perf first when you want a cheap statistical profile of a whole system, kernel time, or hardware counters. Reach for callsight when you need exactness for code you chose — every call counted, true tail latency per function, and zero cost for everything you did not choose.

ToolGranularitySelectionNeeds
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
Compiler note. Selective instrumentation requires GCC: the -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.

Point it at your project.

Two commands to adopt, and nothing in your source tree changes.