-
Notifications
You must be signed in to change notification settings - Fork 30.4k
Optimize how we track data for persistence #89370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
CodSpeed Performance ReportMerging this PR will improve performance by 3.76%Comparing Summary
Performance Changes
Footnotes
|
Stats from current PR🟢 1 improvement
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles: **434 kB** → **435 kB**
|
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 763 B | 764 B | ✓ |
| Total | 763 B | 764 B |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 449 B | 451 B | ✓ |
| Total | 449 B | 451 B |
📦 Webpack
Client
Main Bundles
| Canary | PR | Change | |
|---|---|---|---|
| 5528-HASH.js gzip | 5.47 kB | N/A | - |
| 6280-HASH.js gzip | 54.5 kB | N/A | - |
| 6335.HASH.js gzip | 169 B | N/A | - |
| 912-HASH.js gzip | 4.53 kB | N/A | - |
| e8aec2e4-HASH.js gzip | 62.5 kB | N/A | - |
| framework-HASH.js gzip | 59.7 kB | 59.7 kB | ✓ |
| main-app-HASH.js gzip | 255 B | 254 B | ✓ |
| main-HASH.js gzip | 39 kB | 39.1 kB | ✓ |
| webpack-HASH.js gzip | 1.68 kB | 1.68 kB | ✓ |
| 262-HASH.js gzip | N/A | 4.52 kB | - |
| 2889.HASH.js gzip | N/A | 169 B | - |
| 5602-HASH.js gzip | N/A | 5.48 kB | - |
| 6948ada0-HASH.js gzip | N/A | 62.5 kB | - |
| 9544-HASH.js gzip | N/A | 55.2 kB | - |
| Total | 228 kB | 229 kB |
Polyfills
| Canary | PR | Change | |
|---|---|---|---|
| polyfills-HASH.js gzip | 39.4 kB | 39.4 kB | ✓ |
| Total | 39.4 kB | 39.4 kB | ✓ |
Pages
| Canary | PR | Change | |
|---|---|---|---|
| _app-HASH.js gzip | 194 B | 194 B | ✓ |
| _error-HASH.js gzip | 183 B | 180 B | 🟢 3 B (-2%) |
| css-HASH.js gzip | 331 B | 330 B | ✓ |
| dynamic-HASH.js gzip | 1.81 kB | 1.81 kB | ✓ |
| edge-ssr-HASH.js gzip | 256 B | 256 B | ✓ |
| head-HASH.js gzip | 351 B | 352 B | ✓ |
| hooks-HASH.js gzip | 384 B | 383 B | ✓ |
| image-HASH.js gzip | 580 B | 581 B | ✓ |
| index-HASH.js gzip | 260 B | 260 B | ✓ |
| link-HASH.js gzip | 2.49 kB | 2.49 kB | ✓ |
| routerDirect..HASH.js gzip | 320 B | 319 B | ✓ |
| script-HASH.js gzip | 386 B | 386 B | ✓ |
| withRouter-HASH.js gzip | 315 B | 315 B | ✓ |
| 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
| Total | 7.97 kB | 7.97 kB | ✅ -1 B |
Server
Edge SSR
| Canary | PR | Change | |
|---|---|---|---|
| edge-ssr.js gzip | 126 kB | 126 kB | ✓ |
| page.js gzip | 249 kB | 249 kB | ✓ |
| Total | 375 kB | 375 kB |
Middleware
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 616 B | 614 B | ✓ |
| middleware-r..fest.js gzip | 156 B | 155 B | ✓ |
| middleware.js gzip | 33.1 kB | 33.1 kB | ✓ |
| edge-runtime..pack.js gzip | 842 B | 842 B | ✓ |
| Total | 34.7 kB | 34.8 kB |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 732 B | 736 B | ✓ |
| Total | 732 B | 736 B |
Build Cache
| Canary | PR | Change | |
|---|---|---|---|
| 0.pack gzip | 3.79 MB | 3.81 MB | 🔴 +15.3 kB (+0%) |
| index.pack gzip | 104 kB | 102 kB | 🟢 2.03 kB (-2%) |
| index.pack.old gzip | 103 kB | 102 kB | ✓ |
| Total | 4 MB | 4.01 MB |
🔄 Shared (bundler-independent)
Runtimes
| Canary | PR | Change | |
|---|---|---|---|
| app-page-exp...dev.js gzip | 311 kB | 311 kB | ✓ |
| app-page-exp..prod.js gzip | 166 kB | 166 kB | ✓ |
| app-page-tur...dev.js gzip | 311 kB | 311 kB | ✓ |
| app-page-tur..prod.js gzip | 166 kB | 166 kB | ✓ |
| app-page-tur...dev.js gzip | 308 kB | 308 kB | ✓ |
| app-page-tur..prod.js gzip | 164 kB | 164 kB | ✓ |
| app-page.run...dev.js gzip | 308 kB | 308 kB | ✓ |
| app-page.run..prod.js gzip | 164 kB | 164 kB | ✓ |
| app-route-ex...dev.js gzip | 70.4 kB | 70.5 kB | ✓ |
| app-route-ex..prod.js gzip | 48.9 kB | 49 kB | ✓ |
| app-route-tu...dev.js gzip | 70.4 kB | 70.5 kB | ✓ |
| app-route-tu..prod.js gzip | 49 kB | 49 kB | ✓ |
| app-route-tu...dev.js gzip | 70 kB | 70.1 kB | ✓ |
| app-route-tu..prod.js gzip | 48.7 kB | 48.8 kB | ✓ |
| app-route.ru...dev.js gzip | 70 kB | 70.1 kB | ✓ |
| app-route.ru..prod.js gzip | 48.7 kB | 48.7 kB | ✓ |
| dist_client_...dev.js gzip | 324 B | 324 B | ✓ |
| dist_client_...dev.js gzip | 326 B | 326 B | ✓ |
| dist_client_...dev.js gzip | 318 B | 318 B | ✓ |
| dist_client_...dev.js gzip | 317 B | 317 B | ✓ |
| pages-api-tu...dev.js gzip | 43.1 kB | 43.2 kB | ✓ |
| pages-api-tu..prod.js gzip | 32.9 kB | 32.9 kB | ✓ |
| pages-api.ru...dev.js gzip | 43.1 kB | 43.2 kB | ✓ |
| pages-api.ru..prod.js gzip | 32.8 kB | 32.9 kB | ✓ |
| pages-turbo....dev.js gzip | 52.4 kB | 52.5 kB | ✓ |
| pages-turbo...prod.js gzip | 39.4 kB | 39.4 kB | ✓ |
| pages.runtim...dev.js gzip | 52.4 kB | 52.5 kB | ✓ |
| pages.runtim..prod.js gzip | 39.3 kB | 39.4 kB | ✓ |
| server.runti..prod.js gzip | 62.6 kB | 62.6 kB | ✓ |
| Total | 2.77 MB | 2.78 MB |
📝 Changed Files (25 files)
Files with changes:
app-page-exp..ntime.dev.jsapp-page-exp..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page.runtime.dev.jsapp-page.runtime.prod.jsapp-route-ex..ntime.dev.jsapp-route-ex..time.prod.jsapp-route-tu..ntime.dev.jsapp-route-tu..time.prod.jsapp-route-tu..ntime.dev.jsapp-route-tu..time.prod.jsapp-route.runtime.dev.jsapp-route.ru..time.prod.jspages-api-tu..ntime.dev.jspages-api-tu..time.prod.jspages-api.runtime.dev.jspages-api.ru..time.prod.js- ... and 5 more
View diffs
app-page-exp..ntime.dev.js
failed to diffapp-page-exp..time.prod.js
Diff too large to display
app-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
Diff too large to display
app-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
Diff too large to display
app-page.runtime.dev.js
failed to diffapp-page.runtime.prod.js
Diff too large to display
app-route-ex..ntime.dev.js
Diff too large to display
app-route-ex..time.prod.js
Diff too large to display
app-route-tu..ntime.dev.js
Diff too large to display
app-route-tu..time.prod.js
Diff too large to display
app-route-tu..ntime.dev.js
Diff too large to display
app-route-tu..time.prod.js
Diff too large to display
app-route.runtime.dev.js
Diff too large to display
app-route.ru..time.prod.js
Diff too large to display
pages-api-tu..ntime.dev.js
Diff too large to display
pages-api-tu..time.prod.js
Diff too large to display
pages-api.runtime.dev.js
Diff too large to display
pages-api.ru..time.prod.js
Diff too large to display
pages-turbo...ntime.dev.js
Diff too large to display
pages-turbo...time.prod.js
Diff too large to display
pages.runtime.dev.js
Diff too large to display
pages.runtime.prod.js
Diff too large to display
server.runtime.prod.js
Diff too large to display
Failing test suitesCommit: 4dada51 | About building and testing Next.js
Expand output● app dir - with output export - dynamic api route › should work in prod with dynamicApiRoute 'force-static' › should work |
| let process = |task_id: TaskId, | ||
| (meta, data): (Option<TaskStorage>, Option<TaskStorage>), | ||
| (meta, data, task_type): ( | ||
| Option<TaskStorage>, | ||
| Option<TaskStorage>, | ||
| Option<Arc<CachedTaskType>>, | ||
| ), | ||
| buffer: &mut TurboBincodeBuffer| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Funny formatting..
Maybe move it into a type X = to avoid that
| let meta = match meta { | ||
| Some(Ok(meta)) => { | ||
| #[cfg(feature = "print_cache_item_size")] | ||
| task_cache_stats | ||
| .lock() | ||
| .entry(self.debug_get_task_name(task_id)) | ||
| .or_default() | ||
| .add_meta(&meta); | ||
| Some(meta) | ||
| } | ||
| None => None, | ||
| Some(Err(err)) => { | ||
| println!( | ||
| "Serializing task {} failed (meta): {:?}", | ||
| self.debug_get_task_description(task_id), | ||
| err | ||
| ); | ||
| None | ||
| } | ||
| }; | ||
| let data = match data { | ||
| Some(Ok(data)) => { | ||
| #[cfg(feature = "print_cache_item_size")] | ||
| task_cache_stats | ||
| .lock() | ||
| .entry(self.debug_get_task_name(task_id)) | ||
| .or_default() | ||
| .add_data(&data); | ||
| Some(data) | ||
| } | ||
| None => None, | ||
| Some(Err(err)) => { | ||
| println!( | ||
| "Serializing task {} failed (data): {:?}", | ||
| self.debug_get_task_description(task_id), | ||
| err | ||
| ); | ||
| None | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you keep the print_cache_item_size logic?
| && !task.has_persistent_task_type() | ||
| { | ||
| let _ = task.set_persistent_task_type(task_type); | ||
| task.set_new_persistent_task(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was a bit worried that this will add a race condition, because the task can be persisted before we set the new_persistent_task flag.
I think it's fine, as the task id can't be referenced by other tasks before that.
So in the very rare case of this happening we only end up with a dangling unused task.
| process_snapshot, | ||
| scratch_buffer: TurboBincodeBuffer::with_capacity(SCRATCH_BUFFER_SIZE), | ||
| // Ideally these shards would be perfectly aligned with the dashmap so we could | ||
| // monolithically lock shards instead of axquiring a lock for each item. But doing this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
axquiring?

What?
Optimizes the change tracking mechanism used for persistent cache snapshots in turbo-tasks-backend.
Why?
The previous implementation used a
FxDashMap<TaskId, ModifiedState>to track modified tasks, whereModifiedStatewas an enum withModifiedandSnapshot(Option<Box<TaskStorage>>)variants. This had several inefficiencies:ModifiedStateenum was 16 bytes to account for the rare 'snapshot race' casemodifiedmap was highly sharded leading to large amounts of memory overhead. Most acquisitions were extremely short lived and so this isn't necessary.TaskCacheLogstructure (Sharded<ChunkedVec<...>>), requiring additional synchronization and a separate persistence path.How?
Restructured change tracking into two separate data structures:
modified: Sharded<Vec<TaskId>>- A sharded append-only list for tracking modified task IDs. Since modifications are guarded by a transition in theany_modifiedflag onTaskStorage, each task is only added once, avoiding duplicates without additional synchronization.snapshots: FxDashMap<TaskId, Option<Box<TaskStorage>>>- A small, rarely-used map for the uncommon case where a task is modified during an active snapshot operation. Uses only 16 shards since this is rare (only during dev mode idle-callback persistence races).Unified task cache persistence:
TaskCacheLogandChunkedVecinfrastructuretask_typedirectly in theSnapshotItemstructOther optimizations:
SmallVec::into_boxed_slice()now called directly instead ofinto_vec().into_boxed_slice()(avoids intermediate allocation)swap_retainutility (no longer needed)scope_and_blockwith chunked shards, this reduces the number of 'scratch buffers' allocated to 'one per chunk' instead of 'one per original shard'end_snapshotwhere multiple racing save_snapshot calls could corrupt state.Results on vercel-site
I ran a sequence of builds on vercel-site comparing canary and this branch
Cold Cache Builds
Warm Cache Builds
The latency deltas are in the noise but there are substantial memory wins from this change