Skip to content

Commit 0089e29

Browse files
committed
perf(plugin-nm): cache install state across workspaces
This lowers memory usage of yarn from around 7GB to 600MB on a repo I have with 223 workspaces and 3MB node_modules/.yarn-state.yml.
1 parent d2afdfc commit 0089e29

2 files changed

Lines changed: 72 additions & 14 deletions

File tree

.yarn/versions/dcd1a21a.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/plugin-nm": patch
4+
5+
declined:
6+
- "@yarnpkg/plugin-compat"
7+
- "@yarnpkg/plugin-constraints"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-init"
11+
- "@yarnpkg/plugin-interactive-tools"
12+
- "@yarnpkg/plugin-npm-cli"
13+
- "@yarnpkg/plugin-pack"
14+
- "@yarnpkg/plugin-patch"
15+
- "@yarnpkg/plugin-pnp"
16+
- "@yarnpkg/plugin-pnpm"
17+
- "@yarnpkg/plugin-stage"
18+
- "@yarnpkg/plugin-typescript"
19+
- "@yarnpkg/plugin-version"
20+
- "@yarnpkg/plugin-workspace-tools"
21+
- "@yarnpkg/builder"
22+
- "@yarnpkg/core"
23+
- "@yarnpkg/doctor"

packages/plugin-nm/sources/NodeModulesLinker.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,28 @@ const INSTALL_STATE_FILE = `.yarn-state.yml` as Filename;
2323
const MTIME_ACCURANCY = 1000;
2424

2525
type InstallState = {locatorMap: NodeModulesLocatorMap, locationTree: LocationTree, binSymlinks: BinSymlinkMap, nmMode: NodeModulesMode, mtimeMs: number};
26+
type InstallStateCacheEntry = {statKey: string | null, promise: Promise<InstallState | null>};
2627
type BinSymlinkMap = Map<PortablePath, Map<Filename, PortablePath>>;
2728
type LoadManifest = (locator: LocatorKey, installLocation: PortablePath) => Promise<Pick<Manifest, 'bin'>>;
2829

30+
const installStateCache: Map<string, InstallStateCacheEntry> = new Map();
31+
32+
function getInstallStateCacheKey(project: Project, {unrollAliases}: {unrollAliases: boolean}) {
33+
return `${project.cwd}\0${unrollAliases ? `unroll` : `plain`}`;
34+
}
35+
36+
function clearInstallStateCache(project: Project) {
37+
installStateCache.delete(getInstallStateCacheKey(project, {unrollAliases: true}));
38+
installStateCache.delete(getInstallStateCacheKey(project, {unrollAliases: false}));
39+
}
40+
2941
export enum NodeModulesMode {
3042
CLASSIC = `classic`,
3143
HARDLINKS_LOCAL = `hardlinks-local`,
3244
HARDLINKS_GLOBAL = `hardlinks-global`,
3345
}
3446

3547
export class NodeModulesLinker implements Linker {
36-
private installStateCache: Map<string, Promise<InstallState | null>> = new Map();
37-
3848
getCustomDataKey() {
3949
return JSON.stringify({
4050
name: `NodeModulesLinker`,
@@ -54,9 +64,7 @@ export class NodeModulesLinker implements Linker {
5464
if (workspace)
5565
return workspace.cwd;
5666

57-
const installState = await miscUtils.getFactoryWithDefault(this.installStateCache, opts.project.cwd, async () => {
58-
return await findInstallState(opts.project, {unrollAliases: true});
59-
});
67+
const installState = await this.findInstallState(opts.project, {unrollAliases: true});
6068

6169
if (installState === null)
6270
throw new UsageError(`Couldn't find the node_modules state file - running an install might help (findPackageLocation)`);
@@ -79,9 +87,7 @@ export class NodeModulesLinker implements Linker {
7987
if (!this.isEnabled(opts))
8088
return null;
8189

82-
const installState = await miscUtils.getFactoryWithDefault(this.installStateCache, opts.project.cwd, async () => {
83-
return await findInstallState(opts.project, {unrollAliases: true});
84-
});
90+
const installState = await this.findInstallState(opts.project, {unrollAliases: true});
8591

8692
if (installState === null)
8793
return null;
@@ -107,6 +113,19 @@ export class NodeModulesLinker implements Linker {
107113
return new NodeModulesInstaller(opts);
108114
}
109115

116+
private async findInstallState(project: Project, {unrollAliases}: {unrollAliases: boolean}) {
117+
const cacheKey = getInstallStateCacheKey(project, {unrollAliases});
118+
const statKey = await getInstallStateStatKey(project);
119+
const cached = installStateCache.get(cacheKey);
120+
121+
if (cached?.statKey === statKey)
122+
return await cached.promise;
123+
124+
const promise = findInstallState(project, {unrollAliases});
125+
installStateCache.set(cacheKey, {statKey, promise});
126+
return await promise;
127+
}
128+
110129
private isEnabled(opts: MinimalLinkOptions) {
111130
return opts.project.configuration.get(`nodeLinker`) === `node-modules`;
112131
}
@@ -447,13 +466,29 @@ async function writeInstallState(project: Project, locatorMap: NodeModulesLocato
447466
const rootPath = project.cwd;
448467
const installStatePath = ppath.join(rootPath, NODE_MODULES, INSTALL_STATE_FILE);
449468

450-
// Force install state file rewrite, so that it has mtime bigger than all node_modules subfolders
451-
if (installChangedByUser)
452-
await xfs.removePromise(installStatePath);
469+
try {
470+
// Force install state file rewrite, so that it has mtime bigger than all node_modules subfolders
471+
if (installChangedByUser)
472+
await xfs.removePromise(installStatePath);
453473

454-
await xfs.changeFilePromise(installStatePath, locatorState, {
455-
automaticNewlines: true,
456-
});
474+
await xfs.changeFilePromise(installStatePath, locatorState, {
475+
automaticNewlines: true,
476+
});
477+
} finally {
478+
clearInstallStateCache(project);
479+
}
480+
}
481+
482+
async function getInstallStateStatKey(project: Project) {
483+
const rootPath = project.cwd;
484+
const installStatePath = ppath.join(rootPath, NODE_MODULES, INSTALL_STATE_FILE);
485+
486+
try {
487+
const stats = await xfs.statPromise(installStatePath);
488+
return `${stats.mtimeMs}:${stats.size}`;
489+
} catch {
490+
return null;
491+
}
457492
}
458493

459494
async function findInstallState(project: Project, {unrollAliases = false}: {unrollAliases?: boolean} = {}): Promise<InstallState | null> {

0 commit comments

Comments
 (0)