Skip to content

Commit 692498a

Browse files
pipobscureclaude
andcommitted
vfs: clarify name resolution and add fs benchmark
Rename `followLast` to `followLink` and document it as the difference between `stat()` and `lstat()`. Use `path.join()` for the mount name prefixes purged on register and deregister, and explain why the hot path in `resolveVFS()` concatenates instead. Add a benchmark for the cost of the fs hooks on a real path and on a layer path reached by id or by mount name. On Windows x64 (Release, three runs of `statSync`, n=100000): ops/s vfs/fs-resolve.js target=real 80,929 82,063 82,618 vfs/fs-resolve.js target=id 67,669 66,850 63,834 vfs/fs-resolve.js target=name 65,911 64,155 60,767 Resolving through a mount name costs about 4% over resolving by id. Against the merge base (dd5dfb5), with 30 interleaved runs each and a Welch t-test, there is no significant difference: main ops/s this ops/s change 95% CI p target=id 67,837 67,532 -0.45% [-1.69%, +0.79%] 0.47 target=real 81,951 80,991 -1.17% [-5.91%, +3.57%] 0.62 Main has no mount names, so `target=name` has no baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6570316 commit 692498a

2 files changed

Lines changed: 58 additions & 18 deletions

File tree

benchmark/vfs/fs-resolve.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
const fs = require('fs');
3+
const path = require('path');
4+
const common = require('../common.js');
5+
6+
// Measures what the fs hooks cost a call once a VFS is mounted: a real path
7+
// only has to be told apart from the VFS root, while a path in a layer is
8+
// resolved to the layer, either by its id or through its mount name.
9+
const bench = common.createBenchmark(main, {
10+
target: ['real', 'id', 'name'],
11+
n: [1e5],
12+
}, { flags: ['--experimental-vfs', '--no-warnings'] });
13+
14+
function main({ n, target }) {
15+
const vfs = require('node:vfs');
16+
const layer = vfs.create();
17+
layer.mkdirSync('/dir');
18+
layer.writeFileSync('/dir/file.txt', 'x');
19+
const mountPoint = layer.mount('bench');
20+
const file = {
21+
real: __filename,
22+
id: path.join(mountPoint, 'dir', 'file.txt'),
23+
name: path.join(path.dirname(mountPoint), 'bench', 'dir', 'file.txt'),
24+
}[target];
25+
26+
bench.start();
27+
for (let i = 0; i < n; i++) {
28+
fs.statSync(file);
29+
}
30+
bench.end(n);
31+
layer.unmount();
32+
}

