Skip to content

feat(symbols): map-derived synthesis for anonymous rodata pools (closes #425) - #427

Merged
zackees merged 4 commits into
mainfrom
feat/425-rodata-symbol-attribution
Jun 6, 2026
Merged

feat(symbols): map-derived synthesis for anonymous rodata pools (closes #425)#427
zackees merged 4 commits into
mainfrom
feat/425-rodata-symbol-attribution

Conversation

@zackees

Copy link
Copy Markdown
Member

Summary

Closes#425. Adds map-derived synthetic symbols to fbuild symbols so anonymous merged rodata blocks (the .rodata.<owner>.str1.<N> and .rodata.<owner>.cst<N> sub-sections that hold FL_WARN/FL_LOG and other compiler-emitted strings) are attributed to the owning function instead of stopping at the object file. Stacked on top of #424.

What changed

  • extract_owner_from_section() strips known prefixes (.text., .literal., .rodata., .data., .bss., .data.rel.ro., .gnu.linkonce.{t,r,d,b}.) and known suffixes (.str1.<N>, .cst<N>) to return the mangled owner of an input section.
  • collect_map_derived_owners() walks map ranges, drops any whose address is already covered by an nm-listed symbol, and returns the survivors with their extracted owner.
  • build_fine_grained_map_with_synth() emits synthetic FineGrainedSymbol rows tagged source: \"map-derived\".
  • New source: \"nm\" | \"map-derived\" field on FineGrainedSymbol with serde(default) so JSON written by earlier fbuild symbols still deserialises.
  • CLI driver demangles synthetic owners via c++filt in the same threaded batch as the nm names.

Empirical impact (ESP32-S3 Blink master ELF, the case that motivated #425)

MetricBefore this PRAfter
Sized symbols emitted3,6964,591 (+895 map-derived)
Flash bytes named at symbol level316,368388,380 (+72 KB)

Single largest new attribution: 58 KB of main.cpp.o rodata now correctly named as fl::ClocklessIdf5<3, TIMING_WS2812_800KHZ, GRB, 0, false, 280>::ClocklessIdf5 — the FL_WARN string pool pulled in by the NEOPIXEL chipset header. Other meaningful new resolutions:

BytesNow attributed to
432fl::XY(unsigned char, unsigned char) (libFastLED.a/fl.gfx+.cpp.o)
340printBeforeSetupInfo() (libFrameworkArduino.a/chip-debug-report.cpp.o)
144fl::ChannelEngineRMTImpl::attemptAllocationRecovery(...)
92fl::ChannelEngineRMTImpl::reconfigureForNetwork()
...rodata pools for many audio detector classes

Test plan

  • 11 new unit tests in fbuild-core::symbol_analysis::tests covering every recognised input-section shape, the no-double-count invariant when nm AND map both reference an address, range-overlap detection, and end-to-end synthesis through build_fine_grained_map.
  • 2 existing tests in fbuild-build::symbol_analyzer::tests still pass (toolchain prefix derivation).
  • All 24 unit tests across both crates pass.
  • uv run --script lint clean on touched files (rustfmt + clippy via fbuild's own gate).
  • Verified end-to-end on the ESP32-S3 master ELF (4,591 sized symbols, 388,380 B flash attributed at symbol level).

Out of scope

  • Bytes that genuinely don't carry an owner in their input-section name (catch-all .rodata blocks, .flash_rodata_dummy alignment padding) — these stay object-bucketed by design.
  • The map file also carries explicit vtable for ... annotation lines alongside .rodata._ZTV<class> sections; we already produce the right answer because _ZTV<class> demangles cleanly, but a follow-up could short-circuit the c++filt call when the map already gives us the demangled string.

🤖 Generated with Claude Code

#425)
`fbuild symbols` previously stopped at object-file granularity for
`.flash.rodata` because `nm` does not enumerate the anonymous merged
string pools that `-fdata-sections` puts in `.rodata.<mangled>.str1.<N>`
sub-sections. Concretely, ~377 KB of the ESP32-S3 Blink master ELF's
rodata fell back to "(archive, object, .flash.rodata) -> N bytes" rather
than "this function's strings -> N bytes".
The linker map carries the owner inline in the input-section name. This
PR adds:
- `extract_owner_from_section()` in `fbuild-core::symbol_analysis` that
recognises the input-section shapes that carry an owner:
`.text.<owner>`, `.literal.<owner>`, `.rodata.<owner>`,
`.rodata.<owner>.str1.<N>`, `.rodata.<owner>.cst<N>`,
`.data.<owner>`, `.bss.<owner>`, `.data.rel.ro.<owner>`,
`.gnu.linkonce.{t,r,d,b}.<owner>`.
- `collect_map_derived_owners()` that walks map ranges, skips any
range whose address is covered by an nm-listed symbol (so we don't
double-count), and returns the owner+size for each remaining range.
- `build_fine_grained_map_with_synth()` (and a thin wrapper for the
existing call site) that emits synthetic `FineGrainedSymbol` rows
for those owners, tagged `source: "map-derived"`.
- New `source: "nm" | "map-derived"` field on `FineGrainedSymbol`
with a `serde(default)` so JSON written by earlier `fbuild symbols`
still deserialises.
- The CLI driver (`fbuild-build::symbol_analyzer::analyze_elf`)
collects synthetic mangled owners and demangles them via c++filt
in the same threaded batch as the nm names.
Empirically on the same ESP32-S3 Blink master ELF that motivated #425:
| Metric | Before | After |
|---|---:|---:|
| Sized symbols emitted | 3,696 | 4,591 (+895 map-derived) |
| Flash bytes named at symbol level | 316,368 | 388,380 (+72 KB) |
The single largest new attribution: 58 KB of merged rodata in
`main.cpp.o` now correctly named as belonging to
`fl::ClocklessIdf5<3, fl::TIMING_WS2812_800KHZ, GRB, 0, false, 280>::ClocklessIdf5`,
which is the FL_WARN string pool pulled in by the NEOPIXEL chipset
header. Per-archive rodata for FastLED audio/channels/gfx/stl/net
detector classes also now resolves to function-level owners.
Tests
-----
11 new unit tests in `fbuild-core::symbol_analysis::tests` covering
all input-section shapes listed above, the no-double-count invariant,
range/coverage overlap logic, and the synthetic-row pipeline end to
end. Plus 2 existing tests in `fbuild-build::symbol_analyzer::tests`
(toolchain prefix derivation). All 24 unit tests across both crates
pass. Verified end-to-end on real ESP32-S3 ELFs.
Closes#425.
@coderabbitai

coderabbitaiBot commented Jun 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5109d790-f437-48de-a593-47794df0ee91

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/425-rodata-symbol-attribution

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 and usage tips.

zackees added 2 commits June 6, 2026 10:07
…OC gate
PR #427 surfaced the workspace's "Reject .rs files over 1000 LOC" gate
because the new map-derived synthesis code + its 13 unit tests pushed
crates/fbuild-core/src/symbol_analysis.rs to 1076 lines (limit: 1000).
Promote `symbol_analysis` from a single file to a directory module:
- crates/fbuild-core/src/symbol_analysis/mod.rs (695 LOC)
- crates/fbuild-core/src/symbol_analysis/tests.rs (383 LOC)
`tests.rs` carries the existing `mod tests` body verbatim — no test
content changed; the `use super::*;` makes private items like
`nm_range_covers` and `InputSectionIndex` still visible inside the
child module. 22/22 tests still pass.
Pure refactor; no behavior change to the public API.
…s' into feat/425-rodata-symbol-attribution
# Conflicts:
#	crates/fbuild-build/src/symbol_analyzer.rs
@zackees
zackees changed the base branch from feat/fine-grained-symbol-analysis to mainJune 6, 2026 18:07
@zackees
zackees merged commit 619d6ca into mainJun 6, 2026
2 checks passed
@zackees
zackees deleted the feat/425-rodata-symbol-attribution branch June 6, 2026 18:08
zackees added a commit that referenced this pull request Jun 6, 2026
…-S3 (#448)
The Phase 2 + Phase 3 acceptance gate from the #434 meta. Exercises
the full `fbuild build` → `fbuild bloat` flow against the latest
FastLED master + ESP32-S3 toolchain to verify:
- build_info.json carries the four #428 toolchain paths (nm_path,
cppfilt_path, readelf_path, objdump_path) and mirrors them onto the
PIO-shape `aliases` block.
- `fbuild bloat .` works zero-flag and lands `report.json` +
`report.md` at the documented Phase 3 path
`<project>/.fbuild/build/esp32s3/bloat-report/`.
- stdout prints both absolute paths on exit (Phase 3 invariant).
- Symbol counts, totals, and the >100 map-derived (#427) row count
are within bounds generous enough not to flake on FastLED master
drift, but tight enough to catch real regressions.
- Markdown output renders the top-flash table.
Marked `#[ignore]` so ordinary `cargo test` runs aren't network-
dependent. Wire into the nightly CI matrix via workflow_dispatch +
scheduled weekly to surface FastLED master regressions here too.
Closes#442.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

fbuild symbols: extract owning symbol from .rodata.<mangled>.str1.N input sections (377 KB of rodata currently buckets at object level)

1 participant

@zackees