mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
fs.readFile(path) took four libuv thread pool round trips for a typical small file -- open, fstat, read and close, each its own uv_fs request with its own queue wait, completion callback and JS/C++ crossing -- and fs.promises.readFile(path) did the same through a FileHandle. For the small files applications read most, the round trips are the cost, and each occupies a slot in the pool that concurrent dns.lookup(), fs and crypto work is also queueing for. Add ReadFileJob (an AsyncWrap + ThreadPoolWork) that performs open + fstat + read-to-EOF + close as one pool task and reports the whole content, or, when the file turns out to be larger than one chunk (kReadFileBufferLength, 512 KiB), stops after fstat() and hands the fd and size back so that the existing chunked reader continues unchanged (large reads stay interleaved and abortable between chunks, and still save the fstat round trip). fs.readFile() and fs.promises.readFile() use it for path arguments without a user buffer; file descriptors, FileHandles, options.buffer and an active VFS keep their paths. Behavior is otherwise kept: same bytes for every size and encoding; open failures report syscall 'open' with the path, read failures 'read'; permission errors are delivered through the callback/promise as before; an abort that arrives while the read is in flight still wins; the job is an FSREQCALLBACK resource for async_hooks; a handed back fd is tracked exactly like one from a plain open(). Tests that asserted the internal open/fstat/read/close request chain, used readFile() as a proxy for an fstat trace event, or injected faults through FileHandle.prototype for path-based reads are adjusted to keep testing what they test (a file just over one chunk where the chain shape matters, fs.fstat() for the fstat trace, a larger file so the FileHandle path is taken). fs.readFile() of 4 KiB files at concurrency 64 goes from ~51k to ~306k files per second, and a mixed stat/readFile/dns.lookup burst from ~66k to ~312k operations per second. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65327 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com>
57 lines
1.9 KiB
JavaScript
57 lines
1.9 KiB
JavaScript
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const tick = require('../common/tick');
|
|
const initHooks = require('./init-hooks');
|
|
const { checkInvocations } = require('./hook-checks');
|
|
const fs = require('fs');
|
|
const { isMainThread } = require('worker_threads');
|
|
|
|
if (!isMainThread) {
|
|
common.skip('Worker bootstrapping works differently -> different async IDs');
|
|
}
|
|
|
|
const hooks = initHooks();
|
|
|
|
hooks.enable();
|
|
fs.readFile(__filename, common.mustCall(onread));
|
|
|
|
function onread() {
|
|
// fs.readFile() of a small file is a single request (open + fstat + read +
|
|
// close in one thread pool round trip); larger files continue with one
|
|
// request per chunk. Either way each request is an FSREQCALLBACK triggered
|
|
// by the previous one (or by the top-level for the first).
|
|
const as = hooks.activitiesOfTypes('FSREQCALLBACK');
|
|
assert.ok(as.length >= 1);
|
|
let lastParent = 1;
|
|
for (let i = 0; i < as.length; i++) {
|
|
const a = as[i];
|
|
assert.strictEqual(a.type, 'FSREQCALLBACK');
|
|
assert.strictEqual(typeof a.uid, 'number');
|
|
assert.strictEqual(a.triggerAsyncId, lastParent);
|
|
lastParent = a.uid;
|
|
}
|
|
for (let i = 0; i < as.length - 1; i++) {
|
|
checkInvocations(as[i], { init: 1, before: 1, after: 1, destroy: 1 },
|
|
`reqwrap[${i}]: while in onread callback`);
|
|
}
|
|
|
|
// This callback is called from within the last fs req callback therefore
|
|
// the last req is still going and after/destroy haven't been called yet
|
|
checkInvocations(as[as.length - 1], { init: 1, before: 1 },
|
|
`reqwrap[${as.length - 1}]: while in onread callback`);
|
|
tick(2);
|
|
}
|
|
|
|
process.on('exit', onexit);
|
|
|
|
function onexit() {
|
|
hooks.disable();
|
|
hooks.sanityCheck('FSREQCALLBACK');
|
|
const as = hooks.activitiesOfTypes('FSREQCALLBACK');
|
|
const a = as.pop();
|
|
checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 },
|
|
'when process exits');
|
|
}
|