Skip to content

feat(bloat): graphviz .dot back-reference graphs (Part 2 of #459) #463

Description

@zackees

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:

  1. 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 N for fixed-depth.
  2. 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.
  3. 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 to log(bytes)).
  • --collapse-archive libc.a merges all libc-internal hops into a single super-node so the ESP-IDF/mbedTLS callers stand out.
  • --exclude-archive libc.a,libm.a drops 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 .dot for 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.md with inline ```dot blocks for the top-10 symbols
    • graphs/*.dot sidecar files for every symbol > 256 B
  • --no-graph disables both embedding and sidecars; report still ships referenced_by data.
  • AI-friendliness sanity check: a fresh agent reading only report.md can answer "what pulls in _vfprintf_r?" without fetching report.json or 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::cref module already exposes referenced_by per row. A new fbuild_core::symbol_analysis::graph module should consume that and emit .dot strings.
  • 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_analyzer next to the existing top-N renderer.
  • Sidecar file naming: <rank>_<sanitized-symbol>.dot where sanitized = symbol.replace(['/', '\\', ':', '<', '>', '\"', '|', '?', '*', ' '], '_').truncate(80). Windows-safe.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions