Skip to content

bench(onthebench): pin the dwarf stack dump and flag flamegraphs that did not unwind - #965

Merged
membphis merged 3 commits into
mainfrom
bench/flamegraph-dwarf-stack
Aug 13, 2026
Merged

bench(onthebench): pin the dwarf stack dump and flag flamegraphs that did not unwind#965
membphis merged 3 commits into
mainfrom
bench/flamegraph-dwarf-stack

Conversation

@membphis

@membphis membphis commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

Two changes to the rig harness, both about the same failure: a flamegraph
that renders fine and is entirely wrong.

  1. Pin the per-sample stack dump to 32KB (BENCH_PERF_STACK, validated
    like the other BENCH_* knobs, forwarded to the rig by bench.sh).
  2. Check the collapsed output after rendering and warn when the capture
    is unusable.

Why

While taking the closing census for the performance program, a c=128
capture on a fat-LTO build came back with 95.5% of its stacks collapsed
to [unknown]
. perf's default dump is 8KB, which is smaller than this
gateway's async stacks under fat LTO, so libunwind had nothing to walk.

The failure is silent in every layer: perf record exits 0, perf script
produces output, inferno renders an SVG, and the picture reads as "the
kernel is 94% of the time". A re-capture of the same commit reproduced it
at 91.7%, so it is not a fluke. Attribution taken from such a graph is
wrong in a way that looks authoritative.

Same build, same load, after the fix:

before after
collapsed stacks 529 1876
resolved user-space share ~5% 48.4%

The threshold is measured, not picked

The check warns above 80% unresolved. That number comes from this
repo's own captures, not from judgement:

capture unresolved verdict
old rig, thin LTO + glibc 0.0% usable
this rig, thin LTO + glibc 37.0% usable
this rig, fat LTO, 32KB dump 51.1% usable
this rig, fat LTO (earlier scene) 56.6% usable
this rig, fat LTO, 8KB dump 91.7% failed
this rig, fat LTO, 8KB dump 95.5% failed

The two populations separate cleanly. A tighter threshold flags healthy
captures (the 57% one is a real, usable profile); a looser one lets the
failure through.

Verified by running the check against all six captures plus an empty file:
warns on exactly the two failed ones and the degenerate input, silent on
the four usable ones.

Why warn instead of abort

A bad flamegraph does not invalidate a run's throughput numbers - they come
from the measured windows, not from perf. Aborting a 15-minute measurement
over an auxiliary artifact would be the wrong trade. The share is printed
on every capture either way, so a run's log always states how much of its
own profile is trustworthy.

Note for anyone reading older profiles

Captures taken before this change were subject to the same effect. The two
at 92-96% are unusable outright; the ones in the 51-57% band are fine, but
their "kernel" share is an upper bound - deep stacks that failed to unwind
land in the same bucket.

Summary by CodeRabbit

  • New Features
    • Added configurable perf stack sizing for benchmark flamegraph captures.
    • Added validation for stack size values, including range and alignment requirements.
    • Added warnings when flamegraph stack symbols cannot be resolved reliably.
    • Documented the new benchmark override and its default constraints.

…megraph that did not unwind

A flamegraph taken on a fat-LTO build of the gateway came back with 95.5%
of its stacks collapsed to [unknown]: perf's default 8KB per-sample stack
dump is smaller than this gateway's async stacks, so libunwind had nothing
to walk. Nothing failed loudly - perf exited 0, inferno rendered an SVG,
and the result looked like a profile in which the kernel accounted for 94%
of the time. Any attribution taken from that picture would have been wrong.

Pin the dump to 32KB (BENCH_PERF_STACK, validated like the other knobs and
forwarded to the rig by bench.sh). On the same build and load this moves
the collapsed output from 529 to 1876 stacks and the resolved user-space
share from ~5% to 48%.

