Skip to main content

callsight — compile-time function tracing for C/C++

ci docs PyPI License: MIT

Docs: https://harshithsunku.github.io/callsight/ · Status & roadmap

Getting started · Configuration · Analysis · Web UI · Streaming · Architecture · Reference

Add entry/exit timing hooks to every function in a C/C++ project at compile time, with zero edits to its sources, control exactly which files, folders, or functions get hooks from one config file, and turn the resulting traces into per-function hotspot reports and flame graphs. C and C++, GNU Make and CMake, on Linux.

Compiler: selective instrumentation needs GCC — the exclude flags it builds on are GCC-only (LLVM issue #15627). Clang can instrument everything (a config with no include/exclude directives); callsight detects the toolchain and tells you up front rather than letting the build fail one file at a time.

Install

uv tool install callsight           # from PyPI; puts `callsight` on your PATH
uv tool install 'callsight[ui]'     # same, plus the optional web UI
uv tool install 'callsight[stream]' # same, plus the streaming server
uv tool install .                   # or from a source checkout

(Requires uv. The tool itself is Python stdlib-only; the runtime it injects is dependency-free C.)

Web UI

callsight ui                # serves http://127.0.0.1:8321

A local web app that walks the whole workflow against any project on the machine: browse for the project folder, edit its trace.config, preview the instrumentation selection, build the instrumented profile (Make or CMake — CMake is fetched ephemerally via uvx if not installed), run the binary with tracing enabled, and view the sortable hotspot report. A second tab, the config builder, scans the project folder into searchable checkbox panes of files and functions (enumerated with ctags — auto-downloaded on first scan when the system has none — with a regex fallback) and generates the trace.config from your picks. No root needed; bind address defaults to localhost.

Adopt it in your project (2 steps)

cd /path/to/your/project
callsight init .          # copies runtime + build wiring, writes trace.config

Then follow the printed wiring snippet for your build system:

  • Make: include callsight/Makefile.callsight, add an instrument target linking $(TRACE_OBJ) with -no-pie (snippet printed by init).
  • CMake: include(CallSight) + callsight_instrument(<target>), then configure with -DCALLSIGHT_INSTRUMENT=ON.

Collect and analyze

make instrument                                 # or: cmake --build build-instr
TRACE_ENABLE=1 TRACE_MAX=1000000 ./yourapp      # collect (inert without TRACE_ENABLE=1)
callsight analyze traces/ --exe ./yourapp --top 20

The analyzer reports calls / inclusive / self / max time per function, resolving symbols — including static functions — with addr2line. unmatched_exits=0 means a clean trace. Trace files are streamed, so a multi-million-event run costs a few MB of analyzer memory, not gigabytes.

Flame graphs and machine-readable output

callsight analyze traces/ --exe ./yourapp --format folded > out.folded
flamegraph.pl out.folded > out.svg      # or drag out.folded into speedscope.app
callsight analyze traces/ --exe ./yourapp --format json --top 0 | jq '.rows[0]'

--format folded prints one collapsed stack per call path (main;handle_request;parse <self_ns>), the input format understood by flamegraph.pl and speedscope. --format json emits the whole report — summary counters, per-function rows, per-thread timing — for your own tooling (--top 0 keeps every row).

How it works

  1. callsight flags turns trace.config + your source list into -finstrument-functions plus compile-time exclude lists (-finstrument-functions-exclude-file-list/-exclude-function-list).
  2. The compiler emits calls to __cyg_profile_func_enter/exit at the entry/exit of every selected function. Excluded code emits no hook at all — compile-time exclusion is free at runtime.
  3. The hook runtime (trace.c, itself compiled without the flag) appends 32-byte events to a per-thread buffer — no locks, no malloc, no I/O in the hot path — flushing trace.<pid>.<tid>.bin when full or at exit.
  4. callsight analyze matches enter/exit events per thread and prints hotspot tables.

Selection strategy (important at scale)

Event volume is the main cost — a call-heavy program easily generates millions of events per second. Levers, cheapest first:

  1. Compile-time excludes in trace.config: run wide once, sort analyzer output by calls, exclude the chatty leaf helpers, rebuild. Typically cuts volume 10–100×.
  2. include directives: instrument only the subsystem under investigation.
  3. include-func (function/task level): name one entry function and callsight instruments exactly its call subtree, resolved statically from the sources — include-func workload_sort traces workload_sort and everything it calls, nothing else. Explore first with callsight select src/ --function workload_sort.
  4. Runtime gating: TRACE_ENABLE / TRACE_MAX control when and how much you pay; TRACE_THREADS="sort-*" traces only matching threads.
  5. Source opt-out (optional): __attribute__((no_instrument_function)).

callsight scan <dir> --config trace.config previews what a config selects.

Runtime knobs: TRACE_ENABLE (default off), TRACE_DIR (default ./traces), TRACE_MAX (global event cap — always set one for long runs), TRACE_THREADS (thread-name glob filter), TRACE_SHM / TRACE_SHM_SIZE (streaming mode, see below).

Remote streaming (devices, embedded)

On a constrained device you can't accumulate trace files — a busy program generates millions of events per second. Streaming mode keeps nothing on the device: the runtime flushes into a shared-memory ring, and a tiny on-device client forwards events ZSTD-compressed over raw TCP.

# analysis host (powerful machine):
callsight serve --port 9001 --out traces/     # needs callsight[stream]

# adopt with streaming support:
callsight init --stream /path/to/project      # adds trace_stream.c + zstd.c

# on the device:
cc -O2 -o callsight/trace_stream callsight/trace_stream.c callsight/zstd.c
./callsight/trace_stream /callsight0 <server-ip> 9001 &
TRACE_ENABLE=1 TRACE_SHM=/callsight0 ./yourapp.instr
  • The traced process does no disk or network I/O — only a shared-memory memcpy per buffered batch.
  • If the ring fills faster than the client drains (network slow, ring too small), events are dropped and counted, never blocking the workload; the server reports the drop count. Size the ring with TRACE_SHM_SIZE.
  • The server writes standard trace.stream.*.bin files — callsight analyze and the web UI consume them unchanged.
  • The client is self-contained C built against the vendored single-file zstd v1.5.7 (src/callsight/stream/zstd.c, generated from the official repo's build/single_file_libs; BSD license in zstd.LICENSE).

How it compares

tool granularity selection needs
callsight every function 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/replay mostly runtime filters (-F/-N), so filtered functions still pay the hook rebuild (-pg/-finstrument-functions)
perf record sampled, statistical none needed no rebuild; often root/perf_event_paranoid
gprof (-pg) sampled + call counts none rebuild; single-threaded accounting
Clang XRay entry/exit with runtime patching per-function attributes / lists rebuild with Clang only

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 — every call counted, nothing sampled — and you want the cost of the functions you did not choose to be exactly zero, because they were never given a hook. The selection lives in trace.config next to the code, and the same config drives the traced device and the analysis host.

CLI reference

command purpose
callsight init <dir> [--build make|cmake] adopt into a project
callsight scan <dir> [--config c] preview instrumentation selection
callsight select <dir> --function F [--depth N] show a function's call subtree; emit config lines
callsight flags --config c -- srcs... print compiler flags (build integrations use this)
callsight analyze [traces/] [--exe bin] [--top N] [--format text|json|folded] hotspot report, JSON, or collapsed stacks
callsight ui [--host H] [--port P] web UI (needs callsight[ui])
callsight provision [--force] download the bundled static ctags used by the UI config builder
callsight serve [--host H] [--port P] [--out dir] TCP server for remote streams (needs callsight[stream])

Repo layout

  • src/callsight/ — the tool: cli.py, flags.py (config → compiler flags), analyze.py (offline analyzer), callgraph.py (static call graph behind include-func), symbols.py (function enumeration for the config builder), provision.py (bundled ctags download); the core is stdlib-only, serve.py (streaming TCP server) needs the stream extra. src/callsight/ui/ is the optional web UI (FastAPI, only imported by callsight ui).
  • src/callsight/runtime/trace.c/trace.h/trace_shm.h, the hook runtime copied into adopted projects. Self-contained C, no deps beyond pthreads.
  • src/callsight/stream/trace_stream.c on-device streaming client + vendored single-file zstd v1.5.7 (zstd.c, BSD — see zstd.LICENSE).
  • src/callsight/share/Makefile.callsight, src/callsight/cmake/CallSight.cmake — build-system integrations.
  • tests/matrixlab/ — multi-threaded C11 demo workload; doubles as the end-to-end smoke test (make instrument → run → callsight analyze).
  • tests/cmake_demo/ — tiny CMake fixture for the CMake integration.
  • docs/ — the documentation site (hand-built static HTML, published to GitHub Pages). docs/architecture.html carries the survey of GCC/Clang instrumentation mechanisms and how they map to callsight's roadmap.

Known limitations

  • Selective instrumentation is GCC-only; Clang has -finstrument-functions but not the exclude lists (LLVM #15627). Under Clang, only an unfiltered "instrument everything" config builds.
  • Linux only: the runtime uses SYS_gettid, pthread_getname_np and POSIX shared memory.
  • Inlined functions emit no hooks (there is no call boundary).
  • Overhead ~30–60 ns per event; a profiling build, not a production one.
  • A crashed/killed process loses each thread's buffered tail; clean exits flush everything.
  • exclude-file-list matching by the compiler is substring-based; unusually overlapping directory names can over-match.
  • Analysis needs the recorded addresses to match link addresses, so link with -no-pie (both build integrations do). analyze warns when most addresses fail to resolve, which is what a PIE binary looks like.

Roadmap

  • Phase 2 — web UI: done (callsight ui, optional callsight[ui] extra). Flame graphs: done via analyze --format folded (flamegraph.pl / speedscope). Next: richer in-UI report views, live build-log streaming.
  • Phase 3 — remote streaming: done (TRACE_SHM ring → trace_stream client → ZSTD/TCP → callsight serve). Next: runtime on/off via -fpatchable-function-entry (see the compiler-mechanism survey), live stream view in the web UI.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

callsight-0.3.0.tar.gz (6.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

callsight-0.3.0-py3-none-any.whl (585.2 kB view details)

Uploaded Python 3

File details

Details for the file callsight-0.3.0.tar.gz.

File metadata

  • Download URL: callsight-0.3.0.tar.gz
  • Upload date:
  • Size: 6.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for callsight-0.3.0.tar.gz
Algorithm Hash digest
SHA256 6dbf2dd759995f39e15218cd4b3966101d28aab73a0d879fca69e76c6f5c651f
MD5 96e2241fccc1c20cbea12fe3741962a9
BLAKE2b-256 08bd146729f71ee1f13b37ec89534a5b515fc42f30bf964f438e911f9aeaaaba

See more details on using hashes here.

Provenance

The following attestation bundles were made for callsight-0.3.0.tar.gz:

Publisher: release.yml on harshithsunku/callsight

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file callsight-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: callsight-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 585.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for callsight-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0e2eaf27f02dc00a8160268699494286e67129022d366d18dc4e8ae4a77466f4
MD5 f799656037395c62bc3d8b2e2ed865e0
BLAKE2b-256 2784a2ac45cf457c8d92f0770664fb5219774951e64d0f8004c573bd86e077c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for callsight-0.3.0-py3-none-any.whl:

Publisher: release.yml on harshithsunku/callsight

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page