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: readFile function adds the chunkSize option #41647

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
11 changes: 11 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3313,6 +3313,10 @@ If `options.withFileTypes` is set to `true`, the `files` array will contain
<!-- YAML
added: v0.1.29
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41647
description: Add the `chunkSize` option that can Set `kReadFileBufferLength`
manually.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -3351,6 +3355,8 @@ changes:

* `path` {string|Buffer|URL|integer} filename or file descriptor
* `options` {Object|string}
* `chunkSize` {integer} The number of bytes per read. Use `-1` for no limit.
**Default:** `512 * 1024`
* `encoding` {string|null} **Default:** `null`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
* `signal` {AbortSignal} allows aborting an in-progress readFile
Expand Down Expand Up @@ -3446,6 +3452,11 @@ for instance) and Node.js is unable to determine an actual file size, each read
operation will load on 64 KB of data. For regular files, each read will process
512 KB of data.

Use up to 512kb per read otherwise to partition reading big files to prevent
blocking other threads in case the available threads are all in use. If you use
`options.chunkSize` make sure the value is equal to the nth power of 2 for best
performance.

For applications that require as-fast-as-possible reading of file contents, it
is better to use `fs.read()` directly and for application code to manage
reading the full contents of the file itself.
Expand Down
7 changes: 5 additions & 2 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ function checkAborted(signal, callback) {
* encoding?: string | null;
* flag?: string;
* signal?: AbortSignal;
* chunkSize?: number;
* } | string} [options]
* @param {(
* err?: Error,
Expand All @@ -371,8 +372,10 @@ function checkAborted(signal, callback) {
*/
function readFile(path, options, callback) {
callback = maybeCallback(callback || options);
options = getOptions(options, { flag: 'r' });
const context = new ReadFileContext(callback, options.encoding);
options = getOptions(options, { flag: 'r', chunkSize: 0 });
const context = new ReadFileContext(callback,
options.encoding,
options.chunkSize);
context.isUserFd = isFd(path); // File descriptor ownership

if (options.signal) {
Expand Down
12 changes: 9 additions & 3 deletions lib/internal/fs/read_file_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const {
ArrayPrototypePush,
MathMin,
ReflectApply,
} = primordials;

Expand Down Expand Up @@ -69,7 +68,7 @@ function readFileAfterClose(err) {
}

class ReadFileContext {
constructor(callback, encoding) {
constructor(callback, encoding, chunkSize) {
this.fd = undefined;
this.isUserFd = undefined;
this.size = 0;
Expand All @@ -80,6 +79,7 @@ class ReadFileContext {
this.encoding = encoding;
this.err = null;
this.signal = undefined;
this.chunkSize = chunkSize;
}

read() {
Expand All @@ -99,7 +99,13 @@ class ReadFileContext {
} else {
buffer = this.buffer;
offset = this.pos;
length = MathMin(kReadFileBufferLength, this.size - this.pos);
length = this.size - this.pos;
if ((this.chunkSize === 0 || this.chunkSize === undefined) &&
length > kReadFileBufferLength) {
length = kReadFileBufferLength;
} else if (this.chunkSize > 0 && length > this.chunkSize) {
length = this.chunkSize;
}
}

const req = new FSReqCallback();
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ function getOptions(options, defaultOptions) {
if (options.signal !== undefined) {
validateAbortSignal(options.signal, 'options.signal');
}

if (options.chunkSize !== undefined && options.chunkSize !== -1) {
validateUint32(options.chunkSize, 'options.chunkSize');
}

return options;
}

Expand Down
29 changes: 24 additions & 5 deletions test/parallel/test-fs-readfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ tmpdir.refresh();

const fileInfo = [
{ name: path.join(tmpdir.path, `${prefix}-1K.txt`),
len: 1024 },
len: 1024, chunkSize: 2048 },
{ name: path.join(tmpdir.path, `${prefix}-64K.txt`),
len: 64 * 1024 },
len: 64 * 1024, chunkSize: 1024 },
{ name: path.join(tmpdir.path, `${prefix}-64KLessOne.txt`),
len: (64 * 1024) - 1 },
{ name: path.join(tmpdir.path, `${prefix}-1M.txt`),
len: 1 * 1024 * 1024 },
len: 1 * 1024 * 1024, chunkSize: 0 },
{ name: path.join(tmpdir.path, `${prefix}-1MPlusOne.txt`),
len: (1 * 1024 * 1024) + 1 },
len: (1 * 1024 * 1024) + 1, chunkSize: -1 },
];

// Populate each fileInfo (and file) with unique fill.
Expand All @@ -46,7 +46,7 @@ for (const e of fileInfo) {

// Test readFile on each size.
for (const e of fileInfo) {
fs.readFile(e.name, common.mustCall((err, buf) => {
fs.readFile(e.name, { chunkSize: e.chunkSize }, common.mustCall((err, buf) => {
console.log(`Validating readFile on file ${e.name} of length ${e.len}`);
assert.ifError(err);
assert.deepStrictEqual(buf, e.contents);
Expand Down Expand Up @@ -93,3 +93,22 @@ for (const e of fileInfo) {
fs.readFile(fileInfo[0].name, { signal: 'hello' }, callback);
}, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' });
}
{
// Test chunkSize option
[Symbol(), 1n].forEach((chunkSize) => {
assert.throws(
() => fs.readFile(fileInfo[0].name, { chunkSize }, common.mustNotCall()),
{ name: 'TypeError' });
});
['', () => {}, true, false, [], {}].forEach((chunkSize) => {
assert.throws(
() => fs.readFile(fileInfo[0].name, { chunkSize }, common.mustNotCall()),
{ code: 'ERR_INVALID_ARG_TYPE' });
});
[-2, 2.5, -Infinity, Infinity, NaN].forEach((chunkSize) => {
assert.throws(
() => fs.readFile(fileInfo[0].name, { chunkSize }, common.mustNotCall()),
{ code: 'ERR_OUT_OF_RANGE' }
);
});
}