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: calls, inclusive, self and max time per function, matched per thread. No statistical blur, no missed rare-but-slow paths.

Flame graphs & JSON

--format folded feeds flamegraph.pl and speedscope directly; --format json gives you the whole report — counters, rows, per-thread timing — for your own tooling.

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=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)
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.

$ 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
4

Analyze

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
Where it fits

How it compares.

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.

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.