Skip to content

The clang -M include-graph pass is per-header and cached only in-process, never across runs

Index: Known gaps

Status (re-verified 2026-10-09 at 456989f): PARTIAL. abicheck/buildsource/header_include_memo.py now holds a process-wide memo, keyed in header_graph.py:635-650, so release members and sides no longer re-run the same clang -M set. Still open: the memo lives only in-process (never persisted across runs), and nothing batches per-header invocations.

Measured on the header-count axis (one library, N top-level public headers over one shared detail/ dependency closure, live/live comparison):

top-level headers observed include_pass invocations wall
1 2 1.10 s
8 16 2.48 s
32 64 10.40 s

Two separate things show up here. The invocation count is exactly headers × 2 sides — one clang -M subprocess per top-level header per side, with no batching across headers that share a dependency closure. And the wall time grows superlinearly in header count (8x the headers costs ~9.5x the time going 1→32), so this is not merely a constant per-header cost.

The separate cache finding: on a warm AST cache the header extraction count drops to zero (the AST cache serves it) while the include_pass count stays unchanged. The include graph is recomputed on every run regardless of cache state. Combined with the startup cost above, that is why a fully warm small dump measures ~0.93 s against a cold ~0.88 s — within noise, i.e. the cache hit buys nothing observable at that scale even though it demonstrably happened (proved by the counters, not the clock).