Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: buffer dir entries in opendir() #29893

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions benchmark/fs/bench-opendir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const common = require('../common');
const fs = require('fs');
const path = require('path');

const bench = common.createBenchmark(main, {
n: [100],
dir: [ 'lib', 'test/parallel'],
mode: [ 'async', 'sync', 'callback' ]
});

async function main({ n, dir, mode }) {
const fullPath = path.resolve(__dirname, '../../', dir);

bench.start();

let counter = 0;
for (let i = 0; i < n; i++) {
if (mode === 'async') {
// eslint-disable-next-line no-unused-vars
for await (const entry of await fs.promises.opendir(fullPath))
counter++;
} else if (mode === 'callback') {
const dir = await fs.promises.opendir(fullPath);
await new Promise((resolve, reject) => {
function read() {
dir.read((err, entry) => {
if (err)
reject(err);
if (entry === null)
resolve(dir.close());
else
read();
});
}

read();
});
} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax This patch seems wrong? There should be an else here... I think the benchmark you ran included readSync() along with the callback one...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes … funny the linter wouldn’t complain about this? The correct benchmark results for changing to setImmediate are

                                                                confidence improvement accuracy (*)    (**)   (***)
 fs/bench-opendir.js mode='callback' dir='lib' n=100                           3.40 %      ±15.95% ±21.22% ±27.62%
 fs/bench-opendir.js mode='callback' dir='test/parallel' n=100        ***    -43.16 %      ±10.70% ±14.27% ±18.66%

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This result seems strange to me, how is that worse than before?

Regardless though after further though I think that nextTick is the reasonable thing to do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fishrock123 I think it makes sense – as you noted, the readSync() part was run unconditionally before, so its (unchanged) performance was included in the -30 %. If we only benchmark what has been slowed down, seeing more negative impact seems about right to me.

const dir = fs.opendirSync(fullPath);
while (dir.readSync() !== null)
counter++;
dir.closeSync();
}
}

bench.end(counter);
}
24 changes: 16 additions & 8 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,10 @@ Asynchronously read the next directory entry via readdir(3) as an
After the read is completed, a `Promise` is returned that will be resolved with
an [`fs.Dirent`][], or `null` if there are no more directory entries to read.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._
Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms.
Entries added or removed while iterating over the directory may or may not be
included in the iteration results.

### dir.read(callback)
<!-- YAML
Expand All @@ -380,8 +382,10 @@ Asynchronously read the next directory entry via readdir(3) as an
After the read is completed, the `callback` will be called with an
[`fs.Dirent`][], or `null` if there are no more directory entries to read.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._
Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms.
Entries added or removed while iterating over the directory may or may not be
included in the iteration results.

### dir.readSync()
<!-- YAML
Expand All @@ -395,8 +399,10 @@ Synchronously read the next directory entry via readdir(3) as an

If there are no more directory entries to read, `null` will be returned.

_Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms._
Directory entries returned by this function are in no particular order as
provided by the operating system's underlying directory mechanisms.
Entries added or removed while iterating over the directory may or may not be
included in the iteration results.

### dir\[Symbol.asyncIterator\]()
<!-- YAML
Expand All @@ -413,8 +419,10 @@ The `null` case from `dir.read()` is handled internally.

See [`fs.Dir`][] for an example.

_Directory entries returned by this iterator are in no particular order as
provided by the operating system's underlying directory mechanisms._
Directory entries returned by this iterator are in no particular order as
provided by the operating system's underlying directory mechanisms.
Entries added or removed while iterating over the directory may or may not be
included in the iteration results.

