Repository navigation
Conversation
Delete the per-handler 1 ms duplicate suppression in PathWatcher::emit. It dropped the inotify record that carried the last write of a burst, so a listener never heard of the final change. The kqueue and FSEvents arms share the same function. Under a recursive watch, the reader now skips the nameless IN_ATTRIB, IN_MODIFY, IN_DELETE_SELF and IN_MOVE_SELF records of a subdirectory's own watch. The parent's watch reports the same change under the name of the subdirectory. The suppression hid that second report when the two records landed within 1 ms. The Windows copy of the rule stays. It caps the storm of renames that libuv reports for a removed directory until the watch of a removed directory is retired.
|
Updated 10:10 AM PT - Oct 20th, 2026
✅ @robobun, your commit 00e4ffcdde319e5abbe77cf8df247b0ab60b441b passed in 🧪 To try this PR locally: bunx bun-pr 46191That installs a local version of the PR into your bun-46191 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughLinux ChangesLinux
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Beyond the inline findings, I also checked two things and ruled them out: the new oi += 1; continue skip at src/runtime/node/path_watcher.rs:1029 bypasses touched.get_or_put, but since nothing was emitted for that owner no on_update_end is owed, and the parent wd's named record touches the same watcher in the common case; and the Darwin on_fs_event path (src/runtime/node/path_watcher.rs:1267) also loses the 1 ms fold, but FSEvents already coalesces per path in the kernel before events_cb, so it does not produce the new same-path duplicates inotify can.
Extended reasoning...
The change deletes the per-handler ChangeEvent duplicate suppression in src/runtime/node/path_watcher.rs so every inotify, kqueue and FSEvents record reaches every FSWatcher handler, adds a Linux-only skip of nameless self-records on a recursive watch's subdirectory wds, and adds Linux-gated tests. It touches no security-sensitive surface. Four confirmed findings are posted inline (listener flood without a bound, mount-root attribute events lost under the new skip, double reports from the recursive walk race, and the pre-existing Windows gap), so the inline comments already signal that a human needs to look; this note only records what else was examined.
Findings marked 🟡 are optional suggestions and need no follow-up push.
|
#44007 and #44008 worked on the same rule. I closed both: this PR contains the change of #44007, and it removes the rule with no stack. Two facts from that work, measured on this head (00e4ffc, release build, Linux x64, load average 5):
#44008 dropped a record identical to a listener's last event while the listener has not received that event. With that merge the first number was 8 of 2000 in the runs of #44008. The code is here if a maintainer wants that bound after this PR. |
Fixes #45954
Problem
fs.watch(file)on Linux drops thechangeevent of the last write in a burst. A listener that stats the file on each event never sees the final size.ChangeEvent::should_emit(src/runtime/node/path_watcher.rs:226) discards a record of the same path and kind within 1 ms of the last emitted one. Nothing re-arms a trailing event.Fix
ChangeEventandemit_unsuppressed.PathWatcher::emitdelivers every record to every handler, as libuv does.test/js/node/watch/fs.watch.test.ts, 7 new Linux tests, the first fails 5 of 5 runs on bun 1.4.3. Also the 42 vendoredtest-fs-watch*files.Background
PathWatcheris one OS watch. Its reader thread callsemitonce per kernel record.Downsides
fs.watch: endless file events reported nodejs/node#61398). After fs.watch: report the watched directory's removal once on Windows, and start a new watch when the path is watched again #39827 retires that watch, the copy can go.Notes
Measurements. Linux x64, release builds: main is 1.4.3-canary+367d939d9 (the watcher code did not change between it and the merge base), this PR is 1.4.4-canary+15ff22a27 (before #44007 was folded in, which does not touch the file path), node v26.3.0. The writer child is bun 1.4.3 in every run. The listener spins 20 us per call.
run.sh: this PR reachedDONEin 5 of 5 runs.mkdir nthen a file in it, 100 runs:n/freported 1.00 times on main, this PR and node.max_queued_events. Bun's reader thread drains at once, so a flood yields more records. A bound on that queue is a separate change.bloaty,perf,valgrindandstraceare not on this machine, and no release build of the merge base exists here. The change is a deletion on the reader path: oneclock_gettime, one wyhash of the path and 24 bytes per handler per record are gone.Fail-before detail. On bun 1.4.3, 5 runs of the 7 new tests: the write, chmod, write test fails 5 of 5, and 3 or 4 of the 6 recursive tests fail per run (the 1 ms rule hides the second report only when the two records land in time).
Other PRs. #44008 is superseded: it removed the rule on Linux and FreeBSD but replaced it with a merge into an undelivered event, kept the Windows rule, and is stacked on #44007 and #43715. #44007 is folded in here. #40200 and #42819 keep
should_emitand must drop it when they rebase. #32962 and #33110 bypassed the rule for one event each (IN_IGNORED, queue overflow) with the same libuv-parity reason. No maintainer has ruled on the rule itself. The two questions put on #44008 are answered here by deletion on POSIX and by keeping Windows until #39827.Windows.
win_watcher.rskeepsChangeEvent. Its timestamp is libuv's cached loop time, which changes once per loop iteration, so same-name records of one completion fold into one event, and so do records of the next iteration within 1 ms. The listener can run between two iterations, so a trailing write can still be lost there. Deleting the copy now would raise the removed-directory storm from about 500 calls per second to node's 180000 with no Windows test run here. After #39827 retires the watch of a removed directory, the copy can go.The recursive walk. An entry created after a new subdirectory's watch attaches is reported by the walk and again by its own IN_CREATE, which the next
read(2)returns. The 1 ms rule hid that only when the read landed in time. Node'srecursive_watch.jshas the same race. The vendored node tests usemustCallAtLeastthere.Self-review. 11 concerns raised, 10 addressed: the recursive double report (folded #44007 in), a burst test that passed on 1.4.3 in most runs and would hang on macOS (replaced by the write, chmod, write test), the Windows storm (kept the Windows copy), stale comments, and this body names the superseded PR and the precedents. Rejected: delete the Windows copy now for parity, for the reason above.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/watch/fs.watch.test.ts