Requirements
- Linux. The hook runtime uses
SYS_gettid,pthread_getname_npand POSIX shared memory. - GCC for selective instrumentation. Clang implements
-finstrument-functionsbut not the exclude lists (LLVM #15627), so under Clang only an "instrument everything" config builds. callsight detects the compiler and says so before your build starts. - binutils —
addr2lineresolves the recorded addresses, includingstaticfunctions. - Python 3.9+ and uv. The core is stdlib-only; the extras are optional.
Install
$ uv tool install callsight # core CLI
$ uv tool install 'callsight[ui]' # + the web UI
$ uv tool install 'callsight[stream]' # + the remote streaming server
Both extras can be combined: 'callsight[ui,stream]'. From a source
checkout, uv tool install . does the same.
Adopt a project
callsight init copies the hook runtime and the build wiring into a
callsight/ directory inside your project, writes a starter
trace.config (an existing one is never overwritten), and prints the exact
snippet to paste into your build file.
$ cd /path/to/your/project
$ callsight init .
wrote /path/to/your/project/trace.config
copied runtime + make integration into /path/to/your/project/callsight/ (35 sources found)
The build system is auto-detected — CMakeLists.txt means CMake, otherwise
Make. Force it with --build make or --build cmake, and add
--stream to also copy the on-device streaming client.
Wire it into your build
After your SRCS/OBJS/CFLAGS_SYMBOLS are
defined:
CALLSIGHT_DIR ?= callsight
include $(CALLSIGHT_DIR)/Makefile.callsight
instrument: CFLAGS = $(CFLAGS_INSTRUMENT)
instrument: $(BINDIR)/$(TARGET).instr
$(BINDIR)/$(TARGET).instr: $(OBJS) $(TRACE_OBJ) | $(BINDIR)
$(CC) $(CFLAGS_INSTRUMENT) -o $@ $(OBJS) $(TRACE_OBJ) $(LDFLAGS)
The fragment expects SRCS, BUILDDIR,
BINDIR, TARGET, CC,
CFLAGS_SYMBOLS and LDFLAGS, and defines
CFLAGS_INSTRUMENT (your flags + hooks + exclude lists) and
TRACE_OBJ (the runtime object, compiled without instrumentation).
After the target is defined:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/callsight")
include(CallSight)
callsight_instrument(<your-target>)
Normal builds are completely untouched — instrumentation is opt-in per configure:
$ cmake -DCALLSIGHT_INSTRUMENT=ON -B build-instr
$ cmake --build build-instr
Full details for both, including every overridable variable, are on the reference page.
-no-pie so
that recorded addresses matched link addresses. The runtime now records the load bias in
every trace header and the analyzer subtracts it, so position-independent executables
work unchanged — which matters, because forcing -no-pie meant profiling a
binary built differently from the real one.
Build the instrumented profile
$ make clean && make instrument # clean when switching profiles
The instrumented binary lives beside your normal one
(bin/yourapp.instr by convention with the Make integration), so a normal
make still produces an uninstrumented build with zero hooks in it.
Run it
The hooks are compiled in but inert until you ask for a trace, so an instrumented binary is safe to run normally. One command runs it with tracing on and reports on what it recorded:
$ callsight run --timeout 10 --top 20 -- ./bin/yourapp.instr
run clears trace files from any earlier run first — reporting on a stale
traces/ directory silently mixes two runs into one set of numbers, which is
the easiest way to draw a wrong conclusion here.
Or drive it by hand, which is what run does for you:
$ TRACE_ENABLE=1 ./bin/yourapp.instr
$ callsight analyze traces/ --exe ./bin/yourapp.instr --top 20
TRACE_ENABLE=1turns collection on. Without it, every hook is one predictable branch and returns.TRACE_DIRchanges the output directory (default./traces).TRACE_THREADS="worker-*"records only threads whose name matches — a runtime filter, no rebuild needed.- Capture stops at 512 MB by default and says so in the report. Long runs, flight recorder mode and constant-size summaries are on the capture limits page.
Each thread writes traces/trace.<pid>.<tid>.<seq>.bin. A
clean exit flushes everything; a killed process loses each thread's buffered tail.
Read the report
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)
unmatched_exits=0 means every exit was matched to an enter — a clean
trace. Every call is timed, so p50, p99 and max are
measurements rather than estimates: qs_swap above normally finishes in 8 ns,
and once took 352 µs. Read the columns, export a flame graph, and interpret the
summary line on the analysis page.
If something looks wrong
callsight doctor checks the pieces this depends on — the compiler and
whether it is GCC, addr2line, that your config selects something, and that
the trace directory is writable with room to spare:
$ callsight doctor
[ok ] compiler: cc is GCC — selective instrumentation available
[ok ] addr2line: /usr/bin/addr2line
[ok ] trace.config: 35 sources, 34 instrumented, 1 excluded
[ok ] free space: 28187 MB available
[ok ] -finstrument-functions: accepted by the compiler
Narrow the selection
Your first wide run is a survey. Sort it by calls, find the chatty leaf
helpers, and exclude them — they usually account for most of the volume and none of the
insight.
# trace.config
exclude src/utils/rng.c # RNG helpers: pure noise
exclude-func crc32_update # one hot function, by name
Then rebuild. Or go the other way and name a single entry point — callsight resolves its whole call subtree from the sources and instruments exactly that:
$ callsight select src/ --function handle_request
$ callsight scan . --config trace.config # preview without building
The whole selection model is on the configuration page.
Try it without a project
The repository ships tests/matrixlab, a multi-threaded C11 workload used
as the end-to-end fixture — the traces in these docs come from it.
$ git clone https://github.com/harshithsunku/callsight && cd callsight/tests/matrixlab
$ make clean && make instrument
$ TRACE_ENABLE=1 TRACE_MAX=1000000 timeout 5 ./bin/matrixlab.instr
$ uv run callsight analyze traces/ --exe bin/matrixlab.instr --top 20