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 1 commit
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
Next Next commit
fs: buffer dir entries in opendir()
Read up to 32 directory entries in one batch when `dir.readSync()`
or `dir.read()` are called.

This increases performance significantly, although it introduces
quite a bit of edge case complexity.

                                                                 confidence improvement accuracy (*)    (**)    (***)
     fs/bench-opendir.js mode='async' dir='lib' n=100                  ***    155.93 %      ±30.05% ±40.34%  ±53.21%
     fs/bench-opendir.js mode='async' dir='test/parallel' n=100        ***    479.65 %      ±56.81% ±76.47% ±101.32%
     fs/bench-opendir.js mode='sync' dir='lib' n=100                           10.38 %      ±14.39% ±19.16%  ±24.96%
     fs/bench-opendir.js mode='sync' dir='test/parallel' n=100         ***     63.13 %      ±12.84% ±17.18%  ±22.58%
  • Loading branch information
addaleax committed Oct 10, 2019
commit 8bf527eda5f4b611b33e336e23325b28af5aad6b
35 changes: 35 additions & 0 deletions benchmark/fs/bench-opendir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'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' ]
});

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

(async () => {
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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 {
const dir = fs.opendirSync(fullPath);
while (dir.readSync() !== null)
counter++;
dir.closeSync();
}
}

bench.end(counter);
})();
}
16 changes: 16 additions & 0 deletions lib/internal/fs/dir.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {

const kDirHandle = Symbol('kDirHandle');
const kDirPath = Symbol('kDirPath');
const kDirBufferedEntries = Symbol('kDirBufferedEntries');
const kDirClosed = Symbol('kDirClosed');
const kDirOptions = Symbol('kDirOptions');
const kDirReadPromisified = Symbol('kDirReadPromisified');
Expand All @@ -33,6 +34,7 @@ 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;

Expand All @@ -59,11 +61,19 @@ class Dir {
throw new ERR_INVALID_CALLBACK(callback);
}

if (this[kDirBufferedEntries].length > 0) {
const [ name, type ] = this[kDirBufferedEntries].splice(0, 2);
getDirent(this[kDirPath], name, type, callback);
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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 +88,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 +105,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