Skip to content

build: keep the symbol table in release binaries for field profiling - #849

Merged
membphis merged 5 commits into
mainfrom
profiling-build-profile
Jul 30, 2026
Merged

build: keep the symbol table in release binaries for field profiling#849
membphis merged 5 commits into
mainfrom
profiling-build-profile

Conversation

@membphis

@membphis membphis commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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:

  1. A separate opt-in [profile.profiling] — rejected because the shipped artifact itself must be the profileable one.
  2. debug = "line-tables-only" + strip = "none" on release — rejected on measured cost: with codegen-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):

CARGO_PROFILE_RELEASE_DEBUG=line-tables-only CARGO_PROFILE_RELEASE_STRIP=none cargo build --release

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

  • Binary size: shipped Linux binary goes 46.7 MiB (pre-PR :dev image 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.
  • Runtime: none — the symbol table lives in non-loadable sections; codegen flags are untouched (lto = "thin", codegen-units = 1)
  • Shipped binaries now expose internal function names; accepted as the support-model tradeoff. Build-path strings in rodata (panic/tracing metadata) ship today already and are unaffected either way; --remap-path-prefix is 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/aisix from 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

cargo flamegraph --bin aisix -- <args>   # release is cargo flamegraph's default profile
flamegraph --pid <aisix-pid>             # attach to a running deployment

Verification

  • CI: the new "Verify shipped binary keeps its symbol table (Establish the on-CPU profiling workflow: adopt cargo-flamegraph and add a dedicated profiling build profile #847)" step passed on this head — Linux artifact 56,424,392 bytes, 42,290 text symbols, .eh_frame/.eh_frame_hdr present (perf's DWARF unwinding needs them)
  • Local: full release build (46.4 MiB, 42,029 text symbols on macOS), then an end-to-end profiling session against this exact build — mock upstream, 32-connection load, 380k requests at 12.7k req/s, cargo flamegraph produced 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-rosegment verification, the internal guide) stay tracked in the issue.

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
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Updates release builds to retain line-table and symbol information, and adds pull-request verification that the container’s aisix binary preserves profiling data.

Changes

Profiling-ready release artifacts

Layer / File(s) Summary
Update release profiling settings
Cargo.toml
The release profile retains line tables and disables stripping; the separate profiling profile is removed.
Verify container binary profiling data
.github/workflows/docker-image.yml
Pull-request workflow checks the container binary’s symbol table and DWARF debug sections.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: jarvis9443, moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
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.
E2e Test Quality Review ✅ Passed PASS: The PR-only check exercises the real Docker build-to-image path and validates the shipped binary inside the container with clear, explicit assertions.
Security Check ✅ Passed No security issues found: the changes only alter build/debug settings and add CI checks; they don't log, persist, or expose secrets.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: retaining symbols in release binaries for profiling.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch profiling-build-profile

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.

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
@membphis membphis changed the title build: add a profiling cargo profile for on-CPU flame graphs build: keep symbols and line tables in release binaries for field profiling Jul 30, 2026
@membphis
membphis marked this pull request as ready for review July 30, 2026 08:40
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

@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: 1

🧹 Nitpick comments (1)
.github/workflows/docker-image.yml (1)

201-205: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pass action output through env before 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

📥 Commits

Reviewing files that changed from the base of the PR and between 57530dd and fafa2f1.

📒 Files selected for processing (2)
  • .github/workflows/docker-image.yml
  • Cargo.toml

Comment thread .github/workflows/docker-image.yml Outdated
Comment on lines +210 to +211
test "$(nm /tmp/aisix-shipped | grep -c ' [tT] ')" -gt 1000
readelf -S /tmp/aisix-shipped | grep -E '\.debug_line|\.debug_info|\.symtab'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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
@membphis membphis changed the title build: keep symbols and line tables in release binaries for field profiling build: keep the symbol table in release binaries for field profiling Jul 30, 2026
@membphis
membphis merged commit 0866202 into main Jul 30, 2026
12 checks passed
@membphis
membphis deleted the profiling-build-profile branch July 30, 2026 09:31
jarvis9443 added a commit that referenced this pull request Jul 30, 2026
…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.
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