Repository navigation
Description
Context
#459 / #460 shipped Part 1 — referenced_by: Vec<{archive, object}> on every row of report.json, plus a "Referenced by" column in report.md. That landed the data prerequisite. This issue is Part 2: turn that data into visualizable back-reference graphs.
Per #460's body: "the JSON field shipped here is the data prerequisite, so a graph subcommand becomes a pure rendering PR." This is that PR.
Scope
A. fbuild bloat graph subcommand
fbuild bloat graph <input> --symbol <demangled-or-mangled>
[--depth adaptive|N]
[--fan-out K]
[--collapse-archive A,B,...]
[--exclude-archive A,B,...]
[-o <path>]
Walks the back-reference graph outward from a target symbol using the referenced_by data we already serialise. Default rendering strategy:
- Adaptive termination. Stop expanding a branch the first time it reaches a referencer whose archive is different from the original target's archive. Naturally surfaces the boundary where the symbol "escapes" its own library. Override with
--depth Nfor fixed-depth. - Per-node fan-out cap K (default 5). Sort referencers by their own contributed flash bytes; keep top-K; collapse the rest into a
[… and N more]super-node. - Hard depth cap N=4 as a safety belt — if adaptive termination hasn't fired by depth 4, you're chasing infra.
Render aids:
- Color nodes by archive group: FastLED / ESP-IDF / Arduino / libc / mbedTLS / libstdc++ / app — distinct fill colors so the eye jumps to non-system edges.
- Size nodes by attributed flash bytes (
width=proportional tolog(bytes)). --collapse-archive libc.amerges all libc-internal hops into a single super-node so the ESP-IDF/mbedTLS callers stand out.--exclude-archive libc.a,libm.adrops branches that don't escape system libraries (useful for "show me only the FastLED-side pull").
B. Default-on embedded graphs in report.md
The top-10 entries in report.md get an inline fenced ```dot block each, generated using the same adaptive strategy as the subcommand. AI-friendly: a single self-contained report.md answers "what pulls in X?" without a second fetch.
### #2 `_vfprintf_r` — 11,309 B
- Defined in: `libc.a(libc_a-vfprintf.o)`
- Output section: `.flash.text`
<details>
<summary>Back-reference graph (Graphviz)</summary>
```dot
digraph "_vfprintf_r" { … }C. Sidecar .dot files
Every symbol with > 256 B attributed flash also gets a sidecar file under <output-dir>/graphs/<rank>_<sanitized-symbol>.dot. Lets humans run dot -Tsvg graphs/2_vfprintf_r.dot without extracting from markdown.
D. CLI flags (defaults aim for "do the right thing without tuning")
| Flag | Default | Effect |
|---|---|---|
--graph-top N |
10 (or --top if smaller) |
How many top symbols get embedded graphs. |
--no-graph |
off | Skips both embedded blocks and sidecar files. Escape hatch. |
--graph-min-bytes B |
256 | Sidecar threshold. |
--graph-depth adaptive|N |
adaptive | Override the cross-archive termination. |
--graph-fan-out K |
5 | Per-node fan-out cap. |
--graph-collapse-archive A,B |
libc.a,libgcc.a |
Comma list of archives to collapse into super-nodes. |
--graph-exclude-archive A,B |
(empty) | Comma list of archives whose branches are dropped entirely. |
Acceptance
-
fbuild bloat graph <input> --symbol <S>writes a readable.dotfor both:_vfprintf_r(libc target, ~6-10 nodes after collapsing libc internals)- A FastLED ctor (app target, ~3-5 nodes) — no manual flag tuning needed
-
fbuild bloat <input> --output-dir <dir>produces:report.json(unchanged from Part 1)report.mdwith inline```dotblocks for the top-10 symbolsgraphs/*.dotsidecar files for every symbol > 256 B
-
--no-graphdisables both embedding and sidecars; report still ships referenced_by data. - AI-friendliness sanity check: a fresh agent reading only
report.mdcan answer "what pulls in_vfprintf_r?" without fetchingreport.jsonor the linker map. - Renders gracefully when a symbol has zero referencers (single isolated node, not a parse error).
- Works on a map with no cref block (no embedded graphs emitted, sidecars empty — never a crash).
Implementation notes
- The existing
fbuild_core::symbol_analysis::crefmodule already exposesreferenced_byper row. A newfbuild_core::symbol_analysis::graphmodule should consume that and emit.dotstrings. - The walker is BFS with three termination predicates (depth, cross-archive, fan-out). Keep them composable so users can switch one off without breaking the others.
- Markdown embedding lives in
fbuild_build::symbol_analyzernext to the existing top-N renderer. - Sidecar file naming:
<rank>_<sanitized-symbol>.dotwheresanitized = symbol.replace(['/', '\\', ':', '<', '>', '\"', '|', '?', '*', ' '], '_').truncate(80). Windows-safe.
Related
- feat(bloat): expose linker
--crefback-references asreferenced_by+ optional graphviz.dotexport #459 — original umbrella issue (closed) - feat(bloat): expose linker --cref back-references as referenced_by (#459) #460 — Part 1 (merged) — the data this issue consumes
- Meta:
fbuild bloat— first-class, end-to-end bloat analysis with build_info-driven toolchain resolution (no manual nm/c++filt, no PlatformIO, no side scripts) #434 —symbols→bloatrename - FastLED#2886 — motivating bloat audit
🤖 Generated with Claude Code
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
chore: bump version to 2.2.20
chore(deps): bump fbuild 2.2.18 → 2.2.20 (#2946) (#2947)