next.js
054aa46c - [turbopack] Persistent last_successful_parse (#92852)

Commit
20 days ago
[turbopack] Persistent last_successful_parse (#92852) ## What? Replaces the `TransientState<ReadRef<ParseResult>>` used by `failsafe_parse` with a persistent `LastSuccessfulSource` turbo-tasks cell that stores the raw bytes of the last successfully parsed version of the module. ## Why? The previous implementation had three problems: 1. **Memory**: It held a full AST in memory alongside the current one — effectively doubling AST memory for every file with `keep_last_successful_parse` enabled. 2. **Durability**: `TransientState` skips serialization, so the fallback was silently lost on eviction or server restart, meaning the mechanism that prevents whole-graph invalidation on syntax errors didn't survive across restarts. 3. **`final_read_hint` defeat**: `with_consumed_parse_result` uses `final_read_hint()` + `ReadRef::try_unwrap()` to avoid cloning the AST when this is the last reader. But holding a `ReadRef<ParseResult>` in `TransientState` meant there was always a second `Arc` reference alive, so `try_unwrap` always failed and the AST was always cloned — even in the common case where no syntax error had occurred. Now because we are just cloning the Rope, we don't duplicate any memory until things change and even then only if there is an error. ## How? `LastSuccessfulSource` is a `#[turbo_tasks::value(eq = "manual")]` wrapping `parking_lot::Mutex<Option<Rope>>` and a `SerializationInvalidator` with: - `PartialEq` always `true` and `Hash` as a no-op — re-celling a freshly constructed `LastSuccessfulSource` inside `failsafe_parse` hits the compare-and-update path and preserves the existing cell (and its interior `Mutex<Option<Rope>>`) across task re-runs. The value is not a field on `EcmascriptModuleAsset`; it lives in the `failsafe_parse` task's own cell storage. - Serialized via `#[bincode(with = "parking_lot_mutex_bincode")]` on the Mutex field — persisted to the cache and survives both eviction and restarts. - Calls `serialization_invalidator.invalidate()` on every mutation (`set`/`clear`) so the backend knows to write the updated state back to the persistence layer. `Rope` is just a ref-counted byte buffer, so cloning it (on cache hit) is a single `Arc::clone`. ### Consistency with the parsed AST `ParseResult::Ok` now carries a `program_source: Rope` field: the raw bytes that produced the AST, captured atomically inside `parse_file_content`. `failsafe_parse` reads the rope out of `ParseResult::Ok` rather than re-reading `source.content()` separately — otherwise, under eventual consistency, the file could change between the parse task and the rope read, letting `failsafe_parse` cache bytes that didn't match the AST it just stored. On a successful parse, `program_source` is stored into the cell. On failure, `parse_from_rope` re-derives the AST on demand, reusing the existing `parse_file_content` path. If the fallback itself fails for any reason, the cached bytes are cleared and the real (broken) result is returned. ### Simplification of `SerializationInvalidator` `SerializationInvalidator` previously captured a `Weak<dyn TurboTasksApi>` and a tokio `Handle` at construction time. Since `invalidate()` is always called from within a turbo-tasks execution context (where the `TURBO_TASKS` task-local is available), these captures are unnecessary. The struct is now just a `TaskId` wrapper with derives for `Clone`, `Hash`, `Eq`, `PartialEq`, `Encode`, `Decode`, `TraceRawVcs`, and `NonLocalValue`. <!-- NEXT_JS_LLM_PR -->
Author
Parents
Loading