Size alone is not enough, because the failure is silent and the next build
may outgrow any fixed number. Check the collapsed output and say so when a
capture is unusable. The threshold is 80% unresolved, chosen from this
repo's own captures rather than picked: usable ones measure 0%, 37%, 51%
and 57% across two rigs and three build profiles, while the two failed
captures measure 92% and 96%. The check warns rather than aborts - the
throughput numbers of a run are unaffected by a bad flamegraph, and losing
a 15-minute measurement over an auxiliary artifact would be the wrong
trade.
Copilot AI balanced review requested due to automatic review settings August 12, 2026 17:59
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@membphis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 512f234c-8e06-4083-b0b2-204e7fc0758e

📥 Commits

Reviewing files that changed from the base of the PR and between bdb6242 and 5838749.

📒 Files selected for processing (2)
  • bench/onthebench/README.md
  • bench/onthebench/lib.sh
📝 Walkthrough

Walkthrough

The benchmark harness now supports BENCH_PERF_STACK. It validates the stack size, uses it for DWARF perf capture, and warns when collapsed stacks have insufficient symbolization.

Changes

Benchmark perf stack capture

Layer / File(s) Summary
Perf stack configuration and forwarding
bench/onthebench/lib.sh, bench/onthebench/bench.sh, bench/onthebench/README.md
BENCH_PERF_STACK sets PERF_STACK, defaults to 32768, is forwarded to remote runs, and is documented with its validation constraints.
Capture validation and symbolization check
bench/onthebench/lib.sh
PERF_STACK must be positive, divisible by 8, and no greater than 65528. Flamegraph capture uses the configured value. check_symbolization warns when unresolved stacks exceed 80%.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant bench.sh
  participant lib.sh
  participant perf
  participant check_symbolization
  bench.sh->>lib.sh: Forward BENCH_PERF_STACK
  lib.sh->>lib.sh: Set and validate PERF_STACK
  lib.sh->>perf: Capture DWARF stacks with PERF_STACK
  lib.sh->>check_symbolization: Check collapsed flamegraph data
Loading

Possibly related PRs

  • api7/aisix#917: Introduced the benchmark harness extended by this change.
  • api7/aisix#919: Introduced the flamegraph handling extended by this change.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The PR adds only README and shell changes. No benchmark E2E test or fixture covers bench.sh → remote runner → perf/inferno; manual checks in the description are not reproducible tests. Add a reproducible E2E test that verifies BENCH_PERF_STACK forwarding and validation, perf capture, collapsed-output warnings, empty input, and healthy/unresolved boundaries.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both main changes: pinning the DWARF stack dump and flagging flamegraphs with unresolved stacks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed The diff only adds numeric perf-stack validation/forwarding and aggregate flamegraph warnings; it introduces no credential exposure, plaintext storage, authorization, ownership, TLS, isolation, or...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bench/flamegraph-dwarf-stack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Pins perf’s DWARF stack dump size and detects unusable flamegraphs with excessive unresolved stacks.

Changes:

  • Adds and validates BENCH_PERF_STACK, forwarding it to the rig.
  • Uses the configured stack size during perf record.
  • Warns when collapsed output exceeds 80% unresolved stacks.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
