Skip to content

Commit 0c17f68

Browse files
committed
fs: copy directory trees for fs.cp() on the thread pool
fs.cp() and fs.promises.cp() walked the tree in JavaScript with several thread pool round trips per entry (opendir batches, two stat()s, the copyFile(), a chmod()), all awaited in sequence: a 2 100-file tree took ~215 ms with ~110 ms of that on the main thread, against ~36 ms for fs.cpSync(), which copies the tree in C++ when no filter is given. Factor that C++ walk into CopyDirRecursive(), which records the error instead of throwing so that it can run on any thread, and run it as one ThreadPoolWork request (CpDirJob) for fs.cp()/fs.promises.cp() when the destination directory does not exist yet and nothing has to run per entry (no filter, no dereference, permission model off). Copying into an existing tree keeps the JavaScript walk and its rules for what may already be there. For that job the walk also rejects sockets, FIFOs and unknown entries inside the tree as the JavaScript walk does (cpSync keeps skipping them). The same tree now takes ~28 ms with under 1 ms on the main thread. The walk now uses the error_code overloads of std::filesystem throughout (directory iteration included), so an unreadable directory inside the tree is reported as EACCES by both cp() and cpSync() instead of terminating the process, which cpSync() has done since the walk moved to C++. Errors raised inside the walk carry the same codes as before but are the plain Error objects cpSync produces rather than SystemErrors. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent b533509 commit 0c17f68

5 files changed

Lines changed: 529 additions & 181 deletions

File tree

‎benchmark/fs/bench-cp.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
// fs.promises.cp() of a directory tree.
4+
5+
const common = require('../common');
6+
const fs = require('fs');
7+
const path = require('path');
8+
const tmpdir = require('../../test/common/tmpdir');
9+
10+
const bench = common.createBenchmark(main, {
11+
files: [500],
12+
n: [3],
13+
});
14+
15+
function prepareSource(files) {
16+
const src = tmpdir.resolve('cp-src');
17+
for (let i = 0; i < files; i++) {
18+
const dir = path.join(src, `dir-${i % 10}`, `sub-${i % 7}`);
19+
fs.mkdirSync(dir, { recursive: true });
20+
fs.writeFileSync(path.join(dir, `file-${i}.js`), 'x'.repeat(1024 + (i % 512)));
21+
}
22+
return src;
23+
}
24+
25+
async function main({ files, n }) {
26+
tmpdir.refresh();
27+
const src = prepareSource(files);
28+
bench.start();
29+
for (let i = 0; i < n; i++) {
30+
await fs.promises.cp(src, tmpdir.resolve(`cp-dest-${i}`), { recursive: true });
31+
}
32+
bench.end(n);
33+
}

‎lib/internal/fs/cp/cp.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const {
66
ArrayPrototypeEvery,
77
ArrayPrototypeFilter,
88
Boolean,
9+
ErrorCaptureStackTrace,
10+
Promise,
911
PromisePrototypeThen,
1012
PromiseReject,
1113
SafePromiseAll,
@@ -55,6 +57,7 @@ const {
5557
sep,
5658
} = require('path');
5759
const fsBinding = internalBinding('fs');
60+
const permission = require('internal/process/permission');
5861

5962
async function cpFn(src, dest, opts) {
6063
// Warn about using preserveTimestamps on 32-bit node
@@ -315,11 +318,36 @@ async function onDir(srcStat, destStat, src, dest, opts) {
315318
}
316319

317320
async function mkDirAndCopy(srcMode, src, dest, opts) {
321+
// A destination directory that does not exist yet is filled in one thread
322+
// pool request by the walk fs.cpSync() uses, unless a filter has to run per
323+
// entry, links inside the tree must be dereferenced, or the permission model
324+
// has to check each path. Copying into an existing tree keeps the per-entry
325+
// walk below and its rules for what may already be there.
326+
if (!opts.filter && !opts.dereference && !permission.isEnabled()) {
327+
// Creates dest itself, with the mode of src.
328+
return copyDirNative(src, dest, opts);
329+
}
318330
await mkdir(dest);
319331
await copyDir(src, dest, opts);
320332
return setDestMode(dest, srcMode);
321333
}
322334

335+
function copyDirNative(src, dest, opts) {
336+
return new Promise((resolve, reject) => {
337+
const job = new fsBinding.CpDirJob(src, dest, opts.force, opts.dereference, opts.errorOnExist,
338+
opts.verbatimSymlinks, opts.preserveTimestamps);
339+
job.ondone = (err) => {
340+
if (err != null) {
341+
ErrorCaptureStackTrace(err, copyDirNative);
342+
reject(err);
343+
} else {
344+
resolve();
345+
}
346+
};
347+
job.run();
348+
});
349+
}
350+
323351
async function copyDir(src, dest, opts) {
324352
const dir = await opendir(src);
325353

0 commit comments

Comments
 (0)