Skip to content

feat(memtrack): delta-encode stack captures in eBPF - #547

Draft
not-matthias wants to merge 4 commits into
feat/memtrack-ring-statsfrom
feat/memtrack-stack-compression
Draft

not-matthias wants to merge 4 commits into
feat/memtrack-ring-statsfrom
feat/memtrack-stack-compression

Conversation

@not-matthias

Copy link
Copy Markdown
Member

Exact-hash dedup removes almost no stack records: consecutive captures of the same thread differ in a few words near sp, so nearly every 8 KiB copy reaches the stack ring. This PR delta-encodes the captures in eBPF and decodes them in userspace.

How it works

  • Kernel: each thread's last emitted copy is kept in a per-tid LRU map (stack_refs). A new copy is XORed against it, aligned by absolute address. The record carries a word bitmap per 512 B group plus the nonzero words, and unchanged groups are omitted. Registers are XOR-encoded the same way.
    • A thread with no reference encodes against the zeroed slot; ref_hash == 0 marks this keyframe.
    • The reference moves only after bpf_ringbuf_output succeeds, so it always matches the last record userspace got.
    • The encoder is branch-free per word so the verifier walks it once: 56K instructions at 8 KiB, 217K at 32 KiB.
  • Userspace (stack_codec): the decoder checks ref_hash and then the FNV hash of the rebuilt bytes. On a mismatch it drops the record, and the poller deletes stack_refs[tid] and the stack's dedup marker. The next capture is a keyframe and the lost stack is emitted again.
  • Raw records and CODSPEED_MEMTRACK_STACK_COMPRESSION=0 stay as a kill switch. stack_refs is shrunk to one entry when compression is off.

Cost

32-core x86_64, 5 runs per arm, medians. Python allocation loop, ~1.07M stack records:

Budget Arm Total BPF time malloc probe Stack ring writes Wall time
– capture off 500 ms 42 ns – 1.48 s
8 KiB raw 3709 ms 1683 ns 3341 MB/s 4.98 s
8 KiB delta 3507 ms 1700 ns 71 MB/s 4.64 s
32 KiB raw 4902 ms 2127 ns 9406 MB/s 6.91 s
32 KiB delta 3629 ms 1738 ns 72 MB/s 4.73 s
  • On a small churn fixture (5201 records), delta adds about 25% BPF time per capture over raw, while ring writes drop from 416 to 23 MB/s.
  • Records are 315–515 B instead of 7–8.5 KiB. Offline replay of 100k recorded coreutils stacks gives 20.3x.
  • Userspace decoding takes about 1.1 µs per record.

Known limits

  • stack_refs preallocates 512 × ~100 KB ≈ 51 MB while compression is on.
  • With more than 512 live threads, LRU eviction forces extra keyframes.

Testing

  • Codec unit tests: keyframe round-trip, stale reference, hash mismatch.
  • stack_compression_tests (new fixture stack_churn.c) at 8 KiB and 32 KiB: 8002 delta records each, every allocation hash resolves to a decoded record whose bytes hash back to the kernel value.
  • stack_budget_tests now loads both BPF variants with capture and compression enabled; stack_tests snapshots are unchanged.

Exact-hash dedup removes almost no stack records: consecutive captures of
one thread differ in a handful of inner-frame words, so every 8 KiB copy
reaches the ring. Keep the previous record per tid in a BPF map, XOR the
new copy against it aligned by absolute address, and emit only a bitmap
per 512-byte group plus the nonzero words, skipping unchanged groups.
Registers are XOR-encoded the same way. A thread without a reference
encodes against the zeroed slot, marked by ref_hash 0 (a keyframe).
Offline this yields ~20x smaller ring records on real runs.

The record is emitted with bpf_ringbuf_output at its exact size. The
encoder is branch-free per word and merges verifier states before the
loop, so the verifier walks it once: 56K instructions at 8 KiB, 217K at
the 32 KiB maximum.
Mirror of the kernel encoder: per-tid references, reference-hash check,
and an FNV check of the reconstructed bytes so a desynchronised reference
is detected instead of producing a wrong stack. The encoder half is a
byte-exact mirror used by the tests and by the stack_codec_ratio example,
which replays recorded stack dumps through both halves.
Raw records are parsed in the ring callback; delta records are copied
once and decoded on the resolver thread. A decode failure drops the
record and deletes the kernel reference and the stack's dedup marker, so
the next capture of that thread is a keyframe and the lost stack is
emitted again. The decoder logs its failure count at shutdown.
CODSPEED_MEMTRACK_STACK_COMPRESSION=0 keeps the raw records.
The load test now enables capture: with it frozen off the verifier prunes
the capture path and proves nothing about the budget. The new suite runs
a fixture with thousands of near-identical stacks and requires every
allocation hash to resolve to a decoded record whose bytes hash back to
the kernel's value.
@codspeed

codspeed Bot commented Sep 24, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 33 untouched benchmarks
⏩ 4 skipped benchmarks1


Comparing feat/memtrack-stack-compression (91f9a83) with feat/memtrack-ring-stats (a3e281d)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant