Devices

Remote streaming

On a constrained target you cannot accumulate trace files. Streaming mode keeps nothing on the device: events go through a shared-memory ring and leave over TCP, compressed.

When to stream

How it works

DEVICE · NOTHING TOUCHES DISK ANALYSIS HOST traced process TRACE_SHM=/name trace.c hooks shm ring fixed size, spinlocked full → drop + count trace_stream static C client zstd compress callsight serve trace.stream.*.bin one file per device analyze web UI TCP no disk I/O · no network I/O in the traced process — only a shared-memory memcpy
  1. Traced process. With TRACE_SHM=/name set, the hooks flush per-thread event batches into a POSIX shared-memory ring instead of files. A flush is a locked memcpy of a buffered batch — short critical sections, serialized by a spinlock, never held across I/O, because there is no I/O.
  2. On-device client. trace_stream maps the same ring, drains it in batches, ZSTD-compresses each batch, and sends it over a raw TCP connection. It exits once the traced process detaches and the ring is drained.
  3. Analysis host. callsight serve decompresses each chunk and appends it to a standard trace.stream.<id>.bin file — one per connection. Events from all threads arrive interleaved in one stream and are demultiplexed by the analyzer, since every event carries its tid.

The never-stall guarantee

Profiling must never slow down the thing it measures.

Verified end to end. 500,000 events across 26 threads over localhost with unmatched_exits=0, and drop counting confirmed under deliberate ring overflow — one of the repository's CI smoke jobs.

Quick start

# analysis host (needs the stream extra):
$ uv tool install 'callsight[stream]'
$ callsight serve                    # 0.0.0.0:9001, writes traces/
# adopt with streaming support — copies trace_stream.c + the vendored zstd:
$ callsight init --stream /path/to/project
# 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

Then analyze on the host exactly as with local traces:

$ callsight analyze traces/ --exe ./yourapp.instr --top 20
Keep the binary. Symbols are resolved on the host from the same instrumented binary you shipped to the device — --exe must point at it, and addr2line must match its architecture.

Ring layout and wire protocol

Both live in src/callsight/runtime/trace_shm.h (TRACE_SHM_VERSION / TRACE_STREAM_VERSION are bumped on any layout change). All integers little-endian.

Ring header

trace_shm_header_t, followed by capacity bytes of event storage. The header is 64 bytes.

FieldTypeMeaning
magicchar[8]TKSHM
versionu32TRACE_SHM_VERSION (1)
capacityu32Ring bytes after the header
writersu32Tracer processes attached
locku32Spinlock: 0 = free
headu64Monotonic write offset (bytes)
tailu64Monotonic read offset (bytes)
droppedu64Events dropped because the ring was full
_padu8[16]Reserved

head and tail are monotonic byte counters; bytes in use is head - tail.

Wire protocol

Client → server over TCP: an 8-byte TKSTREAM magic, u32 version and u32 event_size, then a sequence of chunks. Each chunk is u32 type, u32 raw_len, u32 zstd_len followed by zstd_len bytes of ZSTD-compressed payload.

TypePayloadServer action
0 — eventsDecompresses to raw_len bytes of raw eventsAppended to the output file as-is
1 — noticeDecompresses to a u64 dropped-event countPrinted, so loss is visible

The on-device client

trace_stream.c is self-contained C built against a vendored single-file zstd v1.5.7 (generated from the official repository's build/single_file_libs; BSD licensed — see zstd.LICENSE). One cc line, and it cross-compiles like any other C file:

$ aarch64-linux-gnu-gcc -O2 -o trace_stream trace_stream.c zstd.c