Skip to content

Persistent frontend cache: replay the expanded AST when frontend inputs are unchanged - #108

Open
xmakro wants to merge 7 commits into
perf/frontend-cache-basefrom
perf/frontend-expansion-cache
Open

Persistent frontend cache: replay the expanded AST when frontend inputs are unchanged#108
xmakro wants to merge 7 commits into
perf/frontend-cache-basefrom
perf/frontend-expansion-cache

Conversation

@xmakro

@xmakro xmakro commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Prototype of a persistent frontend cache: -Zfrontend-cache (default on under -Cincremental) snapshots the expanded AST after macro expansion and replays it in later sessions whose frontend inputs are unchanged, skipping macro matching, transcription, fragment parsing and proc macro execution wholesale. Incremental compilation currently re-runs all of this every session only to rediscover, fingerprint by fingerprint, that nothing changed.

The snapshot stores the post-expansion crate together with the session state that expansion created: the hygiene table delta, the def creation log, the source map additions (include!-ed, generated and metadata-imported files), the crate load order, metavariable spans, expansion-recorded glob map entries, stripped cfg items, and each module's binding insertion order. Everything is encoded with raw session-local ids. That is sound because the replay reconstructs the id spaces exactly: parsing reproduces the source map prefix, dependency crates are re-loaded and verified by name and SVH, hygiene rows are appended onto a length-checked table prefix, and the def log is replayed through the regular create_def machinery, so DefPathHashes, disambiguators, spans and syntax contexts come out bit for bit identical. The expansion driver then runs over the restored crate in replay mode: it finds no invocations, keeps the restored NodeIds, treats all remaining attributes as inert, and rebuilds the resolver's module graph in one pass. Snapshotted binding orders are applied to the rebuilt resolution tables afterwards so that order-observing consumers (trait-in-scope lists hashed into hir_owner, module children, glob maps) match the recording session. Validation covers the compiler version, the option dep-tracking hash, the crate cfg set, every source file hash, env! reads, raw content hashes of non-source file deps, and dependency SVHs; any mismatch falls back to normal expansion. When inputs are valid but the crate numbering cannot be reproduced (several loaded crates sharing a name), the session expands normally without rewriting the snapshot, costing only the validation.

Perf (stage2, lto=thin + jemalloc, collector bench_local, Check profile, primary suite, instructions:u): incr-unchanged mean -5.7%, with unicode-normalization -26.1%, libc -24.7%, syn -17.3%, diesel -16.1%, typenum -13.6%, serde -12.4%, nalgebra -9.9%. Full builds are unchanged (mean -0.06%). The cost is the snapshot write on recording sessions: incr-full +0.6% mean, incr-patched +2.4% median. Steady-state warm rebuilds measured outside the collector reach -20% to -55% (html5ever -55%, where proc macro execution is also skipped). tests/incremental passes 178/178 with the cache active; ui imports/resolve/macros subsets pass 1207/1207.

Known limitations, deliberate for the prototype: buffered early lints from expansion are not snapshotted, so some expansion-emitted warnings (unused_macros among them) disappear on cache hits; proc macros reading untracked state are assumed deterministic; proc-macro crates are not cached; crates with duplicate dependency names in the crate graph never hit (about half the primary suite, including hyper and html5ever under the collector's dependency sets) - provenance-aware crate preloading would unlock those; the first warm session after a recording still re-executes a small slice (resolutions, crate_hash) because expansion-time glob-use and trait-import-use recordings are not replayed; and the snapshot write on every miss should eventually be made asynchronous or skipped when inputs did not change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant