build: keep the symbol table in release binaries for field profiling - #849
Conversation
The release profile strips all symbols (strip = "symbols"), so sampling profilers degrade to address-only flame graphs. Add a dedicated profile that keeps release codegen (thin LTO, codegen-units = 1) while retaining the symbol table and line tables: - debug = "line-tables-only": file:line info for readable frames at a fraction of full debug-info size - strip = "none": override the inherited symbol stripping Usage: cargo flamegraph --profile profiling --bin aisix-server Refs #847
📝 WalkthroughWalkthroughUpdates release builds to retain line-table and symbol information, and adds pull-request verification that the container’s ChangesProfiling-ready release artifacts
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
The release profile strips all symbols (strip = "symbols"), so any sampling profiler degrades to address-only stacks on release binaries. Add a profiling profile that inherits the release codegen (thin LTO, codegen-units = 1) and keeps the symbol table plus line tables, so the sampled hotspots match production behavior while stacks stay readable. Refs #847
aisix ships as a single binary and performance problems surface on the build users actually run, so the shipped artifact must be profileable in the field (perf / cargo flamegraph, issue #847). Replace the opt-in profiling profile from the previous commit with the debug surface on release itself: - debug = "line-tables-only": file:line info for readable frames - strip = "none" (was "symbols"): keep the symbol table Codegen is unchanged (thin LTO, codegen-units = 1). Binary size grows about 23 percent on macOS (49.6 MiB vs 40.4 MiB stripped), dominated by the symbol table (about 42k text symbols); expect somewhat more on Linux where line tables embed in the binary. Refs #847
The release profile now keeps the symbol table and DWARF line tables so field deployments stay profileable (#847). Pin that contract in the docker PR job: extract the binary from the built image, require text symbols and a .debug_line section, and print the size so every PR records the real Linux artifact cost. A future strip step, RUSTFLAGS change, or base-image swap would otherwise silently break flame graphs. Refs #847
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/docker-image.yml (1)
201-205: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winPass action output through
envbefore using it in Bash.Line 205 expands the metadata output directly into generated shell source. GitHub recommends environment variables for expression values in inline scripts; the current PR tag configuration normally yields a sanitized
pr-<number>, so this is future-proofing rather than a demonstrated exploit. (docs.github.com)Suggested hardening
- name: Verify shipped binary keeps symbols + line tables (`#847`) + env: + IMAGE_TAGS: ${{ steps.meta.outputs.tags }} if: github.event_name == 'pull_request' run: | set -eux - IMAGE="$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1)" + IMAGE="$(printf '%s\n' "$IMAGE_TAGS" | head -n1)"🤖 Prompt for 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. In @.github/workflows/docker-image.yml around lines 201 - 205, Update the “Verify shipped binary keeps symbols + line tables (`#847`)” step to pass steps.meta.outputs.tags through the step’s env configuration, then read that environment variable in the Bash command instead of interpolating the GitHub expression directly into shell source.Source: Linters/SAST tools
🤖 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 @.github/workflows/docker-image.yml:
- Around line 210-211: Update the profiling verification command near the
existing nm check so `.debug_line`, `.debug_info`, and `.symtab` are each
validated independently rather than through one alternation-based grep. Require
every artifact check to succeed while preserving the existing symbol-count
validation.
---
Nitpick comments:
In @.github/workflows/docker-image.yml:
- Around line 201-205: Update the “Verify shipped binary keeps symbols + line
tables (`#847`)” step to pass steps.meta.outputs.tags through the step’s env
configuration, then read that environment variable in the Bash command instead
of interpolating the GitHub expression directly into shell source.
🪄 Autofix (Beta)
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: 59d54687-8384-4c65-8851-82d660c8c892
📒 Files selected for processing (2)
.github/workflows/docker-image.ymlCargo.toml
| test "$(nm /tmp/aisix-shipped | grep -c ' [tT] ')" -gt 1000 | ||
| readelf -S /tmp/aisix-shipped | grep -E '\.debug_line|\.debug_info|\.symtab' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Require both profiling sections independently.
The alternation on Line 211 passes when only one of .debug_line, .debug_info, or .symtab exists. A binary missing DWARF line tables could therefore pass the profiling guard.
Suggested fix
- readelf -S /tmp/aisix-shipped | grep -E '\.debug_line|\.debug_info|\.symtab'
+ readelf -S /tmp/aisix-shipped | grep -q '\.symtab'
+ readelf -S /tmp/aisix-shipped | grep -q '\.debug_line'As per coding guidelines, verification tests must define verifiable success criteria for every required artifact.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test "$(nm /tmp/aisix-shipped | grep -c ' [tT] ')" -gt 1000 | |
| readelf -S /tmp/aisix-shipped | grep -E '\.debug_line|\.debug_info|\.symtab' | |
| test "$(nm /tmp/aisix-shipped | grep -c ' [tT] ')" -gt 1000 | |
| readelf -S /tmp/aisix-shipped | grep -q '\.symtab' | |
| readelf -S /tmp/aisix-shipped | grep -q '\.debug_line' |
🤖 Prompt for 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.
In @.github/workflows/docker-image.yml around lines 210 - 211, Update the
profiling verification command near the existing nm check so `.debug_line`,
`.debug_info`, and `.symtab` are each validated independently rather than
through one alternation-based grep. Require every artifact check to succeed
while preserving the existing symbol-count validation.
Source: Coding guidelines
Function-level frames are what field flame graphs need, and the symbol table delivers them at roughly +10 MiB over a fully stripped binary. DWARF stays out: line tables alone measured ~142 MB here because codegen-units = 1 plus thin LTO explodes inlined-instance records — not worth file:line attribution in every shipped image. strip = "debuginfo" (the modern cargo default, pinned explicitly) keeps .symtab and drops all .debug_* sections, including those from the precompiled std. An ad-hoc line-level build stays one env override away; the manifest comment documents it. Refs #847
…ore #849 Audit follow-ups on the unlimited-default change: - The passthrough tunnel's manual to_bytes still used the raw limit, so the new 0 default rejected EVERY passthrough body with '413 request body exceeds 0-byte limit' (reproduced by the audit). It now goes through body_read_cap and discriminates the length-limit error from transport faults — a real cap hit is the enveloped 413, a read failure is a 400, matching /mcp and /a2a. Regression tests for both, plus chunked-oversize coverage for /mcp and /a2a. - Restores Cargo.toml strip = 'debuginfo' and the docker-image.yml symbol-table guard from #849: the previous commit was assembled with 'git reset --soft origin/main' after the remote ref had advanced past the working tree's base, so its tree silently reverted that commit. No intended change there.
What
Keeps the symbol table in release binaries (
strip = "symbols"→strip = "debuginfo"), so sampling profilers (cargo-flamegraph, samply, perf) produce readable function-level on-CPU flame graphs against the exact build users run. All DWARF stays out of the shipped artifact. The docker PR job now guards this contract.Part of #847.
Why
aisix ships as a single binary, and performance problems surface on the build users actually run — asking a user to reproduce on a special diagnostic build is not a workable support model. With
strip = "symbols", shipped binaries carried no symbol table at all, so a flame graph taken against a deployment degraded to bare-address towers.Two earlier revisions of this PR were rejected in review:
[profile.profiling]— rejected because the shipped artifact itself must be the profileable one.debug = "line-tables-only"+strip = "none"on release — rejected on measured cost: withcodegen-units = 1+ thin LTO, the line tables alone measure ~142 MB (~200k inlined-instance records), roughly quadrupling the shipped binary for file:line attribution that hotspot work rarely needs.Final shape: symbol table only. Function-level frames cover field hotspot profiling, and
.eh_frame(always present — panic unwinding needs it) keeps DWARF-based stack walking in perf working. When a deep dive needs file:line and inline attribution, an ad-hoc build is one override away (documented in the manifest comment):strip = "debuginfo"is the modern cargo default for this situation, pinned explicitly so the contract survives toolchain-default drift; it also drops the DWARF that the precompiled std would otherwise leak into the binary.Cost / impact
:devimage baseline, fully stripped) → 53.8 MiB — +7.1 MiB / +15.2%, CI-measured by this PR's docker job. The macOS local build agrees (+6.0 MiB / +14.9%). For contrast, the rejected line-tables revision measured roughly 4x.lto = "thin",codegen-units = 1)rodata(panic/tracing metadata) ship today already and are unaffected either way;--remap-path-prefixis tracked in Establish the on-CPU profiling workflow: adopt cargo-flamegraph and add a dedicated profiling build profile #847 as a follow-up for both.CI guard
docker-image.yml's PR path now extracts/usr/local/bin/aisixfrom the built image, asserts the text-symbol count, and prints the artifact size — a future Dockerfile strip step, RUSTFLAGS change, or base-image swap cannot silently break field profiling again, and every PR records the real Linux artifact cost.Usage
Verification
.eh_frame/.eh_frame_hdrpresent (perf's DWARF unwinding needs them)cargo flamegraphproduced a fully symbolized graph: 2,747 unique frames, zero address-only frames, 269 aisix-crate frames (aisix_proxy::chat::*,aisix_obs::metrics::*, provider bridge frames all readable)The remaining #847 checklist items (a Linux perf session, rust-lld
--no-rosegmentverification, the internal guide) stay tracked in the issue.