Skip to content

Commit cce0cf5

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() where its behavior is the same as the JavaScript walk's: no filter function, no dereference, not errorOnExist without force, permission model off. For that job it 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. 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 cce0cf5

3 files changed

Lines changed: 418 additions & 167 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: 36 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,12 +318,45 @@ async function onDir(srcStat, destStat, src, dest, opts) {
315318
}
316319

317320
async function mkDirAndCopy(srcMode, src, dest, opts) {
321+
if (canCopyDirNatively(opts)) {
322+
// Creates dest with the mode of src itself.
323+
return copyDirNative(src, dest, opts);
324+
}
318325
await mkdir(dest);
319326
await copyDir(src, dest, opts);
320327
return setDestMode(dest, srcMode);
321328
}
322329

330+
// Without a filter there is no JavaScript to call per entry, so the tree is
331+
// copied in one thread pool request by the implementation fs.cpSync() uses.
332+
// The per-entry walk is kept where that implementation behaves differently:
333+
// it merges into existing nested directories (errorOnExist without force
334+
// rejects them here) and does not dereference symbolic links inside the tree.
335+
function canCopyDirNatively(opts) {
336+
return !opts.filter && !opts.dereference && (opts.force || !opts.errorOnExist) &&
337+
!permission.isEnabled();
338+
}
339+
340+
function copyDirNative(src, dest, opts) {
341+
return new Promise((resolve, reject) => {
342+
const job = new fsBinding.CpDirJob(src, dest, opts.force, opts.dereference, opts.errorOnExist,
343+
opts.verbatimSymlinks, opts.preserveTimestamps);
344+
job.ondone = (err) => {
345+
if (err != null) {
346+
ErrorCaptureStackTrace(err, copyDirNative);
347+
reject(err);
348+
} else {
349+
resolve();
350+
}
351+
};
352+
job.run();
353+
});
354+
}
355+
323356
async function copyDir(src, dest, opts) {
357+
if (canCopyDirNatively(opts)) {
358+
return copyDirNative(src, dest, opts);
359+
}
324360
const dir = await opendir(src);
325361

326362
for await (const { name } of dir) {

0 commit comments

Comments
 (0)