GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||
GeezerVison is showing github.com — fs: read small files in one thread pool round trip by codebytere · Pull Request #65327 · nodejs/node Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@ ## main #65327 +/- ## ========================================== - Coverage 90.13% 90.10% -0.04% ========================================== Files 752 752 Lines 251568 251915 +347 Branches 47270 47353 +83 ========================================== + Hits 226759 226976 +217 - Misses 16168 16266 +98 - Partials 8641 8673 +32
🚀 New features to boost your workflow:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Test failure on AIX needs to be looked at. Also, are these reads actually abortable in any way?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
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 <[email protected]>
21db0ab to
b626e93
Compare
|
@jasnell re aborts: the single round trip itself isn't interruptible once it's on the pool, same as an individual read req now; an already-aborted signal never schedules it, an abort that lands while it's in flight wins when it completes, and anything over one chunk hands the fd back to the existing chunked reader, so those stay abortable between chunks exactly as before. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Landed in 542e2b2 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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 <[email protected]> PR-URL: #65327 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]>
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 <[email protected]> PR-URL: #65327 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]>
Node 26.8.0 rewrote fs.readFile (nodejs/node#65327) to do open, fstat, read and close in a single thread pool round trip, so binding.open is no longer called from JS. mock-fs recovers the ReadFileContext prototype by intercepting binding.open during a dummy readFile, so it now gets undefined and throws at require time: TypeError: Cannot read properties of undefined (reading 'read') at exports.patchReadFileContext (mock-fs/lib/readfilecontext.js:40:30) Because it throws on require rather than in a test, it takes out config_test.ts and file_auth_test.ts in full. The matrix entries are floating majors, so setup-node resolves '26' to whatever the newest 26.x is at run time. That is why main went red on the merge of kubernetes-client#3022 without any change to the code under test: the branch last ran CI on 26.7.0, and by the time it merged five days later the runner had picked up 26.8.1. Pinning to 26.7 restores a green build. It is a stopgap: mock-fs has had no functional release since February 2025 and the upstream report (tschaub/mock-fs#447) is unanswered, so the durable fix is to stop depending on it.
fs.writeFile(path, data) took three libuv thread pool round trips (open, write, close), each its own request with its own queue wait, completion callback and JS/C++ crossing, and fs.promises.writeFile() did the same through a FileHandle. For the small files applications write most, the round trips are the cost, and each occupies a pool slot that concurrent fs, dns.lookup() and crypto work is also queueing for. Add WriteFileJob next to ReadFileJob: an AsyncWrap + ThreadPoolWork that opens, writes the whole buffer (looping on short writes) and closes as one pool task, keeping the buffer alive until it is done. fs.writeFile() uses it for path arguments without flush; fs.promises.writeFile() additionally keeps data above one write chunk (and iterables) on the FileHandle path, so large writes stay abortable between chunks as before. File descriptors, FileHandles, flush: true and an active VFS keep their existing paths. Behavior is otherwise kept: open failures report syscall 'open' with the path, write failures 'write'; permission errors are delivered through the callback/promise; an abort signalled while the write is in flight is still reported as an AbortError; the job is an FSREQCALLBACK resource for async_hooks and emits the 'write' fs trace event. Tests that used fs.writeFile() as a proxy for open/close trace events, or injected FileHandle faults for path-based writes, are adjusted to keep testing what they test. The job holds the buffer's backing store, so the memory stays valid if the buffer is detached or collected before the write finishes; a resizable ArrayBuffer could still have its pages decommitted by a shrink, so its contents are copied when the job is created. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65489 Refs: #65327 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
fs.writeFile(path, data) took three libuv thread pool round trips (open, write, close), each its own request with its own queue wait, completion callback and JS/C++ crossing, and fs.promises.writeFile() did the same through a FileHandle. For the small files applications write most, the round trips are the cost, and each occupies a pool slot that concurrent fs, dns.lookup() and crypto work is also queueing for. Add WriteFileJob next to ReadFileJob: an AsyncWrap + ThreadPoolWork that opens, writes the whole buffer (looping on short writes) and closes as one pool task, keeping the buffer alive until it is done. fs.writeFile() uses it for path arguments without flush; fs.promises.writeFile() additionally keeps data above one write chunk (and iterables) on the FileHandle path, so large writes stay abortable between chunks as before. File descriptors, FileHandles, flush: true and an active VFS keep their existing paths. Behavior is otherwise kept: open failures report syscall 'open' with the path, write failures 'write'; permission errors are delivered through the callback/promise; an abort signalled while the write is in flight is still reported as an AbortError; the job is an FSREQCALLBACK resource for async_hooks and emits the 'write' fs trace event. Tests that used fs.writeFile() as a proxy for open/close trace events, or injected FileHandle faults for path-based writes, are adjusted to keep testing what they test. The job holds the buffer's backing store, so the memory stays valid if the buffer is detached or collected before the write finishes; a resizable ArrayBuffer could still have its pages decommitted by a shrink, so its contents are copied when the job is created. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65489 Refs: #65327 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
fs.writeFile(path, data) took three libuv thread pool round trips (open, write, close), each its own request with its own queue wait, completion callback and JS/C++ crossing, and fs.promises.writeFile() did the same through a FileHandle. For the small files applications write most, the round trips are the cost, and each occupies a pool slot that concurrent fs, dns.lookup() and crypto work is also queueing for. Add WriteFileJob next to ReadFileJob: an AsyncWrap + ThreadPoolWork that opens, writes the whole buffer (looping on short writes) and closes as one pool task, keeping the buffer alive until it is done. fs.writeFile() uses it for path arguments without flush; fs.promises.writeFile() additionally keeps data above one write chunk (and iterables) on the FileHandle path, so large writes stay abortable between chunks as before. File descriptors, FileHandles, flush: true and an active VFS keep their existing paths. Behavior is otherwise kept: open failures report syscall 'open' with the path, write failures 'write'; permission errors are delivered through the callback/promise; an abort signalled while the write is in flight is still reported as an AbortError; the job is an FSREQCALLBACK resource for async_hooks and emits the 'write' fs trace event. Tests that used fs.writeFile() as a proxy for open/close trace events, or injected FileHandle faults for path-based writes, are adjusted to keep testing what they test. The job holds the buffer's backing store, so the memory stays valid if the buffer is detached or collected before the write finishes; a resizable ArrayBuffer could still have its pages decommitted by a shrink, so its contents are copied when the job is created. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65489 Refs: #65327 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
mock-fs 5.5.0 throws at require time on Node 26.8+ because nodejs/node#65327 rewrote fs.readFile and the prototype sniffing in mock-fs no longer finds ReadFileContext (tschaub/mock-fs#447, unfixed). The config tests now write .sizewatcher.yml into a fresh tmp directory and chdir into it, matching the pattern already used in cli.test.js. This removes the mock-fs devDependency entirely. Co-Authored-By: Claude Fable 5.1 <[email protected]>
mock-fs 5.5.0 throws at require time on Node 26.8+ because nodejs/node#65327 rewrote fs.readFile and the prototype sniffing in mock-fs no longer finds ReadFileContext (tschaub/mock-fs#447, unfixed). The config tests now write .sizewatcher.yml into a fresh tmp directory and chdir into it, matching the pattern already used in cli.test.js. This removes the mock-fs devDependency entirely. Co-Authored-By: Claude Fable 5.1 <[email protected]>
fs.writeFile(path, data) took three libuv thread pool round trips (open, write, close), each its own request with its own queue wait, completion callback and JS/C++ crossing, and fs.promises.writeFile() did the same through a FileHandle. For the small files applications write most, the round trips are the cost, and each occupies a pool slot that concurrent fs, dns.lookup() and crypto work is also queueing for. Add WriteFileJob next to ReadFileJob: an AsyncWrap + ThreadPoolWork that opens, writes the whole buffer (looping on short writes) and closes as one pool task, keeping the buffer alive until it is done. fs.writeFile() uses it for path arguments without flush; fs.promises.writeFile() additionally keeps data above one write chunk (and iterables) on the FileHandle path, so large writes stay abortable between chunks as before. File descriptors, FileHandles, flush: true and an active VFS keep their existing paths. Behavior is otherwise kept: open failures report syscall 'open' with the path, write failures 'write'; permission errors are delivered through the callback/promise; an abort signalled while the write is in flight is still reported as an AbortError; the job is an FSREQCALLBACK resource for async_hooks and emits the 'write' fs trace event. Tests that used fs.writeFile() as a proxy for open/close trace events, or injected FileHandle faults for path-based writes, are adjusted to keep testing what they test. The job holds the buffer's backing store, so the memory stays valid if the buffer is detached or collected before the write finishes; a resizable ArrayBuffer could still have its pages decommitted by a shrink, so its contents are copied when the job is created. Signed-off-by: Shelley Vohr <[email protected]> PR-URL: #65489 Refs: #65327 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
读整文件原本每文件三次往返:aopen -> read -> close,每次都是一轮「提交 -> 完成包 -> 回到事件循环」。现在一个线程池任务里做完 open + size + read + close 并交回字节 (py_read_whole_async),形状对齐 Node.js nodejs/node#65327(ReadFileJob)。 实测(Windows、4 KiB 文件、并发 64、SATA SSD): * read_bytes_many(公开 API) 13,092 -> 22,367 files/s 1.71x * 裸 read_whole_async 12,091 -> 25,585 files/s 2.12x 两个反直觉的点,一并记进 changelog: 1) 最直觉的 Python 实现 asyncio.to_thread(同步整读) 比要替换的代码**慢一倍** (6.7k vs 13.1k files/s)。我们的跳是 IOCP 完成包,比「线程池提交 + GIL 往返」 便宜,用 to_thread 等于拿贵机制换便宜机制。 2) 每跳成本约 25-35 us,主要由 Python future 与事件循环转身决定,不是 syscall; 压跳数之所以划算正因如此。 语义保持不变,新增 tests/python/test_whole_read.py(12 项)以**异步路径为基准**逐条 对照:0/1/4095/4096/700000 字节内容、PathLike/str 入参、缺失文件与目录的异常类型、 errno 与文案("Failed to open file: <path>" 逐字节一致)、顺序保持、失败传播、 句柄泄漏计数。文本系列与 write_bytes 仍走异步后端。 架构:新文件 src/bindings/python/py_whole.cpp 走 platform/native_file.hpp(组合根 allow: ["*"]),架构守卫 82 文件 / 169 includes 通过。 验证:全套连跑 3 次 367 passed(+12);强制完成包路径 367 passed; thread_pool 365 passed;ctest 3/3;架构守卫 OK。
Errors from readFile(), writeFile() and appendFile() have had no stack frames since they started using a single thread pool round trip, as the stack was captured from a C++ callback with no JS frames on it. Capture it after the await instead, like the rest of fs/promises. Refs: nodejs#65327 Refs: nodejs#65489 Assisted-by: claude:opus-5.5 Signed-off-by: vedchaudhari <[email protected]>
Errors from readFile(), writeFile() and appendFile() have had no stack frames since they started using a single thread pool round trip, as the stack was captured from a C++ callback with no JS frames on it. Capture it after the await instead, like the rest of fs/promises. Refs: nodejs#65327 Refs: nodejs#65489 Assisted-by: claude:opus-5.5 Signed-off-by: vedchaudhari <[email protected]>
…nnot load (aws#2117) Fixes # On Node 26.8 and later, the integ-runner unit tests that use `mock-fs` fail before any test runs: ``` TypeError: Cannot read properties of undefined (reading 'read') at exports.patchReadFileContext (mock-fs/lib/readfilecontext.js:40:30) ``` `mock-fs` patches Node.js `fs` internals, and [nodejs/node#65327](nodejs/node#65327) changed `fs.readFile` so that the internal hook `mock-fs` relies on no longer fires. The library now throws as soon as it is required ([tschaub/mock-fs#447](tschaub/mock-fs#447)). There is no fixed release. The affected tests in `integ-runner` now write their fixtures into real temp directories created with `fs.mkdtempSync`, the same approach other tests in the repo already use. `mock-fs` is removed as a dev dependency from `integ-runner`, `cdk-assets-lib` and `cloud-assembly-schema`, which no longer import it. Also includes a lint fix for import order in `toolkit-lib`'s `base-credentials.ts`. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
`ReadFileJob` treats a file that is not a regular file as size 0 and reads it to EOF inside one thread pool job, so the `AbortSignal` is only checked once the job ends. `fs.readFile()` and `fs.promises.readFile()` on a FIFO whose writer stays open therefore do not settle until the writer closes, and on `/dev/zero` they keep growing the buffer until the process runs out of memory. Hand the fd back to the chunked reader for non-regular files, the path files larger than one chunk already take. It checks the signal before each read. Regular files that report size 0, such as procfs files, are still read in one job. Signed-off-by: Christian Aurich Zanettini Martins <[email protected]> PR-URL: #66940 Refs: #65327 Reviewed-By: Anna Henningsen <[email protected]>
`ReadFileJob` treats a file that is not a regular file as size 0 and reads it to EOF inside one thread pool job, so the `AbortSignal` is only checked once the job ends. `fs.readFile()` and `fs.promises.readFile()` on a FIFO whose writer stays open therefore do not settle until the writer closes, and on `/dev/zero` they keep growing the buffer until the process runs out of memory. Hand the fd back to the chunked reader for non-regular files, the path files larger than one chunk already take. It checks the signal before each read. Regular files that report size 0, such as procfs files, are still read in one job. Signed-off-by: Christian Aurich Zanettini Martins <[email protected]> PR-URL: #66940 Refs: #65327 Reviewed-By: Anna Henningsen <[email protected]>
Errors from readFile(), writeFile() and appendFile() have had no stack frames since they started using a single thread pool round trip, as the stack was captured from a C++ callback with no JS frames on it. Capture it after the await instead, like the rest of fs/promises. Refs: #65327 Refs: #65489 Assisted-by: claude:opus-5.5 Signed-off-by: vedchaudhari <[email protected]> PR-URL: #66757 Reviewed-By: Christian Aurich Zanettini Martins <[email protected]> Reviewed-By: Gürgün Dayıoğlu <[email protected]>
`ReadFileJob` treats a file that is not a regular file as size 0 and reads it to EOF inside one thread pool job, so the `AbortSignal` is only checked once the job ends. `fs.readFile()` and `fs.promises.readFile()` on a FIFO whose writer stays open therefore do not settle until the writer closes, and on `/dev/zero` they keep growing the buffer until the process runs out of memory. Hand the fd back to the chunked reader for non-regular files, the path files larger than one chunk already take. It checks the signal before each read. Regular files that report size 0, such as procfs files, are still read in one job. Signed-off-by: Christian Aurich Zanettini Martins <[email protected]> PR-URL: #66940 Refs: #65327 Reviewed-By: Anna Henningsen <[email protected]>
`ReadFileJob` treats a file that is not a regular file as size 0 and reads it to EOF inside one thread pool job, so the `AbortSignal` is only checked once the job ends. `fs.readFile()` and `fs.promises.readFile()` on a FIFO whose writer stays open therefore do not settle until the writer closes, and on `/dev/zero` they keep growing the buffer until the process runs out of memory. Hand the fd back to the chunked reader for non-regular files, the path files larger than one chunk already take. It checks the signal before each read. Regular files that report size 0, such as procfs files, are still read in one job. Signed-off-by: Christian Aurich Zanettini Martins <[email protected]> PR-URL: #66940 Refs: #65327 Reviewed-By: Anna Henningsen <[email protected]>
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.