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) -no-pie -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.
addr2line. Both integrations add it for
you. If you link the instrumented binary yourself and skip it, analyze will
warn that nothing resolved.
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.
Collect a trace
The hooks are compiled in but inert until you ask for a trace, so an instrumented binary is safe to run normally.
$ TRACE_ENABLE=1 TRACE_MAX=1000000 ./bin/yourapp.instr
TRACE_ENABLE=1turns collection on. Without it, every hook is one predictable branch and returns.TRACE_MAXcaps total events. Always set one for long runs — a call-heavy program can produce millions of events per second.TRACE_DIRchanges the output directory (default./traces).TRACE_THREADS="worker-*"records only threads whose name matches — a runtime filter, no rebuild needed.
Each thread writes traces/trace.<pid>.<tid>.bin. A clean exit
flushes everything; a killed process loses each thread's buffered tail.
Analyze
$ callsight analyze traces/ --exe ./bin/yourapp.instr --top 20
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)
unmatched_exits=0 means every exit was matched to an enter — a clean
trace. Read the columns, export a flame graph, and interpret the summary line on the
analysis page.
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