Reuse a sibling worktree's node_modules when CoW is available in hoisted mode (44s → 13s in fresh install) #14041
Closed
arthur-fontaine
started this conversation in
Ideas
Replies: 1 comment 1 reply
|
We already have a solution for this: https://pnpm.io/git-worktrees |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
EDIT: it concerns only hoisted mode, as isolated mode already has a solution (https://pnpm.io/git-worktrees)
The problem
Worktrees have gotten a lot more popular in the AI era, and I work in them myself.
Every new worktree of the same repo starts with no
node_modules, so pnpm installs the whole tree from scratch. On my work's 3,582-package workspace that is 212,896 files and 44 seconds, almost all of it disk writes (timings below). Meanwhile... the worktree next door already holds the exact same tree.Where the time goes currently
3,582 packages, 212,896 files, 41,000 directories. macOS (M4 Pro, 24GB of RAM, Tahoe 26.2) on APFS, hoisted linker, lockfile already up to date.
The 44 seconds of a normal fresh install:
As you can see, writing the tree is where the cost is.
What I tried
When an install finds no
node_modules, it clones one from another worktree of the same repository, found withgit worktree listand checked against the gate below until one fits, with a singleclonefilecall over the whole hierarchy, then runs the normal reconcile. The clone brings.modules.yamland the lockfile with it, so the install reads it as the previous install's state. That takes the 44 seconds down to 13.4.This is only safe because of copy-on-write: the two trees share storage until one side writes, so neither worktree sees the other's later edits.
The donor gate
A donor only gets used if its lockfile, workspace manifest and project manifests are byte-identical to this worktree's, and its own tree already holds what its lockfile asks for. Anything stale is skipped, so the feature cannot make an install slower. That gate exists because I measured the alternative: seeding from a donor whose tree lags its own lockfile costs 78s, worse than the 44s of not seeding at all.
The awkward part is the state file the clone brings along:
node_modules/.pnpm-workspace-state-v1.json. Its project entries are keyed by absolute path, and some setting values carry absolute paths too. Read from the new worktree, it describes a workspace whose projects all moved, so the install falls back to a full install. I repoint every path under the donor root, matching on path prefix rather than raw substring, so a value that merely shares a leading string is untouched. If the rewrite fails I delete the file rather than leave it half corrected.The other field is
lastValidatedTimestamp, when pnpm last confirmed the manifests and patches matched the tree. The freshness check compares file mtimes against it, and a git checkout stamps every file with the current time, so the check would read unchanged manifests as edits and reinstall the tree it was just handed. I set the timestamp to now, sound only because the gate already compared the lockfile and every manifest byte for byte.New benchmark
Same workspace and machine as above.
Status
A PoC is implemented in the Rust CLI, always on, no setting. It does nothing on filesystems that cannot clone a directory in one operation, which today means APFS is the only one it works on. Btrfs and ReFS should be able to benefit as well, though less, since neither can clone a whole directory in one call as far as I know.
Source code: https://github.com/arthur-fontaine/pnpm/tree/feat-seed-node-modules-from-sibling-worktree.
I will open a PR if the direction sounds right, but it will need some reviews/refactorings because it is entirely coded with Claude Code.
Try it
Here is a release build: pnpm-12.0.0-rc.7-seed-worktree-macos-arm64.zip, sha256
534d059307ebb11703e99b5b45f5ed80c9f77bb7b5b6c0767803a7a6e846b513.It is unsigned, so macOS quarantines it on download.
Then, in a repo with a committed lockfile and an installed node_modules:
A seeded install prints
Cloned node_modules from <donor> in 13.0s (clone 13.0s)before the usual output. If it does not print that, no worktree passed the gate; run withTRACE=pacquet::install=debugenv var and the log says which check rejected it.Open questions
node_moduleswe pick? Rather than requiring byte-identical manifests, we could allow small divergence, like added, removed or updated dependencies against the current worktree. That only pays if reconciling the difference could be cheaper than installing from scratch, and today it is not: the 78s above is what a diverging tree costs, because the reconcile rebuilds instead of patching.All reactions