Skip to content

Commit 5e77bcd

Browse files
amcaseytargos
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 Backport-PR-URL: #36921 Reviewed-By: Denys Otrishko <shishugi@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent 443cace commit 5e77bcd

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
@@ -2560,6 +2560,9 @@ changes:
25602560
* `options` {Object}
25612561
* `bigint` {boolean} Whether the numeric values in the returned
25622562
[`fs.Stats`][] object should be `bigint`. **Default:** `false`.
2563+
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
2564+
if no file system entry exists, rather than returning `undefined`.
2565+
**Default:** `true`.
25632566
* Returns: {fs.Stats}
25642567

25652568
Synchronous lstat(2).
@@ -3765,6 +3768,9 @@ changes:
37653768
* `options` {Object}
37663769
* `bigint` {boolean} Whether the numeric values in the returned
37673770
[`fs.Stats`][] object should be `bigint`. **Default:** `false`.
3771+
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
3772+
if no file system entry exists, rather than returning `undefined`.
3773+
**Default:** `true`.
37683774
* Returns: {fs.Stats}
37693775

37703776
Synchronous stat(2).

lib/fs.js

+23-3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
ERR_INVALID_CALLBACK,
7272
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM
7373
},
74+
uvErrmapGet,
7475
uvException
7576
} = require('internal/errors');
7677

@@ -1061,28 +1062,47 @@ function stat(path, options = { bigint: false }, callback) {
10611062
binding.stat(pathModule.toNamespacedPath(path), options.bigint, req);
10621063
}
10631064

1064-
function fstatSync(fd, options = { bigint: false }) {
1065+
function hasNoEntryError(ctx) {
1066+
if (ctx.errno) {
1067+
const uvErr = uvErrmapGet(ctx.errno);
1068+
return uvErr && uvErr[0] === 'ENOENT';
1069+
}
1070+
1071+
if (ctx.error) {
1072+
return ctx.error.code === 'ENOENT';
1073+
}
1074+
1075+
return false;
1076+
}
1077+
1078+
function fstatSync(fd, options = { bigint: false, throwIfNoEntry: true }) {
10651079
validateInt32(fd, 'fd', 0);
10661080
const ctx = { fd };
10671081
const stats = binding.fstat(fd, options.bigint, undefined, ctx);
10681082
handleErrorFromBinding(ctx);
10691083
return getStatsFromBinding(stats);
10701084
}
10711085

1072-
function lstatSync(path, options = { bigint: false }) {
1086+
function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
10731087
path = getValidatedPath(path);
10741088
const ctx = { path };
10751089
const stats = binding.lstat(pathModule.toNamespacedPath(path),
10761090
options.bigint, undefined, ctx);
1091+
if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) {
1092+
return undefined;
1093+
}
10771094
handleErrorFromBinding(ctx);
10781095
return getStatsFromBinding(stats);
10791096
}
10801097

1081-
function statSync(path, options = { bigint: false }) {
1098+
function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
10821099
path = getValidatedPath(path);
10831100
const ctx = { path };
10841101
const stats = binding.stat(pathModule.toNamespacedPath(path),
10851102
options.bigint, undefined, ctx);
1103+
if (options.throwIfNoEntry === false && hasNoEntryError(ctx)) {
1104+
return undefined;
1105+
}
10861106
handleErrorFromBinding(ctx);
10871107
return getStatsFromBinding(stats);
10881108
}

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)