bench/onthebench/README.md Documents the new stack-size setting.
bench/onthebench/lib.sh Configures stack capture and validates symbolization.
bench/onthebench/bench.sh Forwards the setting to the rig.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bench/onthebench/lib.sh
# renders as one flat bar. That failure is silent (perf exits 0, inferno
# renders happily), which is why the size is pinned here rather than left
# to the default, and why the collapse output is checked below.
perf record -F 499 --call-graph "dwarf,$PERF_STACK" -p "$GW_PID" -o "$OUT/perf.data" -- sleep 25 \

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bench/onthebench/lib.sh`:
- Line 362: Move check_symbolization to execute immediately after
inferno-collapse-perf succeeds, before the SVG rendering step. Keep SVG
generation independent so symbolization results are reported whenever the folded
file is created, even if inferno-flamegraph fails.

In `@bench/onthebench/README.md`:
- Around line 83-85: Update the flamegraph reproduction command in the README
methodology section to include the configured dwarf stack size, matching
lib.sh’s --call-graph "dwarf,$PERF_STACK" behavior; either show the 32768-byte
default or reference BENCH_PERF_STACK instead of using bare --call-graph dwarf.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5119a978-5902-4379-ab5c-16dfbda79cb7

📥 Commits

Reviewing files that changed from the base of the PR and between cad6fe9 and bdb6242.

📒 Files selected for processing (3)
  • bench/onthebench/README.md
  • bench/onthebench/bench.sh
  • bench/onthebench/lib.sh

Comment thread bench/onthebench/lib.sh
Comment thread bench/onthebench/README.md
…ct the cause

Independent review of the previous commit found the mechanism sound and the
framing wrong. Corrections:

The evidence base was six captures out of forty-seven. Over the whole set
there are four failures (88.1%, 90.0%, 91.7%, 95.5%), not two, and the
highest passing capture is 71.0%, not 57%. The threshold still separates the
two populations, but on a 17-point gap rather than the 35 claimed.

The stated cause was backwards. Fat LTO does not make stacks too deep for
the 8KB default: the first fat-LTO capture in the corpus is healthy at 43.2%
and the two worst failures are a thin-LTO build. The same commit has also
produced both a 42% and a 71% capture. 32KB is a mitigation, not a
diagnosis, and the comments now say so - the measurement is what carries the
guarantee.

Neither the knob nor the result reached anything durable. BENCH_PERF_STACK
is now recorded in meta.json alongside every other method knob, which the
README already promised for a changed knob, and the measured share is
written to perf.log, which is collected off the rig - the reader who most
needs it is the one opening the SVG months later, not the operator watching
the run.

Also: measure the leaf frames as a second share. "No symbol at any depth"
passes a capture whose leaves are nearly all anonymous, and the leaf is what
a flamegraph attributes self-time to; on the same corpus the leaf share
separates 79.3% (worst passing) from 90.9% (best failing). Warn a free-space
shortfall before a capture that now writes about four times the perf.data.
Guard the awk against an unreadable file, distinguish an empty capture from
a collapsed one, drop a non-POSIX awk builtin, and fix a comment that had
perf's rounding behaviour wrong in both directions.
@membphis

Copy link
Copy Markdown
Contributor Author

Independent audit — findings and disposition

Cold-start review by an agent with no context on this work. It reproduced
every number independently and found the mechanism sound and the framing
wrong in three places. All findings fixed in a0e4bec.

HIGH

H1 — the new knob was not recorded in meta.json, while this PR extended a
README sentence promising that it is.
A run with BENCH_PERF_STACK=8192
would have been archivally indistinguishable from a default run — the exact
failure this PR exists to prevent. Fixed: stack_bytes and the real
callgraph string now land in meta.json.

H2 — the evidence table was six captures out of forty-seven. The full
corpus has four failures, not two:

capture unresolved in the original table?
todo10 finale, aisix-final 95.5% yes
todo10 finale, re-capture 91.7% yes
todo7 A/B base-back 90.0% no
todo7 A/B base-front 88.1% no
issue941 A/B base-front 71.0% (highest passing) no

The six quoted numbers reproduce exactly, so the table was not wrong — it was
incomplete in a way that flattered the margin. The real separation is
71 → 88 (17 points), not 57 → 92 (35). The threshold still sits in the
gap, but "a tighter threshold would flag healthy captures" holds down to ~72,
not ~58. The closing note about older profiles was also under-reporting the
damage and is corrected.

MEDIUM

M1 — the stated cause was backwards. Fat LTO does not make stacks too deep
for the 8KB default. The first fat-LTO capture in the corpus (cea29be) is
healthy at 43.2%, and the two worst failures are its thin-LTO base
(2cbf39c). The same commit has produced both a 42% and a 71% capture, so
there is a large capture-to-capture component. Verified independently:
git show 2cbf39c:Cargo.tomllto = "thin", cea29belto = "fat".
32768 is a mitigation, not a diagnosis — the measurement is what carries
the guarantee. Comments and this description corrected.

M2 — the measured share reached no collected artifact. It went to stderr
of the on-rig runner, i.e. to whoever was watching the terminal. perf.log
is rsynced back; the share is written there too now.

M3 — "a symbol at any depth" is a weak signal. A passing capture in the
corpus measures 71.0% by that metric but 93.1% per frame. The leaf frame is
what a flamegraph attributes self-time to, and separates the corpus more
widely (79.3% worst passing vs 90.9% best failing). Both shares are now
measured and either can trigger the warning.

M4 — a 32KB dump writes ~4x the perf.data (~1.5GB per capture, measured
from archived perf.logs), on a shared rig that has run out of disk before.
Free-space warning added before the capture.

LOW

L1 comment had perf's rounding behaviour wrong in both directions (it rounds
up, and hard-errors above the bound rather than capping) — corrected against
callchain.c: get_stack_size(). L2 the awk assignment could abort the run
if the file were unreadable — guarded. L3 length() on a split array is a
gawk/mawk extension — now uses the return of split. L4 README still showed
the old perf invocation. L5 an empty capture now says so instead of
reporting "100% unresolved" like a collapsed one.

Verified correct, left alone

The awk && echo idiom under set -euo pipefail, the validation chain's
precedence (tested 32768/8/65528 accepted; 65529/65536/0/-8/abc/$(id)
rejected), --call-graph dwarf,N syntax and the 65528 bound, $NF against
frame names containing spaces, and the i = 2 start being load-bearing
rather than cosmetic.

Re-validated after the fixes: all four failures warn, the 71.0% capture
passes, and the share appears in perf.log.

…ollapses

The symbolization check sat inside the branch that required both the
collapse and the SVG render to succeed. A capture that collapsed but failed
to render therefore reported nothing - losing exactly the diagnostic that
explains why that capture is worth nothing, in the case where something has
already gone wrong.

Split the two steps: check the folded file as soon as it exists, render
after. A failed render still keeps perf.data, and a failed collapse now says
so rather than reporting a rendering failure that never happened.
@membphis

Copy link
Copy Markdown
Contributor Author

Bot review disposition

Copilot — meta_method_json records only callgraph: "dwarf", contradicting the README's promise that a changed knob is recorded. Agreed and fixed in a0e4bec; meta.json now carries "callgraph": "dwarf,<n>" and "stack_bytes": <n>. The independent audit raised this as its only HIGH, from the same reasoning — a run with a non-default stack size was archivally indistinguishable from a default one, which is the failure this PR exists to prevent.

CodeRabbit — run the symbolization check after collapse succeeds, not after rendering. Correct, and fixed in 5838749. The check sat inside a branch requiring both inferno-collapse-perf and inferno-flamegraph to succeed, so a capture that collapsed but failed to render reported nothing — losing the diagnostic precisely when something had already gone wrong. Collapse and render are now separate steps; a failed collapse also stops reporting itself as a rendering failure.

Verified with a stubbed toolchain (collapse succeeds, render exits 3):

  flamegraph: 90.0% unresolved stacks, 90.0% unresolved leaves
WARNING: ... unwound poorly - do not attribute from this flamegraph
WARNING: flamegraph rendering failed; perf.data kept

The share reaches both the terminal and the collected perf.log, and perf.data is retained for an off-rig retry.

CodeRabbit — README still shows the old perf record invocation. Fixed in a0e4bec (CodeRabbit has since marked it addressed).

All three bot findings are fixed in code; none were dismissed.

@membphis

Copy link
Copy Markdown
Contributor Author

Verified on the rig

Ran on the real Graviton box (c=128 saturation, fail=0), not a stubbed toolchain:

before (same commit, 8KB default) after (dwarf,32768)
collapsed stacks 529 2,093
unresolved share 95.5% / 91.7% (two captures) 57.8% (leaves 69.3%)
guard verdict would warn silent — 57.8% sits inside the usable band
meta.json "callgraph": "dwarf" only "callgraph": "dwarf,32768", "stack_bytes": 32768

Three things confirmed at once on live traffic: the larger dump genuinely
changes unwinding (4x the stacks, unresolved 96% → 58%), the 80% threshold
does not false-positive on a healthy capture from this rig, and the knob
reaches the archived metadata — which was the audit's HIGH finding.

@membphis
membphis merged commit d51fe78 into main Aug 13, 2026
13 checks passed
@membphis
membphis deleted the bench/flamegraph-dwarf-stack branch August 13, 2026 00:20
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.

2 participants