Skip to content

Commit bca620d

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 bca620d

1 file changed

Lines changed: 56 additions & 14 deletions

File tree

packages/plugin-nm/sources/NodeModulesLinker.ts

Lines changed: 56 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,26 @@ 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+
let promise: Promise<InstallState | null>;
125+
promise = findInstallState(project, {unrollAliases}).catch(error => {
126+
const cached = installStateCache.get(cacheKey);
127+
if (cached?.promise === promise)
128+
installStateCache.delete(cacheKey);
129+
throw error;
130+
});
131+
132+
installStateCache.set(cacheKey, {statKey, promise});
133+
return await promise;
134+
}
135+
110136
private isEnabled(opts: MinimalLinkOptions) {
111137
return opts.project.configuration.get(`nodeLinker`) === `node-modules`;
112138
}
@@ -447,13 +473,29 @@ async function writeInstallState(project: Project, locatorMap: NodeModulesLocato
447473
const rootPath = project.cwd;
448474
const installStatePath = ppath.join(rootPath, NODE_MODULES, INSTALL_STATE_FILE);
449475

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);
476+
try {
477+
// Force install state file rewrite, so that it has mtime bigger than all node_modules subfolders
478+
if (installChangedByUser)
479+
await xfs.removePromise(installStatePath);
453480

454-
await xfs.changeFilePromise(installStatePath, locatorState, {
455-
automaticNewlines: true,
456-
});
481+
await xfs.changeFilePromise(installStatePath, locatorState, {
482+
automaticNewlines: true,
483+
});
484+
} finally {
485+
clearInstallStateCache(project);
486+
}
487+
}
488+
489+
async function getInstallStateStatKey(project: Project) {
490+
const rootPath = project.cwd;
491+
const installStatePath = ppath.join(rootPath, NODE_MODULES, INSTALL_STATE_FILE);
492+
493+
try {
494+
const stats = await xfs.statPromise(installStatePath);
495+
return `${stats.mtimeMs}:${stats.size}`;
496+
} catch (e) {
497+
return null;
498+
}
457499
}
458500

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

0 commit comments

Comments
 (0)