From 15ff155c12cdc4aaae0e99146f82aebebfa66015 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 3 Jun 2020 01:18:58 +0000 Subject: [PATCH] lib: add throws option to fs.f/l/statSync For consumers that aren't interested in *why* a `statSync` call failed, allocating and throwing an exception is an unnecessary expense. This PR adds an option that will cause it to return `undefined` in such cases instead. As a motivating example, the JavaScript & TypeScript language service shared between Visual Studio and Visual Studio Code is stuck with synchronous file IO for architectural and backward-compatibility reasons. It frequently needs to speculatively check for the existence of files and directories that may not exist (and cares about file vs directory, so `existsSync` is insufficient), but ignores file system entries it can't access, regardless of the reason. Benchmarking the language service is difficult because it's so hard to get good coverage of both code bases and user behaviors, but, as a representative metric, we measured batch compilation of a few hundred popular projects (by star count) from GitHub and found that, on average, we saved about 1-2% of total compilation time. We speculate that the savings could be even more significant in interactive (language service or watch mode) scenarios, where the same (non-existent) files need to be polled over and over again. It's not a huge improvement, but it's a very small change and it will affect a lot of users (and CI runs). For reference, our measurements were against `v12.x` (3637a061a at the time) on an Ubuntu Server desktop with an SSD. PR-URL: https://github.com/nodejs/node/pull/33716 Reviewed-By: Denys Otrishko Reviewed-By: Joyee Cheung --- benchmark/fs/bench-statSync-failure.js | 28 ++++++++++++++++++++++++++ doc/api/fs.md | 6 ++++++ lib/fs.js | 26 +++++++++++++++++++++--- test/parallel/test-fs-stat-bigint.js | 27 +++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 benchmark/fs/bench-statSync-failure.js diff --git a/benchmark/fs/bench-statSync-failure.js b/benchmark/fs/bench-statSync-failure.js new file mode 100644 index 00000000000000..82cb24c09f4af2 --- /dev/null +++ b/benchmark/fs/bench-statSync-failure.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); + +const bench = common.createBenchmark(main, { + n: [1e6], + statSyncType: ['throw', 'noThrow'] +}); + + +function main({ n, statSyncType }) { + const arg = path.join(__dirname, 'non.existent'); + + bench.start(); + for (let i = 0; i < n; i++) { + if (statSyncType === 'noThrow') { + fs.statSync(arg, { throwIfNoEntry: false }); + } else { + try { + fs.statSync(arg); + } catch { + } + } + } + bench.end(n); +} diff --git a/doc/api/fs.md b/doc/api/fs.md index 0ee98e5646afff..991ecbc39107a7 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -2568,6 +2568,9 @@ changes: * `options` {Object} * `bigint` {boolean} Whether the numeric values in the returned [`fs.Stats`][] object should be `bigint`. **Default:** `false`. + * `throwIfNoEntry` {boolean} Whether an exception will be thrown + if no file system entry exists, rather than returning `undefined`. + **Default:** `true`. * Returns: {fs.Stats} Synchronous lstat(2). @@ -3810,6 +3813,9 @@ changes: * `options` {Object} * `bigint` {boolean} Whether the numeric values in the returned [`fs.Stats`][] object should be `bigint`. **Default:** `false`. + * `throwIfNoEntry` {boolean} Whether an exception will be thrown + if no file system entry exists, rather than returning `undefined`. + **Default:** `true`. * Returns: {fs.Stats} Synchronous stat(2). diff --git a/lib/fs.js b/lib/fs.js index 474b8c5d6c1d3e..cb9312c9d13fbe 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -72,6 +72,7 @@ const { ERR_FEATURE_UNAVAILABLE_ON_PLATFORM }, hideStackFrames, + uvErrmapGet, uvException } = require('internal/errors'); @@ -1076,7 +1077,20 @@ function stat(path, options = { bigint: false }, callback) { binding.stat(pathModule.toNamespacedPath(path), options.bigint, req); } -function fstatSync(fd, options = { bigint: false }) { +function hasNoEntryError(ctx) { + if (ctx.errno) { + const uvErr = uvErrmapGet(ctx.errno); + return uvErr && uvErr[0] === 'ENOENT'; + } + + if (ctx.error) { + return ctx.error.code === 'ENOENT'; + } + + return false; +} + +function fstatSync(fd, options = { bigint: false, throwIfNoEntry: true }) { validateInt32(fd, 'fd', 0); const ctx = { fd }; const stats = binding.fstat(fd, options.bigint, undefined, ctx); @@ -1084,20 +1098,26 @@ function fstatSync(fd, options = { bigint: false }) { return getStatsFromBinding(stats); } -function lstatSync(path, options = { bigint: false }) { +function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) { path = getValidatedPath(path); const ctx = { path }; const stats = binding.lstat(pathModule.toNamespacedPath(path), options.bigint, undefined, ctx); + if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) { + return undefined; + } handleErrorFromBinding(ctx); return getStatsFromBinding(stats); } -function statSync(path, options = { bigint: false }) { +function statSync(path, options = { bigint: false, throwIfNoEntry: true }) { path = getValidatedPath(path); const ctx = { path }; const stats = binding.stat(pathModule.toNamespacedPath(path), options.bigint, undefined, ctx); + if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) { + return undefined; + } handleErrorFromBinding(ctx); return getStatsFromBinding(stats); } diff --git a/test/parallel/test-fs-stat-bigint.js b/test/parallel/test-fs-stat-bigint.js index 6f1db6078eac6f..cffec39288de4a 100644 --- a/test/parallel/test-fs-stat-bigint.js +++ b/test/parallel/test-fs-stat-bigint.js @@ -122,6 +122,33 @@ if (!common.isWindows) { fs.closeSync(fd); } +{ + assert.throws( + () => fs.statSync('does_not_exist'), + { code: 'ENOENT' }); + assert.strictEqual( + fs.statSync('does_not_exist', { throwIfNoEntry: false }), + undefined); +} + +{ + assert.throws( + () => fs.lstatSync('does_not_exist'), + { code: 'ENOENT' }); + assert.strictEqual( + fs.lstatSync('does_not_exist', { throwIfNoEntry: false }), + undefined); +} + +{ + assert.throws( + () => fs.fstatSync(9999), + { code: 'EBADF' }); + assert.throws( + () => fs.fstatSync(9999, { throwIfNoEntry: false }), + { code: 'EBADF' }); +} + const runCallbackTest = (func, arg, done) => { const startTime = process.hrtime.bigint(); func(arg, { bigint: true }, common.mustCall((err, bigintStats) => {