sys: hint POSIX_FADV_SEQUENTIAL on the copy-file read/write fallback
When clonefile/copy_file_range/sendfile all bail and we fall back to a manual read()/write() loop, tell the kernel up front that the source will be read sequentially so it doubles its readahead window.
What changed
NodeFS::copy_file_using_read_write_loop(node_fs.rs): oneposix_fadvise(src_fd, 0, 0, POSIX_FADV_SEQUENTIAL)at the top, gated to Linux/Android/FreeBSD. Failures are ignored.bun_sys::copy_file_range(sys/copy_file.rs): a newHAS_HINTED_SEQUENTIALbit onLinuxCopyFileState, cleared on entry tocopy_file_with_stateand set the first time we fall through tocopy_file_read_write_loop. This gives exactly one hint per source fd even though the outer loop re-enters the 32 KiB read/write helper once per chunk, and even whenbun installreuses the state across a whole directory tree.bench/copyfile/fallback-rw-loop.mjs: reproducible cold-cache benchmark (usesPOSIX_FADV_DONTNEEDto evict the source between iterations).
Benchmarks
All on overlayfs/NVMe, kernel 6.17, 128 KiB default readahead.
Isolated read/write loop (standalone C, 512 MiB file, cold cache, 10 iters alternating order; this is the shape of both fallback loops with their respective buffer sizes):
| buffer | no hint | with hint | speedup |
|---|---|---|---|
| 32 KiB | 2387 ms | 1715 ms | 1.39x |
| 64 KiB | 2352 ms | 1748 ms | 1.35x |
| 8 MiB | 1813 ms | 1809 ms | ~1.00x |
End-to-end Bun.write(file, file) with BUN_CONFIG_DISABLE_COPY_FILE_RANGE=1 (debug build, cold cache, interleaved 6x10 iters):
| file size | path | before (min) | after (min) | speedup |
|---|---|---|---|---|
| 1 MiB | 64 KiB buffer | 6.5-7.3 ms | 5.4-5.9 ms | ~1.2x |
| 512 MiB | 8 MiB slab | 1810 ms | 1810 ms | 1.00x |
| 64 MiB warm cache | 46.2 ms med | 46.6 ms med | no regression |
Why the 8 MiB slab case is flat
copy_file_using_read_write_loop allocates an 8 MiB heap slab for files > 1 MiB. An 8 MiB read() already dwarfs the 128/256 KiB readahead window, and the kernel's adaptive readahead ramps to its ceiling after the first couple of reads with or without the hint. So for large files through this path the hint is a no-op (one extra syscall, ~2 us). The measurable win is on the fixed 32 KiB loop in sys/copy_file.rs (always) and the 64 KiB stack-buffer branch of node_fs.rs (files <= 1 MiB).
Windows
Not touched. On Windows fs.copyFile/fs.cp and bun_sys::copy_file all go through CopyFileW, so the read/write fallback in node_fs.rs/sys/copy_file.rs is never reached. The only Windows-side read/write loop is CopyFileWindows::ReadWriteLoop in blob/copy_file.rs (libuv-async, 64 KiB, used when Bun.write can't get a source path); adding FILE_FLAG_SEQUENTIAL_SCAN there would mean reopening a handle we were handed, which is a larger change than this PR attempts.
Correctness
Existing coverage exercises the fallback path directly:
test/js/bun/io/bun-write.test.js"should work when copyFileRange is not available" (large + small)test/js/node/fs/fs.test.ts"copyFileSync > should work when copyFileRange is not available"test/js/node/fs/cp.test.ts(all passing with this change)
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/io/bun-write.test.js