## Class: fs.Dirent
<!-- YAML
Expand Down
27 changes: 26 additions & 1 deletion lib/internal/fs/dir.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,27 @@ const {

const kDirHandle = Symbol('kDirHandle');
const kDirPath = Symbol('kDirPath');
const kDirBufferedEntries = Symbol('kDirBufferedEntries');
const kDirClosed = Symbol('kDirClosed');
const kDirOptions = Symbol('kDirOptions');
const kDirReadImpl = Symbol('kDirReadImpl');
const kDirReadPromisified = Symbol('kDirReadPromisified');
const kDirClosePromisified = Symbol('kDirClosePromisified');

class Dir {
constructor(handle, path, options) {
if (handle == null) throw new ERR_MISSING_ARGS('handle');
this[kDirHandle] = handle;
this[kDirBufferedEntries] = [];
this[kDirPath] = path;
this[kDirClosed] = false;

this[kDirOptions] = getOptions(options, {
encoding: 'utf8'
});

this[kDirReadPromisified] = internalUtil.promisify(this.read).bind(this);
this[kDirReadPromisified] =
internalUtil.promisify(this[kDirReadImpl]).bind(this, false);
this[kDirClosePromisified] = internalUtil.promisify(this.close).bind(this);
}

Expand All @@ -49,6 +53,10 @@ class Dir {
}

read(callback) {
return this[kDirReadImpl](true, callback);
}

[kDirReadImpl](maybeSync, callback) {
if (this[kDirClosed] === true) {
throw new ERR_DIR_CLOSED();
}
Expand All @@ -59,11 +67,22 @@ class Dir {
throw new ERR_INVALID_CALLBACK(callback);
}

if (this[kDirBufferedEntries].length > 0) {
const [ name, type ] = this[kDirBufferedEntries].splice(0, 2);
if (maybeSync)
process.nextTick(getDirent, this[kDirPath], name, type, callback);
else
getDirent(this[kDirPath], name, type, callback);
return;
}

const req = new FSReqCallback();
req.oncomplete = (err, result) => {
if (err || result === null) {
return callback(err, result);
}

this[kDirBufferedEntries] = result.slice(2);
getDirent(this[kDirPath], result[0], result[1], callback);
};

Expand All @@ -78,6 +97,11 @@ class Dir {
throw new ERR_DIR_CLOSED();
}

if (this[kDirBufferedEntries].length > 0) {
const [ name, type ] = this[kDirBufferedEntries].splice(0, 2);
return getDirent(this[kDirPath], name, type);
}

const ctx = { path: this[kDirPath] };
const result = this[kDirHandle].read(
this[kDirOptions].encoding,
Expand All @@ -90,6 +114,7 @@ class Dir {
return result;
}

this[kDirBufferedEntries] = result.slice(2);
return getDirent(this[kDirPath], result[0], result[1]);
}

Expand Down
92 changes: 52 additions & 40 deletions src/node_dir.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ DirHandle::DirHandle(Environment* env, Local<Object> obj, uv_dir_t* dir)
dir_(dir) {
MakeWeak();

dir_->nentries = 1;
dir_->dirents = &dirent_;
dir_->nentries = arraysize(dirents_);
dir_->dirents = dirents_;
}

DirHandle* DirHandle::New(Environment* env, uv_dir_t* dir) {
Expand Down Expand Up @@ -160,7 +160,37 @@ void DirHandle::Close(const FunctionCallbackInfo<Value>& args) {
}
}

void AfterDirReadSingle(uv_fs_t* req) {
static MaybeLocal<Array> DirentListToArray(
Environment* env,
uv_dirent_t* ents,
int num,
enum encoding encoding,
Local<Value>* err_out) {
MaybeStackBuffer<Local<Value>, 96> entries(num * 3);

// Return an array of all read filenames.
int j = 0;
for (int i = 0; i < num; i++) {
Local<Value> filename;
Local<Value> error;
const size_t namelen = strlen(ents[i].name);
if (!StringBytes::Encode(env->isolate(),
ents[i].name,
namelen,
encoding,
&error).ToLocal(&filename)) {
*err_out = error;
return MaybeLocal<Array>();
}

entries[j++] = filename;
entries[j++] = Integer::New(env->isolate(), ents[i].type);
}

return Array::New(env->isolate(), entries.out(), j);
}

static void AfterDirRead(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);

Expand All @@ -170,7 +200,6 @@ void AfterDirReadSingle(uv_fs_t* req) {

Environment* env = req_wrap->env();
Isolate* isolate = env->isolate();
Local<Value> error;

if (req->result == 0) {
// Done
Expand All @@ -182,26 +211,17 @@ void AfterDirReadSingle(uv_fs_t* req) {
uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr);
req->ptr = nullptr;

// Single entries are returned without an array wrapper
const uv_dirent_t& ent = dir->dirents[0];

MaybeLocal<Value> filename =
StringBytes::Encode(isolate,
ent.name,
req_wrap->encoding(),
&error);
if (filename.IsEmpty())
Local<Value> error;
Local<Array> js_array;
if (!DirentListToArray(env,
dir->dirents,
req->result,
req_wrap->encoding(),
&error).ToLocal(&js_array)) {
return req_wrap->Reject(error);
}


Local<Array> result = Array::New(isolate, 2);
result->Set(env->context(),
0,
filename.ToLocalChecked()).FromJust();
result->Set(env->context(),
1,
Integer::New(isolate, ent.type)).FromJust();
req_wrap->Resolve(result);
req_wrap->Resolve(js_array);
}


Expand All @@ -217,10 +237,10 @@ void DirHandle::Read(const FunctionCallbackInfo<Value>& args) {
DirHandle* dir;
ASSIGN_OR_RETURN_UNWRAP(&dir, args.Holder());

FSReqBase* req_wrap_async = static_cast<FSReqBase*>(GetReqWrap(env, args[1]));
FSReqBase* req_wrap_async = GetReqWrap(env, args[1]);
if (req_wrap_async != nullptr) { // dir.read(encoding, req)
AsyncCall(env, req_wrap_async, args, "readdir", encoding,
AfterDirReadSingle, uv_fs_readdir, dir->dir());
AfterDirRead, uv_fs_readdir, dir->dir());
} else { // dir.read(encoding, undefined, ctx)
CHECK_EQ(argc, 3);
FSReqWrapSync req_wrap_sync;
Expand All @@ -240,28 +260,20 @@ void DirHandle::Read(const FunctionCallbackInfo<Value>& args) {
}

CHECK_GE(req_wrap_sync.req.result, 0);
const uv_dirent_t& ent = dir->dir()->dirents[0];

Local<Value> error;
MaybeLocal<Value> filename =
StringBytes::Encode(isolate,
ent.name,
encoding,
&error);
if (filename.IsEmpty()) {
Local<Array> js_array;
if (!DirentListToArray(env,
dir->dir()->dirents,
req_wrap_sync.req.result,
encoding,
&error).ToLocal(&js_array)) {
Local<Object> ctx = args[2].As<Object>();
ctx->Set(env->context(), env->error_string(), error).FromJust();
USE(ctx->Set(env->context(), env->error_string(), error));
return;
}

Local<Array> result = Array::New(isolate, 2);
result->Set(env->context(),
0,
filename.ToLocalChecked()).FromJust();
result->Set(env->context(),
1,
Integer::New(isolate, ent.type)).FromJust();
args.GetReturnValue().Set(result);
args.GetReturnValue().Set(js_array);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/node_dir.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class DirHandle : public AsyncWrap {
static void Close(const v8::FunctionCallbackInfo<Value>& args);

inline uv_dir_t* dir() { return dir_; }
AsyncWrap* GetAsyncWrap() { return this; }

void MemoryInfo(MemoryTracker* tracker) const override {
tracker->TrackFieldWithSize("dir", sizeof(*dir_));
Expand All @@ -46,7 +45,8 @@ class DirHandle : public AsyncWrap {
void GCClose();

uv_dir_t* dir_;
uv_dirent_t dirent_;
// Up to 32 directory entries are read through a single libuv call.
uv_dirent_t dirents_[32];
addaleax marked this conversation as resolved.
Show resolved Hide resolved
bool closing_ = false;
bool closed_ = false;
};
Expand Down
14 changes: 12 additions & 2 deletions test/parallel/test-fs-opendir.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,27 @@ const dirclosedError = {
// Check the opendir async version
fs.opendir(testDir, common.mustCall(function(err, dir) {
assert.ifError(err);
dir.read(common.mustCall(function(err, dirent) {
let sync = true;
dir.read(common.mustCall((err, dirent) => {
assert(!sync);
assert.ifError(err);

// Order is operating / file system dependent
assert(files.includes(dirent.name), `'files' should include ${dirent}`);
assertDirent(dirent);

dir.close(common.mustCall(function(err) {
let syncInner = true;
dir.read(common.mustCall((err, dirent) => {
assert(!syncInner);
assert.ifError(err);

dir.close(common.mustCall(function(err) {
assert.ifError(err);
}));
}));
syncInner = false;
}));
sync = false;
}));

// opendir() on file should throw ENOTDIR
Expand Down