lib/internal/vfs/setup.js

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ function registerVFS(vfs, name) {
118118
if (name !== undefined) {
119119
// What was loaded through the name came from the layer it linked to.
120120
if (activeNames.has(name)) {
121-
purgeLoaderCachesForPrefix(getVfsRoot() + sep + name);
121+
purgeLoaderCachesForPrefix(join(getVfsRoot(), name));
122122
}
123123
activeNames.set(name, vfs[kLayerId]);
124124
}
@@ -134,7 +134,7 @@ function deregisterVFS(vfs) {
134134
MapPrototypeForEach(activeNames, (target, name) => {
135135
if (target === layerId) {
136136
activeNames.delete(name);
137-
purgeLoaderCachesForPrefix(getVfsRoot() + sep + name);
137+
purgeLoaderCachesForPrefix(join(getVfsRoot(), name));
138138
}
139139
});
140140
purgeLoaderCachesForPrefix(vfs.mountPoint);
@@ -153,10 +153,14 @@ function deregisterVFS(vfs) {
153153
*
154154
* A mount name is a symbolic link to its layer, so a path that starts with
155155
* one is rewritten to start with the layer's mount point instead, and
156-
* `path` is what the layer must be handed. With `followLast` false, a path
157-
* that is the name itself is not followed, for the operations that act on
158-
* a link rather than on its target. `mountPoint` is the layer's mount point
159-
* as the input spells it, by name or by id.
156+
* `path` is what the layer must be handed. `followLink` is the difference
157+
* between stat() and lstat(): a name in the middle of a path is always
158+
* followed, but a path that is the name itself is only followed when
159+
* `followLink` is true. The operations that act on a link rather than on its
160+
* target (lstat, readlink, unlink, rename, ...) pass false, so that they see
161+
* the name as a symbolic link, not as the layer's root directory.
162+
* `mountPoint` is the layer's mount point as the input spells it, by name or
163+
* by id.
160164
*
161165
* A path under the root that no layer serves comes back with `vfs: null`
162166
* rather than as `null`, because the two cases must not be treated alike
@@ -170,15 +174,16 @@ function deregisterVFS(vfs) {
170174
* it rejects the empty device as an invalid package.json, so the loader
171175
* must answer for the whole root.
172176
* @param {string} inputPath
173-
* @param {boolean} [followLast]
177+
* @param {boolean} [followLink] Whether a path that is a mount name
178+
* resolves to its layer; false to act on the name's link itself.
174179
* @returns {{
175180
* vfs: object|null,
176181
* path: string,
177182
* normalized: string,
178183
* mountPoint: string|null,
179184
* }|null}
180185
*/
181-
function resolveVFS(inputPath, followLast = true) {
186+
function resolveVFS(inputPath, followLink = true) {
182187
const normalized = normalizeMountedPath(inputPath);
183188
if (normalized !== normalizedVfsRoot &&
184189
!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
@@ -192,9 +197,12 @@ function resolveVFS(inputPath, followLast = true) {
192197
if (vfs !== undefined && vfs.shouldHandleNormalized(normalized)) {
193198
return { vfs, path: inputPath, normalized, mountPoint: vfs.mountPoint };
194199
}
195-
} else if (followLast || end < normalized.length) {
200+
} else if (followLink || end < normalized.length) {
196201
const layerId = activeNames.get(segment);
197202
if (layerId !== undefined) {
203+
// This runs for every fs call through a name, so the paths are
204+
// concatenated rather than joined: the root is already normalized and
205+
// `segment` is a single segment, so there is nothing to normalize.
198206
const path = normalizedVfsRootPrefix + layerId +
199207
StringPrototypeSlice(normalized, end);
200208
return {
@@ -214,11 +222,11 @@ function resolveVFS(inputPath, followLast = true) {
214222
* fs functions see the root as a directory, so a path under the root that
215223
* no layer serves is the root file system's to answer.
216224
* @param {string} inputPath
217-
* @param {boolean} [followLast]
225+
* @param {boolean} [followLink] See `resolveVFS()`.
218226
* @returns {{ vfs: object, path: string }|null}
219227
*/
220-
function findVFS(inputPath, followLast) {
221-
const r = resolveVFS(inputPath, followLast);
228+
function findVFS(inputPath, followLink) {
229+
const r = resolveVFS(inputPath, followLink);
222230
if (r === null) return null;
223231
if (r.vfs === null) r.vfs = rootVFS;
224232
return r;
@@ -388,28 +396,28 @@ function vfsRead(path, syscall, fn) {
388396
return findVFSWith(pathStr, syscall, fn);
389397
}
390398

391-
function vfsOp(path, fn, followLast) {
399+
function vfsOp(path, fn, followLink) {
392400
const pathStr = toPathStr(path);
393401
if (pathStr !== null) {
394-
const r = findVFS(pathStr, followLast);
402+
const r = findVFS(pathStr, followLink);
395403
if (r !== null) return fn(r.vfs, r.path);
396404
}
397405
return undefined;
398406
}
399407

400-
function vfsOpVoid(path, fn, followLast) {
408+
function vfsOpVoid(path, fn, followLink) {
401409
const pathStr = toPathStr(path);
402410
if (pathStr !== null) {
403-
const r = findVFS(pathStr, followLast);
411+
const r = findVFS(pathStr, followLink);
404412
if (r !== null) { fn(r.vfs, r.path); return true; }
405413
}
406414
return undefined;
407415
}
408416

409417
// Returns the path to hand `srcVfs` for `destPath`, which must be served by
410418
// the same VFS as the source.
411-
function findSameVFS(srcPath, destPath, syscall, srcVfs, followLast) {
412-
const r = findVFS(destPath, followLast);
419+
function findSameVFS(srcPath, destPath, syscall, srcVfs, followLink) {
420+
const r = findVFS(destPath, followLink);
413421
if (r?.vfs !== srcVfs) {
414422
throw createEXDEV(syscall, srcPath);
415423
}

0 commit comments

Comments
 (0)