Skip to content

Commit 77427b7

Browse files
jeswrclaude
andcommitted
perf: Reduce and parallelize file system operations in the module state scan
ModuleStateBuilder.buildModuleState walks the entire node_modules tree on every ComponentsManager build (1,381 modules for a Community Solid Server install). Three refinements, none of which change the resulting module state: 1. buildNodeModulePathsInner awaited the recursion for each non-scoped dependency sequentially, while scoped dependencies were already handled with Promise.all. Both now recurse in parallel. (The ignorePaths check-and-set is synchronous after the realpath await, so deduplication is unaffected.) 2. buildPackageJsons performed a stat (fileExists) followed by a readFile for every module; it now reads directly and treats a read failure as "no package.json", halving the file system operations of the hottest phase (1,381 -> 0 extra stats). Malformed JSON still throws exactly as before. 3. preprocessPackageJson probed components/components.jsonld, components/context.jsonld, components/ and config/ with four sequential stats per lsd:module package; the probes now run in parallel. A/B against the real Community Solid Server node_modules tree (1,381 modules, warm file system cache, 9 reps, 2-core shared box, alternating arms in the same run): before: median 355 ms (min 324) after: median 259-267 ms (min 236) ~1.35x, -90 ms per boot The resulting module state is identical in both arms (1,381 nodeModulePaths / 1,381 packageJsons / 168 componentModules / 169 contexts / 175 importPaths). The gain grows with cold file caches, where the parallel recursion overlaps disk latency instead of paying it serially. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0512b9a commit 77427b7

1 file changed

Lines changed: 38 additions & 30 deletions

File tree

lib/loading/ModuleStateBuilder.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ export class ModuleStateBuilder {
115115

116116
// Recursively handle all the Node modules of this valid Node module
117117
const dependenciesPath = Path.posix.join(path, 'node_modules');
118-
for (const dependency of await fs.readdir(dependenciesPath)) {
118+
const dependencies = await fs.readdir(dependenciesPath);
119+
await Promise.all(dependencies.map(async(dependency) => {
119120
// Ignore hidden folders, such as .bin
120121
if (!dependency.startsWith('.')) {
121122
const dependencyPath = Path.posix.join(dependenciesPath, dependency);
@@ -131,7 +132,7 @@ export class ModuleStateBuilder {
131132
await this.buildNodeModulePathsInner(dependencyPath, nodeModulePaths, ignorePaths);
132133
}
133134
}
134-
}
135+
}));
135136
}
136137
} catch {
137138
// Ignore invalid paths
@@ -155,9 +156,16 @@ export class ModuleStateBuilder {
155156
const packageJsons: Record<string, any> = {};
156157
await Promise.all(nodeModulePaths.map(async(modulePath) => {
157158
const path = Path.posix.join(modulePath, 'package.json');
158-
if (await this.fileExists(path)) {
159-
packageJsons[modulePath] = JSON.parse(await fs.readFile(path, 'utf8'));
159+
// Try the read directly instead of checking existence first,
160+
// which halves the number of file system operations.
161+
let contents: string;
162+
try {
163+
contents = await fs.readFile(path, 'utf8');
164+
} catch {
165+
// Ignore modules without a package.json file
166+
return;
160167
}
168+
packageJsons[modulePath] = JSON.parse(contents);
161169
}));
162170
return packageJsons;
163171
}
@@ -181,37 +189,37 @@ export class ModuleStateBuilder {
181189
if (packageJson['lsd:module'] === true) {
182190
packageJson['lsd:module'] = `https://linkedsoftwaredependencies.org/bundles/npm/${packageJson.name}`;
183191
const basePath = packageJson['lsd:basePath'] || '';
184-
try {
185-
if ((await fs.stat(Path.posix.join(packagePath, basePath, 'components/components.jsonld'))).isFile()) {
186-
packageJson['lsd:components'] = `${basePath}components/components.jsonld`;
187-
}
188-
} catch {
189-
// Ignore errors
190-
}
191192
const baseIri = `${packageJson['lsd:module']}/^${semverMajor(packageJson.version)}.0.0/`;
192-
try {
193-
if ((await fs.stat(Path.posix.join(packagePath, basePath, 'components/context.jsonld'))).isFile()) {
194-
packageJson['lsd:contexts'] = {
195-
[`${baseIri}components/context.jsonld`]: `${basePath}components/context.jsonld`,
196-
};
193+
// Probe the file system in parallel instead of sequentially.
194+
const probe = async(subPath: string, file: boolean): Promise<boolean> => {
195+
try {
196+
const stat = await fs.stat(Path.posix.join(packagePath, basePath, subPath));
197+
return file ? stat.isFile() : stat.isDirectory();
198+
} catch {
199+
// Ignore errors
200+
return false;
197201
}
198-
} catch {
199-
// Ignore errors
202+
};
203+
const [ hasComponents, hasContext, hasComponentsDir, hasConfigDir ] = await Promise.all([
204+
probe('components/components.jsonld', true),
205+
probe('components/context.jsonld', true),
206+
probe('components', false),
207+
probe('config', false),
208+
]);
209+
if (hasComponents) {
210+
packageJson['lsd:components'] = `${basePath}components/components.jsonld`;
211+
}
212+
if (hasContext) {
213+
packageJson['lsd:contexts'] = {
214+
[`${baseIri}components/context.jsonld`]: `${basePath}components/context.jsonld`,
215+
};
200216
}
201217
packageJson['lsd:importPaths'] = {};
202-
try {
203-
if ((await fs.stat(Path.posix.join(packagePath, basePath, 'components'))).isDirectory()) {
204-
packageJson['lsd:importPaths'][`${baseIri}components/`] = `${basePath}components/`;
205-
}
206-
} catch {
207-
// Ignore errors
218+
if (hasComponentsDir) {
219+
packageJson['lsd:importPaths'][`${baseIri}components/`] = `${basePath}components/`;
208220
}
209-
try {
210-
if ((await fs.stat(Path.posix.join(packagePath, basePath, 'config'))).isDirectory()) {
211-
packageJson['lsd:importPaths'][`${baseIri}config/`] = `${basePath}config/`;
212-
}
213-
} catch {
214-
// Ignore errors
221+
if (hasConfigDir) {
222+
packageJson['lsd:importPaths'][`${baseIri}config/`] = `${basePath}config/`;
215223
}
216224
return true;
217225
}

0 commit comments

Comments
 (0)