mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +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>
99 lines
2.9 KiB
JavaScript
99 lines
2.9 KiB
JavaScript
// Flags: --expose-internals
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
|
|
const assert = require('assert');
|
|
const { open, writeFile, readFile } = require('fs').promises;
|
|
const tmpdir = require('../common/tmpdir');
|
|
const { internalBinding } = require('internal/test/binding');
|
|
const fsBinding = internalBinding('fs');
|
|
tmpdir.refresh();
|
|
|
|
const fn = tmpdir.resolve('large-file');
|
|
|
|
// Creating large buffer with random content
|
|
const largeBuffer = Buffer.from(
|
|
Array.from({ length: 1024 ** 2 + 19 }, (_, index) => index)
|
|
);
|
|
|
|
async function createLargeFile() {
|
|
// Writing buffer to a file then try to read it
|
|
await writeFile(fn, largeBuffer);
|
|
}
|
|
|
|
async function validateReadFile() {
|
|
const readBuffer = await readFile(fn);
|
|
assert.strictEqual(readBuffer.equals(largeBuffer), true);
|
|
}
|
|
|
|
async function validateReadFileProc() {
|
|
// Test to make sure reading a file under the /proc directory works. Adapted
|
|
// from test-fs-read-file-sync-hostname.js.
|
|
// Refs:
|
|
// - https://groups.google.com/forum/#!topic/nodejs-dev/rxZ_RoH1Gn0
|
|
// - https://github.com/nodejs/node/issues/21331
|
|
|
|
// Test is Linux-specific.
|
|
if (!common.isLinux)
|
|
return;
|
|
|
|
const hostname = await readFile('/proc/sys/kernel/hostname');
|
|
assert.ok(hostname.length > 0);
|
|
}
|
|
|
|
function validateReadFileAbortLogicBefore() {
|
|
const signal = AbortSignal.abort();
|
|
assert.rejects(readFile(fn, { signal }), {
|
|
name: 'AbortError'
|
|
}).then(common.mustCall());
|
|
}
|
|
|
|
function validateReadFileAbortLogicDuring() {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
process.nextTick(() => controller.abort());
|
|
assert.rejects(readFile(fn, { signal }), {
|
|
name: 'AbortError'
|
|
}).then(common.mustCall());
|
|
}
|
|
|
|
async function validateWrongSignalParam() {
|
|
// Verify that if something different than Abortcontroller.signal
|
|
// is passed, ERR_INVALID_ARG_TYPE is thrown
|
|
|
|
await assert.rejects(async () => {
|
|
const callback = common.mustNotCall();
|
|
await readFile(fn, { signal: 'hello' }, callback);
|
|
}, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' });
|
|
|
|
}
|
|
|
|
async function validateZeroByteLiar() {
|
|
// readFile(path) sizes and reads small files natively (a lying size is
|
|
// handled there, see validateReadFileProc); the JS chunked reader that
|
|
// FileHandles use must cope with a file that claims size 0 as well.
|
|
const originalFStat = fsBinding.fstat;
|
|
fsBinding.fstat = common.mustCall(
|
|
async () => (/* stat fields */ [0, 1, 2, 3, 4, 5, 6, 7, 0 /* size */])
|
|
);
|
|
const fh = await open(fn);
|
|
try {
|
|
const readBuffer = await readFile(fh);
|
|
assert.strictEqual(readBuffer.toString(), largeBuffer.toString());
|
|
} finally {
|
|
fsBinding.fstat = originalFStat;
|
|
await fh.close();
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
await createLargeFile();
|
|
await validateReadFile();
|
|
await validateReadFileProc();
|
|
await validateReadFileAbortLogicBefore();
|
|
await validateReadFileAbortLogicDuring();
|
|
await validateWrongSignalParam();
|
|
await validateZeroByteLiar();
|
|
})().then(common.mustCall());
|