diff --git a/benchmark/vfs/bench-fs-dispatch.js b/benchmark/vfs/bench-fs-dispatch.js new file mode 100644 index 00000000000000..9ae681669f83ac --- /dev/null +++ b/benchmark/vfs/bench-fs-dispatch.js @@ -0,0 +1,73 @@ +'use strict'; + +// Measures the dispatch overhead the VFS layer adds to fs operations +// against real-filesystem paths (paths that no VFS claims). The hot +// path in lib/fs.js is: +// +// const h = vfsState.handlers; +// if (h !== null) { const r = h.opSync(path, ...); if (r !== undefined) return r; } +// binding.op(getValidatedPath(path)); +// +// With layers=0 the VFS module is never required and `h === null` is +// the first thing fs sees. With layers>=1 the handler normalizes the +// path once and rejects it with a single prefix comparison against the +// reserved VFS namespace - the number of mounted layers should not +// matter. The benchmark mounts N VFSes and probes a real file under +// __dirname, so every call falls through after the namespace check +// declines the path. + +const common = require('../common.js'); +const fs = require('fs'); +const path = require('path'); + +const bench = common.createBenchmark(main, { + n: [3e5], + op: ['statSync', 'existsSync', 'accessSync', 'readFileSync'], + // 0 = VFS module never loaded (true baseline) + // >=1 = that many VFS instances mounted + layers: [0, 1, 2, 5, 10], +}, { + flags: ['--experimental-vfs', '--no-warnings'], +}); + +function mountLayers(count) { + const vfs = require('node:vfs'); + const handles = []; + for (let i = 0; i < count; i++) { + const v = vfs.create(); + v.mount('/bench'); + handles.push(v); + } + return handles; +} + +function main({ n, op, layers }) { + const handles = layers > 0 ? mountLayers(layers) : null; + + const target = layers === 0 ? __filename : path.join(__dirname, path.basename(__filename)); + + // Warm-up - get the JIT past the first-call icache + IC misses so we + // measure steady-state dispatch cost, not first-call resolution. + for (let i = 0; i < 1000; i++) { + if (op === 'statSync') fs.statSync(target); + else if (op === 'existsSync') fs.existsSync(target); + else if (op === 'accessSync') fs.accessSync(target); + else fs.readFileSync(target); + } + + bench.start(); + if (op === 'statSync') { + for (let i = 0; i < n; i++) fs.statSync(target); + } else if (op === 'existsSync') { + for (let i = 0; i < n; i++) fs.existsSync(target); + } else if (op === 'accessSync') { + for (let i = 0; i < n; i++) fs.accessSync(target); + } else { + for (let i = 0; i < n; i++) fs.readFileSync(target); + } + bench.end(n); + + if (handles) { + for (const v of handles) v.unmount(); + } +} diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 90b8e9c303125a..a38ccd73da902e 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -61,6 +61,11 @@ callback-based, and promise-based file system methods that mirror the shape of the [`node:fs`][] API. All paths are POSIX-style and absolute (starting with `/`). +By default, the file tree is private to the VFS instance. To expose +it through the global `node:fs` module, `require()`, and `import`, +call [`vfs.mount(prefix)`][]; call [`vfs.unmount()`][] (or rely on a +`using` declaration) to detach again. + ## `vfs.create([provider][, options])` + +* `prefix` {string} A logical name for the mount inside the reserved + VFS namespace. Interpreted as a relative path; leading separators + are ignored. **Default:** `'/'`. +* Returns: {string} The absolute mount point. + +Mounts the virtual file system and returns the resulting mount point. +After mounting, files in the VFS can be accessed through the +`node:fs` module — and resolved through `require()` and `import` — +using paths under the returned mount point. + +Mount points always live inside a reserved namespace, +`${os.devNull}/vfs/layer-/`. Because [`os.devNull`][] is a +character device (POSIX) or a device-namespace path (Windows) that +cannot have child file system entries, no real file-system path can +exist under this namespace: virtual paths never conflate with (or +shadow) real paths, and the layer that owns a path is visible in the +path itself. The `prefix` argument is a purely logical name inside +the namespace — it is never resolved against the working directory, +and a prefix that attempts to escape the namespace (for example with +`..` segments) throws `ERR_INVALID_ARG_VALUE`. + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/data.txt', 'Hello'); +const mountPoint = myVfs.mount('/virtual'); +// e.g. '/dev/null/vfs/layer-0/virtual' + +fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' +``` + +Each `VirtualFileSystem` instance may be mounted at most once at a +time. Attempting to mount an already-mounted instance throws +`ERR_INVALID_STATE`. Because each instance mounts inside its own +`layer-` namespace, mounts from different instances can +never overlap, even when they use the same `prefix`. + +The VFS supports the [Explicit Resource Management][] proposal. Use +a `using` declaration to unmount automatically when leaving scope: + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +let mountPoint; +{ + using myVfs = vfs.create(); + myVfs.writeFileSync('/data.txt', 'Hello'); + mountPoint = myVfs.mount('/virtual'); + + fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' +} // VFS is automatically unmounted here + +fs.existsSync(`${mountPoint}/data.txt`); // false +``` + +### `vfs.unmount()` + + + +Unmounts the virtual file system. After unmounting, virtual files +are no longer reachable through `node:fs`, `require()`, or `import`. +The same instance may be mounted again, at the same or a different +prefix, by calling `mount()`. + +This method is idempotent: calling `unmount()` on a VFS that is not +currently mounted has no effect. + +### `vfs.mounted` + + + +* {boolean} + +`true` while the VFS is mounted; `false` otherwise. + +### `vfs.mountPoint` + + + +* {string | null} + +The current mount point as an absolute string (the value returned by +the last [`vfs.mount(prefix)`][] call), or `null` when the VFS is not +mounted. + +### `vfs.layerId` + + + +* {number} + +A per-process monotonically increasing identifier assigned at +construction. The id is stable across `mount()` / `unmount()` cycles +for the lifetime of the instance, and is independent of the order in +which VFS layers are mounted. + +The layer id forms the `layer-` segment of the reserved mount +namespace, so every path served by this instance carries the id, and +it appears in the `NODE_DEBUG=vfs` output for `register` and +`deregister` events. + +```cjs +const vfs = require('node:vfs'); + +const a = vfs.create(); +const b = vfs.create(); +console.log(a.layerId); // e.g. 0 +console.log(b.layerId); // a.layerId + 1 +``` + ### `vfs.provider`