Repository navigation
Conversation
`NativeWatcher.startWatching` resolves as soon as `fs.watch` returns, but on macOS that can be before any changes are reported. libuv only signals another thread to (re)create the FSEvents stream, and the stream reports only changes made after it starts: https://github.com/libuv/libuv/blob/2b4b918d3381100854250c89d5159d4206daafb7/src/unix/fsevents.c#L362-L372 So a change made straight after startup can be silently missed, and since #2054 started non-Watchman backends before the crawl, so can one made during the crawl. On a slow host the gap is long enough to hit regularly - it's the remaining flake in the Native watcher integration tests, where the suite's first event never arrives. 14 of 297 macOS test jobs since #1974 hit it, across all three Node versions. This has `Watcher` confirm each root's watch before relying on it. Once every backend has started, it writes a health check file into each root and waits for it to be reported, as `checkHealth()` already does for the project root. If a watch can't be confirmed within 10s, Metro warns and carries on. Roots added later with `addRoot` are confirmed the same way. A backend opts in by returning `false` from a new static `isLiveOnStart()`, which only `NativeWatcher` does - Watchman subscribes from the crawl's clock, and `FallbackWatcher`'s watches are live as soon as `fs.watch` returns. It's done in `Watcher`, after all the backends have started, because `NativeWatcher`s for multiple roots share one FSEvents stream, which restarts as each root is added. This adds ~10ms to a watch-mode start on macOS without Watchman (145ms -> 155ms on our benchmark app). Changelog: ``` - **[Fix]**: Fix changes made immediately after startup sometimes being missed on macOS ``` Test plan: New `Watcher-test.js` cases cover confirming each root before the crawl, warning and carrying on when a watch can't be confirmed, confirming a root added later, and leaving backends that are live on start alone. The backend integration tests now wait for a cookie of their own, as `Watcher` does. To reproduce the race, I patched `NativeWatcher` to drop all events for its first 300ms. A `FileMap` in watch mode with two roots, writing a file to each as soon as `build()` resolves, sees neither change without this PR (3/3 runs) and both with it (3/3). The `metro-file-map` suite passes on macOS, Linux and Windows. Benchmark (AI-driven): the harness from #2104, 8 interleaved rounds on an M4 Pro Mac mini, n=32 warm watch-mode starts per variant (+6.7%, 95% CI +5.2% to +8.0%).
huntie
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NativeWatcher.startWatchingresolves as soon asfs.watchreturns, but on macOS that can before we’re actually listening for changes. libuv only signals another thread to (re)create the FSEvents stream, and the stream reports only changes made after it starts:https://github.com/libuv/libuv/blob/2b4b918d3381100854250c89d5159d4206daafb7/src/unix/fsevents.c#L362-L372
So a change made straight after startup or during crawl (since #2054) can be silently missed.
This is real - especially on a slow host the gap is long enough to hit regularly - it's the remaining flake in the NativeWatcher integration tests, where the test’s first event never arrives. 14 of 297 macOS test jobs in CI since #1974 hit it.
This change has
Watcherconfirm each root's watch before relying on it, if the backend declares itself not to be live on start, using the established pattern of a health check cookie. Once every backend has started, it writes a cookie into each root and waits for it to be reported. If a watch can't be confirmed within 10s, Metro warns and carries on. Roots added later withaddRootare confirmed the same way.Only the
NativeWatcherbackend returnsfalsefrom the new staticisLiveOnStart()mechanism. Watchman doesn’t need it due to the clock mechanism, andFallbackWatcherdoesn’t need it on Windows or Linux becausefs.watchestablishes a shallow watch before returning.This adds ~10ms to a watch-mode start on macOS without Watchman (145ms -> 155ms on our benchmark app). We’ll recover that in the stack of #2104.
Changelog:
Test plan:
New
Watcher-test.jscases cover confirming each root before the crawl, warning and carrying on when a watch can't be confirmed, confirming a root added later, and leaving backends that are live on start alone. The backend integration tests now wait for a cookie of their own, asWatcherdoes.To reproduce the race, I patched
NativeWatcherto drop all events for its first 300ms. AFileMapin watch mode with two roots, writing a file to each as soon asbuild()resolves, sees neither change without this PR (3/3 runs) and both with it (3/3).The
metro-file-mapsuite passes on macOS, Linux and Windows. Benchmark (AI-driven): the harness from #2104, 8 interleaved rounds on an M4 Pro Mac mini, n=32 warm watch-mode starts per variant (+6.7%, 95% CI +5.2% to +8.0%).