Skip to content

metro-file-map: Write the cache without copying the file map first - #2219

Draft
robhogan wants to merge 1 commit into
pr2108from
pr2219
Draft

robhogan wants to merge 1 commit into
pr2108from
pr2219

Conversation

@robhogan

Copy link
Copy Markdown
Collaborator

TreeFS.getSerializableSnapshot() deep-copies the whole tree - a Map per directory and an array per file - before every cache write:

#cloneTree(root: DirectoryNode): DirectoryNode {
const clone: DirectoryNode = new Map();
for (const [name, node] of root) {
if (isDirectory(node)) {
clone.set(name, this.#cloneTree(node));
} else {
clone.set(name, [...node]);
}
}
return clone;
}

DiskCacheManager serialises the snapshot as soon as it asks for it, so nothing can change it in between and the copy is thrown away:

const data = getSnapshot();
this.#hasUnwrittenChanges = false;
const serialized = serialize(data);

This returns the tree itself. That saves a copy of the file map on every cold start, and on every save of the cache while watching.

A custom cache manager (unstable_fileMapCacheManagerFactory) that holds on to the snapshot to serialise later would now see later changes, so CacheManager.write's docs now say to serialise what getSnapshot returns straight away, or copy it. Expo's fork (@expo/metro-file-map) already skips the deep copy, for the same reason.

Benchmark

This takes 28-51% off writing the cache (Windows: 19.4ms -> 9.5ms), or 2-5% off a cold start on Linux and Windows. macOS's cold starts are too noisy to show it. The same copy was made on every save of the cache while watching, which this benchmark doesn't exercise.

main #2108 This PR vs main vs #2108
macOS: Cache write in a cold start 7.8ms 6.6ms 4.3ms -44.4% (-45.1% to -43.6%) -34.2% (-35.4% to -33.7%)
Linux: Cache write in a cold start 30ms 28ms 20ms -31.9% (-39.7% to -22.0%) -27.9% (-32.0% to -25.1%)
Windows: Cache write in a cold start 20ms 19.4ms 9.5ms -53.5% (-54.9% to -36.4%) -51.0% (-53.2% to -32.1%)
macOS: Cold start 129ms 96ms 90ms -30.2% (-36.0% to -22.6%) -5.7% (-14.6% to +8.9%)
Linux: Cold start 648ms 324ms 319ms -50.8% (-51.7% to -50.1%) -1.8% (-5.0% to -0.5%)
Windows: Cold start 363ms 179ms 170ms -53.2% (-53.8% to -52.5%) -5.1% (-6.8% to -2.6%)
macOS: Watch-mode start, cold cache 134ms 98ms 96ms -28.6% (-29.5% to -27.0%) -2.7% (-4.5% to +0.2%)
Linux: Watch-mode start, cold cache 1,892ms 470ms 451ms -76.1% (-76.3% to -75.8%) -3.9% (-4.9% to -0.9%)
Windows: Watch-mode start, cold cache 2,556ms 496ms 490ms -80.8% (-80.9% to -80.8%) -1.1% (-1.6% to -0.7%)

Changelog

 - **[Performance]**: metro-file-map: Write the cache without copying the file map first

Test plan

New unit test that DiskCacheManager serialises the snapshot before anything else can run. Benchmark as in #2104, with 24 rounds on macOS.

`TreeFS.getSerializableSnapshot()` deep-copies the whole tree - a `Map` per directory and an array per file - before every cache write:

https://github.com/react/metro/blob/1461d81e97fa8fb83a1017925c2870e260d833cc/packages/metro-file-map/src/lib/TreeFS.js#L1476-L1486

`DiskCacheManager` serialises the snapshot as soon as it asks for it, so nothing can change it in between and the copy is thrown away:

https://github.com/react/metro/blob/1461d81e97fa8fb83a1017925c2870e260d833cc/packages/metro-file-map/src/cache/DiskCacheManager.js#L119-L121

This returns the tree itself. That saves a copy of the file map on every cold start, and on every save of the cache while watching.

A custom cache manager (`unstable_fileMapCacheManagerFactory`) that holds on to the snapshot to serialise later would now see later changes, so `CacheManager.write`'s docs now say to serialise what `getSnapshot` returns straight away, or copy it. Expo's fork (`@expo/metro-file-map`) already skips the deep copy, for the same reason.

## Benchmark

This takes 28-51% off writing the cache (Windows: 19.4ms -> 9.5ms), or 2-5% off a cold start on Linux and Windows. macOS's cold starts are too noisy to show it. The same copy was made on every save of the cache while watching, which this benchmark doesn't exercise.

|  | `main` | #2108 | This PR | vs `main` | vs #2108 |
|---|---|---|---|---|---|
| macOS: Cache write in a cold start | 7.8ms | 6.6ms | 4.3ms | -44.4% (-45.1% to -43.6%) | -34.2% (-35.4% to -33.7%) |
| Linux: Cache write in a cold start | 30ms | 28ms | 20ms | -31.9% (-39.7% to -22.0%) | -27.9% (-32.0% to -25.1%) |
| Windows: Cache write in a cold start | 20ms | 19.4ms | 9.5ms | -53.5% (-54.9% to -36.4%) | -51.0% (-53.2% to -32.1%) |
| macOS: Cold start | 129ms | 96ms | 90ms | -30.2% (-36.0% to -22.6%) | -5.7% (-14.6% to +8.9%) |
| Linux: Cold start | 648ms | 324ms | 319ms | -50.8% (-51.7% to -50.1%) | -1.8% (-5.0% to -0.5%) |
| Windows: Cold start | 363ms | 179ms | 170ms | -53.2% (-53.8% to -52.5%) | -5.1% (-6.8% to -2.6%) |
| macOS: Watch-mode start, cold cache | 134ms | 98ms | 96ms | -28.6% (-29.5% to -27.0%) | -2.7% (-4.5% to +0.2%) |
| Linux: Watch-mode start, cold cache | 1,892ms | 470ms | 451ms | -76.1% (-76.3% to -75.8%) | -3.9% (-4.9% to -0.9%) |
| Windows: Watch-mode start, cold cache | 2,556ms | 496ms | 490ms | -80.8% (-80.9% to -80.8%) | -1.1% (-1.6% to -0.7%) |

## Changelog

```
 - **[Performance]**: metro-file-map: Write the cache without copying the file map first
```

## Test plan

New unit test that `DiskCacheManager` serialises the snapshot before anything else can run. Benchmark as in #2104, with 24 rounds on macOS.
@robhogan
robhogan added this pull request to stack #2109 November 11, 2026 19:31
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 11, 2026

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant