Skip to content

feat(bloat): per-symbol back-references + single-symbol lookup (#478) - #480

Merged
zackees merged 1 commit into
mainfrom
feat/issue-478-per-symbol-callers
Jun 7, 2026
Merged

feat(bloat): per-symbol back-references + single-symbol lookup (#478)#480
zackees merged 1 commit into
mainfrom
feat/issue-478-per-symbol-callers

Conversation

@zackees

@zackeeszackees commented Jun 7, 2026

Copy link
Copy Markdown
Member

Closes#478.

Summary

Two complementary capabilities so AI optimisation passes can answer
"who calls this specific symbol?" and "what does this specific
symbol cost?"
with per-symbol precision (not TU-level).

  • FineGrainedSymbol.called_by — per-symbol inverse of
    references_to, populated by inverting the existing objdump-derived
    forward map (callgraph::invert) in run_objdump_and_attribute.
    Single objdump pass produces both directions.
  • NodeKind::Caller + walk_backward_per_symbol — graph walker
    mirror of walk_forward. When called_by is non-empty the graph
    uses per-symbol back-edges (more precise); falls back to the
    TU-level walker when empty (older reports, vtable-only calls).
  • Top callers markdown sub-table — sibling of the existing
    Top callees table, dual-ranked by caller flash size and by caller
    breadth (callees-count, proxy for downstream-leverage).
  • fbuild bloat lookup <input> --symbol <demangled> — focused
    per-symbol block (size, archive, object, region, per-symbol callers,
    per-symbol callees, TU-level referencers). --symbol-mangled for
    exact mangled lookup, --json for machine consumption. Substring
    search falls back from exact-demangled-first → ambiguous list.
  • FineGrainedSymbolMap::find_symbol with SymbolQuery /
    SymbolLookup — programmatic API behind the CLI.

Test plan

  • find_symbol_dispatches_correctly — exact/substring/ambiguous/unique/miss/mangled
  • called_by_roundtrips_via_serde_with_default — JSON round-trip + back-compat for older reports
  • backward_uses_per_symbol_when_called_by_populated — graph walker takes per-symbol path
  • backward_falls_back_to_tu_when_called_by_empty — TU walker still runs as fallback
  • rank_callers_dual_sorts_each_axis_independently — both rankings work
  • soldr cargo check --workspace --all-targets
  • soldr cargo clippy --workspace --all-targets -- -D warnings
  • soldr cargo fmt --all -- --check
  • RUSTDOCFLAGS=-D warnings soldr cargo doc --workspace --no-deps
  • soldr cargo test -p fbuild-core --lib symbol_analysis:: (72 passed)
  • soldr cargo test -p fbuild-build --lib symbol_analyzer (17 passed)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added fbuild bloat lookup subcommand to search and analyze specific symbols
    • Symbol lookup via exact or substring matching on demangled and mangled names
    • Display direct callers and callees for symbols with optional JSON output
    • Enhanced symbol analysis with backward callgraph relationships showing which symbols call into a target symbol

Adds two complementary capabilities to the bloat-analysis pipeline so
AI optimisation passes can answer "who calls this specific symbol?"
and "what does this specific symbol cost?" with per-symbol precision.
Schema
- `FineGrainedSymbol.called_by: Vec<String>` — per-symbol inverse of
`references_to`, populated by inverting the existing objdump-derived
forward map (`callgraph::invert`) in `run_objdump_and_attribute`.
Single objdump pass produces both directions.
- `SymbolQuery` + `SymbolLookup` + `FineGrainedSymbolMap::find_symbol`:
resolves one symbol by exact demangled / substring demangled / exact
mangled. Substring falls back from exact-demangled-first → ambiguous
list when multiple match; mangled is always exact.
Graph
- `NodeKind::Caller { demangled, size, callees_count }` — per-symbol
inverse of `Callee`, mirroring the `walk_forward` layout.
- `walk_backward_per_symbol`: BFS on `called_by`, ranked by caller
flash size descending, same fan-out + max-depth + overflow node
rendering as the forward walker.
- `BackrefGraph::build_with_index` prefers per-symbol back-references
when `called_by` is non-empty (more precise), falls back to the
TU-level walker when it's empty (older reports, vtable/fn-ptr-only
call sites). No double-rendering.
Markdown
- New `Top callers (dual ranking)` sub-table sibling to the existing
callees one. `rank_callers_dual` ranks by caller size and by caller
breadth (callees-count, proxy for "downstream leverage if this caller
were eliminated"), surfacing what an AI bloat reducer most wants.
CLI
- `fbuild bloat lookup <input> --symbol <demangled>` (or
`--symbol-mangled`, or `--json`): emits a focused per-symbol block
with size, archive, object, region, per-symbol callers + callees,
and TU-level referencers — without re-rendering the whole report.
Tests
- `find_symbol_dispatches_correctly` covers exact / substring /
ambiguous-substring / unique-substring / miss / mangled.
- `called_by_roundtrips_via_serde_with_default` proves the new field
serialises and that older reports (without it) still parse.
- `backward_uses_per_symbol_when_called_by_populated` and
`backward_falls_back_to_tu_when_called_by_empty` lock in the
per-symbol-vs-TU branch in the graph walker.
- `rank_callers_dual_sorts_each_axis_independently` proves the
markdown sub-table's two rankings independently surface "biggest"
vs. "broadest".
Closes#478
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds per-symbol backward callgraph edges to the fbuild bloat analyzer by inverting objdump-derived forward calls, introduces a symbol lookup API with multiple query modes, replaces callee dual-ranking with caller dual-ranking, integrates backward graph traversal with a new Caller node type, emits "Top callers" tables in markdown reports, and implements a new fbuild bloat lookup CLI command for targeted symbol resolution.

Changes

Per-Symbol Backward Edges and Symbol Lookup Infrastructure

Layer / File(s)Summary
Symbol schema and backward edge population
crates/fbuild-build/src/symbol_analyzer/mod.rs, crates/fbuild-core/src/symbol_analysis/mod.rs, crates/fbuild-build/src/symbol_analyzer/tests.rs, crates/fbuild-core/src/symbol_analysis/tests.rs
FineGrainedSymbol gains a called_by: Vec<String> field populated by inverting the objdump-derived forward call graph in run_objdump_and_attribute, with support for both mangled and demangled name matching. Test fixtures initialize the field across markdown, graph, and sidecar tests.
Symbol lookup API
crates/fbuild-core/src/symbol_analysis/mod.rs, crates/fbuild-core/src/symbol_analysis/tests.rs
New SymbolQuery and SymbolLookup enums enable ExactDemangled, SubstringDemangled (with exact-first precedence), and ExactMangled lookup modes via FineGrainedSymbolMap::find_symbol, with tests validating dispatch correctness and serde round-trip with backward compatibility.
Backward graph walker and dual caller ranking
crates/fbuild-core/src/symbol_analysis/graph/walker.rs
New walk_backward_per_symbol BFS walker traverses called_by outward with fan-out capping and overflow handling; CallerCandidate helper ranks callers. The exported dual-ranking API is inverted: rank_callees_dual is replaced with rank_callers_dual, which ranks by caller size and caller breadth (number of callees).
Graph node types and builder integration
crates/fbuild-core/src/symbol_analysis/graph/mod.rs, crates/fbuild-core/src/symbol_analysis/graph/tests.rs
NodeKind gains a Caller variant for per-symbol caller nodes. Graph builder conditionally uses walk_backward_per_symbol when called_by is populated, preventing TU-level backward duplication. Tests cover per-symbol vs TU fallback behavior and dual-ranking correctness.
Markdown report callers sub-table
crates/fbuild-build/src/symbol_analyzer/markdown.rs
New emit_dual_callers_subtable renders "Top callers (dual ranking)" using rank_callers_dual, escaping symbol names and appending overflow rows when needed. Direction import enables graph configuration during report generation.
fbuild bloat lookup CLI command
crates/fbuild-cli/src/cli/args.rs, crates/fbuild-cli/src/cli/bloat_lookup.rs, crates/fbuild-cli/src/cli/dispatch.rs, crates/fbuild-cli/src/cli/mod.rs
New BloatCmd::Lookup subcommand accepts an ELF/directory plus --symbol or --symbol-mangled with optional tool/map overrides and --json output. run_bloat_lookup validates inputs, resolves paths, analyzes the ELF, and queries via find_symbol. format_symbol_block renders human-readable reports with core facts, top-N callers/callees (size-sorted), and TU-level referencers with truncation messaging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • FastLED/fbuild#470: Both PRs enhance the symbol-analysis graph/report pipeline with bidirectional traversal and dual-ranking tables; #470 implements forward references_to + rank_callees_dual ("Top callees"), while this PR mirrors that with backward per-symbol called_by + rank_callers_dual ("Top callers").

🐰 A backward hop through callgraph trees,
Each symbol now knows who calls with ease,
Dual rankings dance on size and breadth,
No more translation units mask the path.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and specifically summarizes the main changes: adding per-symbol back-references (called_by field) and a single-symbol lookup CLI feature. It references the issue number and uses clear, concise language that matches the changeset.
Linked Issues check✅ PassedThe PR fully implements all coding requirements from issue #478: FineGrainedSymbol.called_by populated via objdump inversion, NodeKind::Caller with backward graph walker preferring per-symbol edges, dual-ranking Top callers sub-table, and FineGrainedSymbolMap::find_symbol programmatic lookup API with comprehensive test coverage.
Out of Scope Changes check✅ PassedAll changes are directly scoped to issue #478 requirements: symbol analyzer enhancements, graph walker extensions, markdown reporting updates, CLI lookup implementation, and supporting test infrastructure. No extraneous modifications detected.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/issue-478-per-symbol-callers

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.

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

🧹 Nitpick comments (1)
crates/fbuild-build/src/symbol_analyzer/tests.rs (1)

120-121: ⚡ Quick win

Add a dedicated markdown regression test for the callers sub-table path.

All updated fixtures still keep called_by empty, so this suite doesn’t assert the new #### Top callers (dual ranking) rendering/overflow behavior. Adding one focused non-empty called_by case here would guard the new report contract.

🤖 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 `@crates/fbuild-build/src/symbol_analyzer/tests.rs` around lines 120 - 121, Add
a focused regression test that populates the called_by field (instead of leaving
Vec::new()) and asserts the new sub-table rendering; update the test in tests.rs
to create a non-empty called_by entry (use the same struct used in the
surrounding fixtures) and assert the generated markdown contains the "#### Top
callers (dual ranking)" header and expected overflow/entry rows so the callers
sub-table path is exercised (name the test e.g. callers_subtable_regression to
clearly reference it).
🤖 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 `@crates/fbuild-core/src/symbol_analysis/graph/mod.rs`:
- Around line 369-375: The current logic sets use_per_symbol_backward =
want_backward && !root.called_by.is_empty() and then disables want_tu_backward,
causing TU-level backward results to be skipped whenever root.called_by has
entries; change the control flow so that when both per-symbol callers
(root.called_by) and TU-level referencers exist you run both walkers instead of
choosing one: update the conditions around want_tu_backward /
use_per_symbol_backward and the subsequent emission (where CappedReferencer and
the per-symbol walker are produced) to always include the TU-level
CappedReferencer list alongside the per-symbol results when both datasets are
present, and ensure the rendering/emission code distinguishes the two views
visually (e.g., different labels or sections) so both per-symbol and TU-level
backward callers are shown side-by-side.
In `@crates/fbuild-core/src/symbol_analysis/mod.rs`:
- Around line 238-251: find_symbol currently uses .find(...) for ExactDemangled
and ExactMangled (and the other exact branches), which returns only the first
match; change these branches in the find_symbol function to collect all matching
entries from self.symbols (use .iter().filter(...) and collect or count) and: if
zero matches return SymbolLookup::Miss, if exactly one return SymbolLookup::Hit
with that entry, and if more than one return SymbolLookup::Ambiguous (including
the matching entries or enough info per SymbolLookup::Ambiguous variant). Update
both ExactDemangled and ExactMangled branches (and the similar exact-mode
branches around the 253-265 region) to follow this collect->switch-on-count
pattern so duplicate exact matches are detected and reported as Ambiguous.
---
Nitpick comments:
In `@crates/fbuild-build/src/symbol_analyzer/tests.rs`:
- Around line 120-121: Add a focused regression test that populates the
called_by field (instead of leaving Vec::new()) and asserts the new sub-table
rendering; update the test in tests.rs to create a non-empty called_by entry
(use the same struct used in the surrounding fixtures) and assert the generated
markdown contains the "#### Top callers (dual ranking)" header and expected
overflow/entry rows so the callers sub-table path is exercised (name the test
e.g. callers_subtable_regression to clearly reference it).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d9983ea-54e3-462a-a4ab-3ba4f2f929e5

📥 Commits

Reviewing files that changed from the base of the PR and between 95d8ffa and ecdc416.

📒 Files selected for processing (12)
  • crates/fbuild-build/src/symbol_analyzer/markdown.rs
  • crates/fbuild-build/src/symbol_analyzer/mod.rs
  • crates/fbuild-build/src/symbol_analyzer/tests.rs
  • crates/fbuild-cli/src/cli/args.rs
  • crates/fbuild-cli/src/cli/bloat_lookup.rs
  • crates/fbuild-cli/src/cli/dispatch.rs
  • crates/fbuild-cli/src/cli/mod.rs
  • crates/fbuild-core/src/symbol_analysis/graph/mod.rs
  • crates/fbuild-core/src/symbol_analysis/graph/tests.rs
  • crates/fbuild-core/src/symbol_analysis/graph/walker.rs
  • crates/fbuild-core/src/symbol_analysis/mod.rs
  • crates/fbuild-core/src/symbol_analysis/tests.rs

Comment on lines 369 to +375
let want_backward = matches!(
config.direction,
Direction::Backward | Direction::Bidirectional
);
let level1: Vec<CappedReferencer> = if want_backward {
let use_per_symbol_backward = want_backward && !root.called_by.is_empty();
let want_tu_backward = want_backward && !use_per_symbol_backward;
let level1: Vec<CappedReferencer> = if want_tu_backward {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Render both backward views when both datasets exist

Line 374 disables TU-level backward expansion whenever called_by is non-empty, and Line 553 then emits only the per-symbol walker. This drops cref-only callers (indirect/unresolved call paths) instead of showing both views side-by-side, so backward graphs can lose valid call-in evidence. Please run both views when both sources are present and distinguish them visually.

Also applies to: 547-566

🤖 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 `@crates/fbuild-core/src/symbol_analysis/graph/mod.rs` around lines 369 - 375,
The current logic sets use_per_symbol_backward = want_backward &&
!root.called_by.is_empty() and then disables want_tu_backward, causing TU-level
backward results to be skipped whenever root.called_by has entries; change the
control flow so that when both per-symbol callers (root.called_by) and TU-level
referencers exist you run both walkers instead of choosing one: update the
conditions around want_tu_backward / use_per_symbol_backward and the subsequent
emission (where CappedReferencer and the per-symbol walker are produced) to
always include the TU-level CappedReferencer list alongside the per-symbol
results when both datasets are present, and ensure the rendering/emission code
distinguishes the two views visually (e.g., different labels or sections) so
both per-symbol and TU-level backward callers are shown side-by-side.

Comment on lines +238 to +251
pub fn find_symbol(&self, query: &SymbolQuery<'_>) -> SymbolLookup<'_> {
match query {
SymbolQuery::ExactDemangled(name) => self
.symbols
.iter()
.find(|s| s.demangled == *name)
.map(SymbolLookup::Hit)
.unwrap_or(SymbolLookup::Miss),
SymbolQuery::ExactMangled(name) => self
.symbols
.iter()
.find(|s| s.mangled == *name)
.map(SymbolLookup::Hit)
.unwrap_or(SymbolLookup::Miss),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle duplicate exact matches in find_symbol.

Line 240 and Line 246 use .find(...), so exact queries return only the first matching row. This can hide valid duplicates (e.g., multiple map-derived rows sharing one owner symbol), yielding partial/unstable lookup results for bloat lookup --symbol. Exact modes should collect all exact matches and return Ambiguous when count > 1 (or apply a documented deterministic merge rule).

Suggested fix
 pub fn find_symbol(&self, query: &SymbolQuery<'_>) -> SymbolLookup<'_> {
match query {
- SymbolQuery::ExactDemangled(name) => self- .symbols- .iter()- .find(|s| s.demangled == *name)- .map(SymbolLookup::Hit)- .unwrap_or(SymbolLookup::Miss),- SymbolQuery::ExactMangled(name) => self- .symbols- .iter()- .find(|s| s.mangled == *name)- .map(SymbolLookup::Hit)- .unwrap_or(SymbolLookup::Miss),+ SymbolQuery::ExactDemangled(name) => {+ let mut hits: Vec<&FineGrainedSymbol> =+ self.symbols.iter().filter(|s| s.demangled == *name).collect();+ match hits.len() {+ 0 => SymbolLookup::Miss,+ 1 => SymbolLookup::Hit(hits.remove(0)),+ _ => SymbolLookup::Ambiguous(hits),+ }+ }+ SymbolQuery::ExactMangled(name) => {+ let mut hits: Vec<&FineGrainedSymbol> =+ self.symbols.iter().filter(|s| s.mangled == *name).collect();+ match hits.len() {+ 0 => SymbolLookup::Miss,+ 1 => SymbolLookup::Hit(hits.remove(0)),+ _ => SymbolLookup::Ambiguous(hits),+ }+ }
SymbolQuery::SubstringDemangled(needle) => {

Also applies to: 253-265

🤖 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 `@crates/fbuild-core/src/symbol_analysis/mod.rs` around lines 238 - 251,
find_symbol currently uses .find(...) for ExactDemangled and ExactMangled (and
the other exact branches), which returns only the first match; change these
branches in the find_symbol function to collect all matching entries from
self.symbols (use .iter().filter(...) and collect or count) and: if zero matches
return SymbolLookup::Miss, if exactly one return SymbolLookup::Hit with that
entry, and if more than one return SymbolLookup::Ambiguous (including the
matching entries or enough info per SymbolLookup::Ambiguous variant). Update
both ExactDemangled and ExactMangled branches (and the similar exact-mode
branches around the 253-265 region) to follow this collect->switch-on-count
pattern so duplicate exact matches are detected and reported as Ambiguous.

@zackees
zackees merged commit 1a0e4a8 into mainJun 7, 2026
87 checks passed
@zackees
zackees deleted the feat/issue-478-per-symbol-callers branch June 7, 2026 19:09
zackees added a commit that referenced this pull request Jun 7, 2026
Picks up:
- #480 — per-symbol back-references (`called_by`) + `fbuild bloat lookup`
- #486 — drop intra-function jump labels (`<funcname+0xNN>`) from the
call graph so `references_to` / `called_by` stay honest
- #482 — truncate subprocess output fed back to Claude in hooks
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.

Per-symbol backward edges in bloat graph (replace TU-level cref-only view)

1 participant

@zackees