Skip to content

feat: bring CodSpeed profiling support and validated stack cache to main - #15

Draft
not-matthias wants to merge 5 commits into
codspeedfrom
codspeed-stack-cache
Draft

not-matthias wants to merge 5 commits into
codspeedfrom
codspeed-stack-cache

Conversation

@not-matthias

Copy link
Copy Markdown
Member

Bring the codspeed branch onto main, including validated stack-cache continuation for Linux DWARF unwinding.

The cache keeps per-thread stack snapshots and only continues beyond a captured stack window when the stable overlap matches. Returned frames and exited threads retire snapshots, preventing stale stack words from splicing unrelated call paths. A recorded replay fixture exercises this failure mode.

This also carries the existing CodSpeed fork changes: library entry points and symbol configuration, macOS timestamps and preload handling, extra perf events and system-wide capture fallback, frame-pointer recovery, V8 inline-frame recovery, JIT-dump validation, and debug-info retry settings.

The incomplete-stack labels remain a separate change in #13, stacked on this PR.

When a binary is found through the binary lookup dirs, the resolved path
uses the host's separators. Splitting it at the last '/' kept the whole
path as the library name on Windows. Path::file_name handles both.
tools/stack-cache-repro is a small program whose stacks are deeper than
samply's 32000-byte user stack copy. Phase A recurses and works at every
depth, then phase B recurses to the same depth through a different function
and works only in its leaf, so phase B's frames above the copy sit where
phase A's were seen earlier.

replay.sh records it once with perf and imports the same perf.data with each
given samply binary, so every build sees identical samples. stats.py counts
stitched stacks (a phase B leaf under phase A's recursion, or the reverse)
and exits non-zero if there are any.

make-fixture.sh records a short run as a static binary into
fixtures/other/stack-read-cache/, so a replay test can check the
converter without perf permissions.
…shot

Samples copy 32000 bytes of the user stack, starting at the sampled SP.
The stack read cache answered unwinder reads above that copy with the word
that some earlier sample of the process had seen at the same address. The
thread has since returned and called other code at those addresses, so the
unwinder followed stale return addresses and spliced an old call chain onto
the current stack, e.g. RocksDB and serde frames under a pure in-memory
recursive function in jazz profiles.

Keep the last 64 captured windows per thread instead of a per-process
address -> word map. A read above the current window is served by an
earlier snapshot only if that snapshot starts inside the window and every
word from its stable start (the lowest slot its own unwind read; below that
are the leaf's live locals) up to the end of the window matches the current
sample, over at least 512 words. Past that snapshot the same check chains
to an older one, so each part of the stack comes from one earlier sample.
Otherwise the stack is truncated.

This is still a heuristic: a snapshot whose overlap is identical but whose
callers above it differ is accepted.

Tests:
- unit tests on StackSnapshot::continues cover the 512-word threshold and
  that only words from the stable start upward must match;
- tests/stack_read_cache_replay.rs imports the fixtures/other/stack-read-cache
  recording of tools/stack-cache-repro and checks that no stack is spliced
  across phases while phase A is still completed to its full depth. The old
  cache splices all 14 phase B stacks of this fixture.
The stack snapshots are keyed by tid, so a new thread that reuses an exited
thread's tid (and possibly its stack mapping) could otherwise continue
through the old thread's windows. Remove them when a non-main thread exits
or execs; main-thread exit and exec already recreate the whole process.
Rust 1.98 adds useless_borrows_in_formatting and a format-width check, and
making the samply modules public enabled the new_without_default,
should_implement_trait and result_unit_err lints on their APIs.
@not-matthias
not-matthias added this pull request to stack #16 October 30, 2026 13:54
@not-matthias
not-matthias removed this pull request from stack #16 October 30, 2026 13:56
@not-matthias
not-matthias changed the base branch from main to codspeed October 30, 2026 13:56
@not-matthias
not-matthias added this pull request to stack #17 October 30, 2026 13:56
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