Skip to content

Commit 133a8a1

Browse files
committed
vfs: hoist path normalization out of dispatch loop
VirtualFileSystem#shouldHandle was recomputing both toNamespacedPath(resolve(mountPoint)) AND toNamespacedPath(resolve(inputPath)) on every fs call, for every active VFS. With N mounted layers this meant 2N path normalizations per fs op against a real-filesystem path that no VFS owns. Cache the normalized mount point on the VFS instance at mount time, and add a shouldHandleNormalized fast path that takes an already-normalized input. The setup.js dispatch helpers (findVFSForStat / findVFSForRead / findVFSForExists / findVFSForPath / findVFSWith / findVFSPackageJSON / legacyMainResolve / getLayerForPath) now normalize the input once before the walk and share it across every layer. Benchmarked with benchmark/vfs/bench-fs-dispatch.js on a real-filesystem target path under 0/1/2/5/10 mounted layers: op layers=10 before layers=10 after speedup statSync 96k ops/s 278k ops/s 2.9x existsSync 99k ops/s 315k ops/s 3.2x accessSync 98k ops/s 309k ops/s 3.2x readFileSync 42k ops/s 99k ops/s 2.4x Per-additional-layer cost drops from ~860 ns to ~80 ns for stat/exists/access (and from ~1700 ns to ~200 ns for readFile). The layers=0 (VFS not loaded) path is unaffected: vfsState.handlers stays null and fs ops short-circuit on a single null check. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 7bedf50 commit 133a8a1

2 files changed

Lines changed: 53 additions & 14 deletions

File tree

