Turbopack: aggregate server HMR into one subscription#94948
Conversation
Tests PassedCommit: f5be7ce |
Stats cancelledCommit: f5be7ce |
831cbfb to
98ac44a
Compare
This uses the infrastructure for a single firehose feed of HMR events introduced in #94948. It replaces the chunk list subscriber and per-chunk-list fan-out of \`project.hmrEvents\` subscriptions with a single \`projectClientHmrEvents\` sub scription that diffs every client chunk list under the client root in one tick. For now, unlike the server side, updates are *not* merged into one instruction. The browser identifies each update by chunk list path (\`resource.path\`), so th e napi layer emits one \`ClientUpdateInstruction\` per changed chunk list and th e existing per-resource HMR dispatcher routes each one to its callback. Rust: - \`aggregate_hmr.rs\`: new \`ClientChunkListUpdate\` / \`ClientChunkListUpdateK ind\` / \`ClientHmrUpdates\` to model per-chunk-list updates while preserving ch unk list identity. - \`versioned_content_map.rs\`: \`hmr_chunks_in_path\` filters to entries that d owncast to \`EcmascriptDevChunkListContent\` for \`HmrTarget::Client\`, the only shape the browser HMR runtime can apply. - \`project.rs\`: - \`hmr_version_state\` handles \`HmrTarget::Client\` by snapshotting chunk li sts. - \`client_hmr_update\` returns one entry per chunk list with a non-empty diff . \`Total\`/\`Missing\` becomes a per-resource \`Restart\`; the rest of the batc h is still delivered. The first tick returns an empty result so the seed transi tion can advance \`VersionState\` without the JS consumer applying anything. - \`ClientHmrUpdates::to\` exposes the aggregate \`to\` version on the same st ruct as \`updates\` so the napi subscriber can advance state even when no chunk lists changed. NAPI: - \`projectClientHmrEvents\` returns a single subscription emitting \`{ updates: ClientUpdateInstruction[] }\`. Issues are mirrored on every entry; when there a re issues but no updates, a sentinel \`issues\`-only frame is emitted under \`__ next_client_hmr__\`. JS: - \`hot-reloader-turbopack.ts\` subscribes once via \`clientHmrEvents()\` instea d of fanning out per \`turbopack-subscribe\`. The browser still emits \`turbopa ck-subscribe\`/\`-unsubscribe\` frames on chunk list registration; they're now accepted as no-ops for protocol compat. - \`turbopack-utils.ts\` drops the per-client \`subscriptions\` map and \`unsubs cribeFromHmrEvents\` hook.
46e9051 to
9c55cbe
Compare
This uses the infrastructure for a single firehose feed of HMR events introduced in #94948. It replaces the chunk list subscriber and per-chunk-list fan-out of \`project.hmrEvents\` subscriptions with a single \`projectClientHmrEvents\` sub scription that diffs every client chunk list under the client root in one tick. For now, unlike the server side, updates are *not* merged into one instruction. The browser identifies each update by chunk list path (\`resource.path\`), so th e napi layer emits one \`ClientUpdateInstruction\` per changed chunk list and th e existing per-resource HMR dispatcher routes each one to its callback. Rust: - \`aggregate_hmr.rs\`: new \`ClientChunkListUpdate\` / \`ClientChunkListUpdateK ind\` / \`ClientHmrUpdates\` to model per-chunk-list updates while preserving ch unk list identity. - \`versioned_content_map.rs\`: \`hmr_chunks_in_path\` filters to entries that d owncast to \`EcmascriptDevChunkListContent\` for \`HmrTarget::Client\`, the only shape the browser HMR runtime can apply. - \`project.rs\`: - \`hmr_version_state\` handles \`HmrTarget::Client\` by snapshotting chunk li sts. - \`client_hmr_update\` returns one entry per chunk list with a non-empty diff . \`Total\`/\`Missing\` becomes a per-resource \`Restart\`; the rest of the batc h is still delivered. The first tick returns an empty result so the seed transi tion can advance \`VersionState\` without the JS consumer applying anything. - \`ClientHmrUpdates::to\` exposes the aggregate \`to\` version on the same st ruct as \`updates\` so the napi subscriber can advance state even when no chunk lists changed. NAPI: - \`projectClientHmrEvents\` returns a single subscription emitting \`{ updates: ClientUpdateInstruction[] }\`. Issues are mirrored on every entry; when there a re issues but no updates, a sentinel \`issues\`-only frame is emitted under \`__ next_client_hmr__\`. JS: - \`hot-reloader-turbopack.ts\` subscribes once via \`clientHmrEvents()\` instea d of fanning out per \`turbopack-subscribe\`. The browser still emits \`turbopa ck-subscribe\`/\`-unsubscribe\` frames on chunk list registration; they're now accepted as no-ops for protocol compat. - \`turbopack-utils.ts\` drops the per-client \`subscriptions\` map and \`unsubs cribeFromHmrEvents\` hook.
2c63e80 to
98f5dd9
Compare
| bail!("all_hmr_version_state is not yet implemented for the client target"); | ||
| } | ||
|
|
||
| // The session argument keeps this from caching across sessions. |
There was a problem hiding this comment.
this is a trick reused from
next.js/crates/next-api/src/project.rs
Lines 2449 to 2451 in 98f5dd9
@bgw @lukesandberg is there a more modern way of doing this with turbotasks?
There was a problem hiding this comment.
you can also tag it as session_dependent which will force a re-execution on next session
if you don't want it to be cached then you could just make VersionState as serialization=skip
sokra
left a comment
There was a problem hiding this comment.
I really don't like that we stay subscribed to all visited routes over the lifetime of the dev server. This leads to excessive compilation and memory usage over time.
Instead we should maintain some state in a turbo-tasks value which is the list of subscribed chunk groups. We can update that state via napi methods, when clients are subscribing/unsubscribing.
As discussed, this will be a priority going forward with the client implementation, but this is a reasonable limitation to start with for the server implementation. |
Replace per-chunk Server HMR fan-out with a single firehose subscription that diffs every HMR-eligible chunk under the target root in one tick. This significantly cuts the number of tokio task churn on projects with many server chunks and centralizes the diff/clear logic. It leads to a multi-second saving in both cold and warm builds in a large app. A following PR will bring this to client chunks. Rust: - New `aggregate_hmr` module: `AggregateHmrVersion` keyed by chunk path, `merged_partial_update` builder, and `is_hmr_eligible_chunk` (excludes `.map` files, which would force every diff to `Total`). - `Project::all_hmr_version_state` / `all_hmr_update` aggregate over the whole `hmr_root_path`. The seed transition emits an empty `Partial` so the JS consumer doesn't treat it as a restart and wipe handlers the triggering request just populated. Any chunk requiring `Total`/`Missing` escalates the batch. - `VersionedContentMap::hmr_chunks_in_path` lists eligible chunks with their `VersionedContent`. NAPI: - `projectAllHmrEvents(target)` returns a single subscription. JS: - `setupServerHmr` subscribes once via `allHmrEvents` instead of fanning out over `hmrChunkNamesSubscribe`. - `clear()` evicts every chunk under `server/chunks/` from `require.cache` directly rather than tracking subscriptions. This is a bit fragile as it relies on the path prefix and scanning require.cache.
This uses the infrastructure for a single firehose feed of HMR events introduced in #94948. It replaces the chunk list subscriber and per-chunk-list fan-out of \`project.hmrEvents\` subscriptions with a single \`projectClientHmrEvents\` sub scription that diffs every client chunk list under the client root in one tick. For now, unlike the server side, updates are *not* merged into one instruction. The browser identifies each update by chunk list path (\`resource.path\`), so th e napi layer emits one \`ClientUpdateInstruction\` per changed chunk list and th e existing per-resource HMR dispatcher routes each one to its callback. Rust: - \`aggregate_hmr.rs\`: new \`ClientChunkListUpdate\` / \`ClientChunkListUpdateK ind\` / \`ClientHmrUpdates\` to model per-chunk-list updates while preserving ch unk list identity. - \`versioned_content_map.rs\`: \`hmr_chunks_in_path\` filters to entries that d owncast to \`EcmascriptDevChunkListContent\` for \`HmrTarget::Client\`, the only shape the browser HMR runtime can apply. - \`project.rs\`: - \`hmr_version_state\` handles \`HmrTarget::Client\` by snapshotting chunk li sts. - \`client_hmr_update\` returns one entry per chunk list with a non-empty diff . \`Total\`/\`Missing\` becomes a per-resource \`Restart\`; the rest of the batc h is still delivered. The first tick returns an empty result so the seed transi tion can advance \`VersionState\` without the JS consumer applying anything. - \`ClientHmrUpdates::to\` exposes the aggregate \`to\` version on the same st ruct as \`updates\` so the napi subscriber can advance state even when no chunk lists changed. NAPI: - \`projectClientHmrEvents\` returns a single subscription emitting \`{ updates: ClientUpdateInstruction[] }\`. Issues are mirrored on every entry; when there a re issues but no updates, a sentinel \`issues\`-only frame is emitted under \`__ next_client_hmr__\`. JS: - \`hot-reloader-turbopack.ts\` subscribes once via \`clientHmrEvents()\` instea d of fanning out per \`turbopack-subscribe\`. The browser still emits \`turbopa ck-subscribe\`/\`-unsubscribe\` frames on chunk list registration; they're now accepted as no-ops for protocol compat. - \`turbopack-utils.ts\` drops the per-client \`subscriptions\` map and \`unsubs cribeFromHmrEvents\` hook.
98f5dd9 to
f5be7ce
Compare
This replaces per-chunk Server HMR turbo tasks with a single firehose subscription that diffs every HMR chunk. This significantly cuts the number of tokio task churn on projects with many server chunks and centralizes the diff/clear logic. It leads to a multi-second saving in both cold and warm builds in a large app.
A following PR will bring this to client chunks.
Rust:
aggregate_hmrmodule:AggregateHmrVersionkeyed by chunk path,merged_partial_updatebuilder, andis_hmr_eligible_chunk(excludes.mapfiles, which would force every diff toTotal).Project::all_hmr_version_state/all_hmr_updateaggregate over the wholehmr_root_path. The seed transition emits an emptyPartialso the JS consumer doesn't treat it as a restart and wipe handlers the triggering request just populated. Any chunk requiringTotal/Missingescalates the batch.VersionedContentMap::hmr_chunks_in_pathlists eligible chunks with theirVersionedContent.NAPI:
projectAllHmrEvents(target)returns a single subscription.JS:
setupServerHmrsubscribes once viaallHmrEventsinstead of fanning out overhmrChunkNamesSubscribe.clear()evicts every chunk underserver/chunks/fromrequire.cachedirectly rather than tracking subscriptions. This is a bit fragile as it relies on the path prefix and scanning require.cache.