Skip to content

Commit 15ff155

Browse files
amcaseycodebytere
authored andcommitted
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` (3637a06 at the time) on an Ubuntu Server desktop with an SSD. PR-URL: #33716 Reviewed-By: Denys Otrishko <shishugi@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent e48ec70 commit 15ff155

File tree

4 files changed

+84
-3
lines changed

4 files changed

+84
-3
lines changed
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fs = require('fs');
5+
const path = require('path');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e6],
9+
statSyncType: ['throw', 'noThrow']
10+
});
11+
12+
13+
function main({ n, statSyncType }) {
14+
const arg = path.join(__dirname, 'non.existent');
15+
16+
bench.start();
17+
for (let i = 0; i < n; i++) {
18+
if (statSyncType === 'noThrow') {
19+
fs.statSync(arg, { throwIfNoEntry: false });
20+
} else {
21+
try {
22+
fs.statSync(arg);
23+
} catch {
24+
}
25+
}
26+
}
27+
bench.end(n);
28+
}

doc/api/fs.md

+6
Original file line numberDiff line numberDiff line change
@@ -2568,6 +2568,9 @@ changes:
25682568
* `options` {Object}
25692569
* `bigint` {boolean} Whether the numeric values in the returned
25702570
[`fs.Stats`][] object should be `bigint`. **Default:** `false`.
2571+
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
2572+
if no file system entry exists, rather than returning `undefined`.
2573+
**Default:** `true`.
25712574
* Returns: {fs.Stats}
25722575

25732576
Synchronous lstat(2).
@@ -3810,6 +3813,9 @@ changes:
38103813
* `options` {Object}
38113814
* `bigint` {boolean} Whether the numeric values in the returned
38123815
[`fs.Stats`][] object should be `bigint`. **Default:** `false`.
3816+
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
3817+
if no file system entry exists, rather than returning `undefined`.
3818+
**Default:** `true`.
38133819
* Returns: {fs.Stats}
38143820

38153821
Synchronous stat(2).

lib/fs.js

+23-3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const {
7272
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM
7373
},
7474
hideStackFrames,
75+
uvErrmapGet,
7576
uvException
7677
} = require('internal/errors');
7778

@@ -1076,28 +1077,47 @@ function stat(path, options = { bigint: false }, callback) {
10761077
binding.stat(pathModule.toNamespacedPath(path), options.bigint, req);
10771078
}
10781079

1079-
function fstatSync(fd, options = { bigint: false }) {
1080+
function hasNoEntryError(ctx) {
1081+
if (ctx.errno) {
1082+
const uvErr = uvErrmapGet(ctx.errno);
1083+
return uvErr && uvErr[0] === 'ENOENT';
1084+
}
1085+
1086+
if (ctx.error) {
1087+
return ctx.error.code === 'ENOENT';
1088+
}
1089+
1090+
return false;
1091+
}
1092+
1093+
function fstatSync(fd, options = { bigint: false, throwIfNoEntry: true }) {
10801094
validateInt32(fd, 'fd', 0);
10811095
const ctx = { fd };
10821096
const stats = binding.fstat(fd, options.bigint, undefined, ctx);
10831097
handleErrorFromBinding(ctx);
10841098
return getStatsFromBinding(stats);
10851099
}
10861100

1087-
function lstatSync(path, options = { bigint: false }) {
1101+
function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
10881102
path = getValidatedPath(path);
10891103
const ctx = { path };
10901104
const stats = binding.lstat(pathModule.toNamespacedPath(path),
10911105
options.bigint, undefined, ctx);
1106+
if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) {
1107+
return undefined;
1108+
}
10921109
handleErrorFromBinding(ctx);
10931110
return getStatsFromBinding(stats);
10941111
}
10951112

1096-
function statSync(path, options = { bigint: false }) {
1113+
function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
10971114
path = getValidatedPath(path);
10981115
const ctx = { path };
10991116
const stats = binding.stat(pathModule.toNamespacedPath(path),
11001117
options.bigint, undefined, ctx);
1118+
if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) {
1119+
return undefined;
1120+
}
11011121
handleErrorFromBinding(ctx);
11021122
return getStatsFromBinding(stats);
11031123
}

test/parallel/test-fs-stat-bigint.js

+27
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,33 @@ if (!common.isWindows) {
122122
fs.closeSync(fd);
123123
}
124124

125+
{
126+
assert.throws(
127+
() => fs.statSync('does_not_exist'),
128+
{ code: 'ENOENT' });
129+
assert.strictEqual(
130+
fs.statSync('does_not_exist', { throwIfNoEntry: false }),
131+
undefined);
132+
}
133+
134+
{
135+
assert.throws(
136+
() => fs.lstatSync('does_not_exist'),
137+
{ code: 'ENOENT' });
138+
assert.strictEqual(
139+
fs.lstatSync('does_not_exist', { throwIfNoEntry: false }),
140+
undefined);
141+
}
142+
143+
{
144+
assert.throws(
145+
() => fs.fstatSync(9999),
146+
{ code: 'EBADF' });
147+
assert.throws(
148+
() => fs.fstatSync(9999, { throwIfNoEntry: false }),
149+
{ code: 'EBADF' });
150+
}
151+
125152
const runCallbackTest = (func, arg, done) => {
126153
const startTime = process.hrtime.bigint();
127154
func(arg, { bigint: true }, common.mustCall((err, bigintStats) => {

0 commit comments

Comments
 (0)