lib/internal/vfs/file_system.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => {
4242
// Private symbols
4343
const kProvider = Symbol('kProvider');
4444
const kMountPoint = Symbol('kMountPoint');
45+
// Cached `normalizeMountedPath(kMountPoint)` so the hot dispatch path
46+
// (shouldHandle) does not recompute toNamespacedPath(resolve(...)) on
47+
// every fs call. Set at mount time, cleared at unmount.
48+
const kNormalizedMountPoint = Symbol('kNormalizedMountPoint');
4549
const kMounted = Symbol('kMounted');
4650
const kPromises = Symbol('kPromises');
4751
const kLayerId = Symbol('kLayerId');
@@ -119,6 +123,7 @@ class VirtualFileSystem {
119123

120124
this[kProvider] = provider ?? new MemoryProvider();
121125
this[kMountPoint] = null;
126+
this[kNormalizedMountPoint] = null;
122127
this[kMounted] = false;
123128
this[kPromises] = null; // Lazy-initialized
124129
this[kLayerId] = nextLayerId++;
@@ -180,6 +185,7 @@ class VirtualFileSystem {
180185
throw new ERR_INVALID_STATE('VFS is already mounted');
181186
}
182187
this[kMountPoint] = resolvePath(prefix);
188+
this[kNormalizedMountPoint] = normalizeMountedPath(this[kMountPoint]);
183189
this[kMounted] = true;
184190
debug('mount %s', this[kMountPoint]);
185191
loadVfsSetup();
@@ -195,6 +201,7 @@ class VirtualFileSystem {
195201
loadVfsSetup();
196202
deregisterVFS(this);
197203
this[kMountPoint] = null;
204+
this[kNormalizedMountPoint] = null;
198205
this[kMounted] = false;
199206
}
200207

@@ -214,12 +221,27 @@ class VirtualFileSystem {
214221
* @returns {boolean}
215222
*/
216223
shouldHandle(inputPath) {
217-
if (!this[kMounted] || !this[kMountPoint]) {
224+
const mountPoint = this[kNormalizedMountPoint];
225+
if (mountPoint === null) {
218226
return false;
219227
}
220-
const normalized = normalizeMountedPath(inputPath);
221-
const mountPoint = normalizeMountedPath(this[kMountPoint]);
222-
return isUnderMountPoint(normalized, mountPoint);
228+
return isUnderMountPoint(normalizeMountedPath(inputPath), mountPoint);
229+
}
230+
231+
/**
232+
* Fast path used by the VFS dispatch loops, which can normalize the
233+
* input path once and reuse the result across every active VFS. Saves
234+
* an O(N) path.resolve()+toNamespacedPath() per fs call.
235+
* @param {string} normalizedInput A path already passed through
236+
* normalizeMountedPath.
237+
* @returns {boolean}
238+
*/
239+
shouldHandleNormalized(normalizedInput) {
240+
const mountPoint = this[kNormalizedMountPoint];
241+
if (mountPoint === null) {
242+
return false;
243+
}
244+
return isUnderMountPoint(normalizedInput, mountPoint);
223245
}
224246

225247
/**
@@ -280,9 +302,9 @@ class VirtualFileSystem {
280302
* @returns {string}
281303
*/
282304
#toProviderPath(inputPath) {
283-
if (this[kMounted] && this[kMountPoint]) {
305+
const mountPoint = this[kNormalizedMountPoint];
306+
if (mountPoint !== null) {
284307
const resolved = normalizeMountedPath(inputPath);
285-
const mountPoint = normalizeMountedPath(this[kMountPoint]);
286308
if (!isUnderMountPoint(resolved, mountPoint)) {
287309
throw createENOENT('open', inputPath);
288310
}
@@ -1359,4 +1381,5 @@ class VirtualFileSystem {
13591381

13601382
module.exports = {
13611383
VirtualFileSystem,
1384+
normalizeMountedPath,
13621385
};

lib/internal/vfs/setup.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
},
3333
} = require('internal/errors');
3434
const { createENOENT, createEXDEV } = require('internal/vfs/errors');
35+
const { normalizeMountedPath } = require('internal/vfs/file_system');
3536
const { getVirtualFd, closeVirtualFd } = require('internal/vfs/fd');
3637
const { assertEncoding, setVfsHandlers } = require('internal/fs/utils');
3738
const permission = require('internal/process/permission');
@@ -241,9 +242,11 @@ function vfsStat(vfs, filePath) {
241242
* @returns {{ vfs: object, result: number }|null}
242243
*/
243244
function findVFSForStat(filename) {
245+
if (activeVFSList.length === 0) return null;
246+
const normalized = normalizeMountedPath(filename);
244247
for (let i = 0; i < activeVFSList.length; i++) {
245248
const vfs = activeVFSList[i];
246-
if (vfs.shouldHandle(filename)) {
249+
if (vfs.shouldHandleNormalized(normalized)) {
247250
vfs.recordOwnedFilename(filename);
248251
return { vfs, result: vfsStat(vfs, filename) };
249252
}
@@ -258,9 +261,11 @@ function findVFSForStat(filename) {
258261
* @returns {{ vfs: object, content: Buffer|string }|null}
259262
*/
260263
function findVFSForRead(filename, options) {
264+
if (activeVFSList.length === 0) return null;
265+
const normalized = normalizeMountedPath(filename);
261266
for (let i = 0; i < activeVFSList.length; i++) {
262267
const vfs = activeVFSList[i];
263-
if (vfs.shouldHandle(filename)) {
268+
if (vfs.shouldHandleNormalized(normalized)) {
264269
vfs.recordOwnedFilename(filename);
265270
if (vfs.existsSync(filename) && vfsStat(vfs, filename) === 0) {
266271
return { vfs, content: vfs.readFileSync(filename, options) };
@@ -357,9 +362,10 @@ function findVFSPackageJSON(startPath) {
357362
}
358363
const pjsonPath = join(currentDir, 'package.json');
359364
sentinel = pjsonPath;
365+
const normalizedPjsonPath = normalizeMountedPath(pjsonPath);
360366
for (let i = 0; i < activeVFSList.length; i++) {
361367
const vfs = activeVFSList[i];
362-
if (vfs.shouldHandle(pjsonPath) && vfsStat(vfs, pjsonPath) === 0) {
368+
if (vfs.shouldHandleNormalized(normalizedPjsonPath) && vfsStat(vfs, pjsonPath) === 0) {
363369
try {
364370
const content = vfs.readFileSync(pjsonPath, 'utf8');
365371
const parsed = JSONParse(content);
@@ -376,19 +382,23 @@ function findVFSPackageJSON(startPath) {
376382
}
377383

378384
function findVFSForExists(filename) {
385+
if (activeVFSList.length === 0) return null;
386+
const normalized = normalizeMountedPath(filename);
379387
for (let i = 0; i < activeVFSList.length; i++) {
380388
const vfs = activeVFSList[i];
381-
if (vfs.shouldHandle(filename)) {
389+
if (vfs.shouldHandleNormalized(normalized)) {
382390
return { vfs, exists: vfs.existsSync(filename) };
383391
}
384392
}
385393
return null;
386394
}
387395

388396
function findVFSForPath(filename) {
397+
if (activeVFSList.length === 0) return null;
398+
const normalized = normalizeMountedPath(filename);
389399
for (let i = 0; i < activeVFSList.length; i++) {
390400
const vfs = activeVFSList[i];
391-
if (vfs.shouldHandle(filename)) {
401+
if (vfs.shouldHandleNormalized(normalized)) {
392402
return { vfs, normalized: filename };
393403
}
394404
}
@@ -397,9 +407,11 @@ function findVFSForPath(filename) {
397407

398408
// Sync read: check exists first, fall through to ENOENT for mounted VFS.
399409
function findVFSWith(filename, syscall, fn) {
410+
if (activeVFSList.length === 0) return undefined;
411+
const normalized = normalizeMountedPath(filename);
400412
for (let i = 0; i < activeVFSList.length; i++) {
401413
const vfs = activeVFSList[i];
402-
if (vfs.shouldHandle(filename)) {
414+
if (vfs.shouldHandleNormalized(normalized)) {
403415
vfs.recordOwnedFilename(filename);
404416
if (vfs.existsSync(filename)) {
405417
return fn(vfs, filename);
@@ -961,9 +973,11 @@ function installModuleLoaderOverrides() {
961973
return findVFSWith(filename, 'realpath', (vfs, n) => vfs.realpathSync(n));
962974
},
963975
legacyMainResolve(pkgPath, main, base) {
976+
if (activeVFSList.length === 0) return undefined;
977+
const normalizedPkg = normalizeMountedPath(pkgPath);
964978
let handled = false;
965979
for (let i = 0; i < activeVFSList.length; i++) {
966-
if (activeVFSList[i].shouldHandle(pkgPath)) {
980+
if (activeVFSList[i].shouldHandleNormalized(normalizedPkg)) {
967981
handled = true;
968982
break;
969983
}
@@ -1023,9 +1037,11 @@ function installModuleLoaderOverrides() {
10231037
return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT
10241038
},
10251039
getLayerForPath(filename) {
1040+
if (activeVFSList.length === 0) return undefined;
1041+
const normalized = normalizeMountedPath(filename);
10261042
for (let i = 0; i < activeVFSList.length; i++) {
10271043
const vfs = activeVFSList[i];
1028-
if (vfs.shouldHandle(filename)) return vfs.layerId;
1044+
if (vfs.shouldHandleNormalized(normalized)) return vfs.layerId;
10291045
}
10301046
return undefined;
10311047
},

0 commit comments

Comments
 (0)