Skip to content

fsevent: coalesce watches nested inside other watches - #975

Merged
JohnTitor merged 2 commits into
notify-rs:mainfrom
daandemeyer:push-lyprlzrmspkr
Aug 11, 2026
Merged

fsevent: coalesce watches nested inside other watches#975
JohnTitor merged 2 commits into
notify-rs:mainfrom
daandemeyer:push-lyprlzrmspkr

Conversation

@daandemeyer

Copy link
Copy Markdown
Contributor

An FSEvents stream root reports the entire hierarchy below it, and
callback_impl attributes every event to the longest watch it falls
under, so passing both a directory and something below it to
FSEventStreamCreate adds nothing while costing a slot against the path
limit that macOS enforces by corrupting the process descriptor table
above roughly RLIMIT_NOFILE/10 paths.

Derive the CFArray handed to FSEventStreamCreate from the watch map in
run() instead of maintaining it alongside, dropping any watch whose
ancestors include another watch on the same volume. FSEvents does not
promise that a stream root reports events from a volume mounted below
it, so a covering ancestor across a mount point does not count.
Unwatching a covering directory promotes the nested watches back to
stream roots on the next run with no separate bookkeeping, and
remove_cf_path goes away.

One behavioural difference: with kFSEventStreamCreateFlagWatchRoot, a
nested watch no longer receives a root-changed event of its own when an
ancestor is renamed. A recursive covering watch still reports the
rename; a non-recursive one reports nothing, since callback_impl
accepts only direct children for those.

Covered by unit tests for the stream roots produced by nested and by
sibling watches, and by black-box tests that a nested watch still
delivers events after its covering watch is removed and that a covering
watch still delivers events outside the nested one.

Signed-off-by: Daan De Meyer daan@amutable.com

daandemeyer and others added 2 commits August 8, 2026 00:28
An FSEvents stream root reports the entire hierarchy below it, and
`callback_impl` attributes every event to the longest watch it falls
under, so passing both a directory and something below it to
`FSEventStreamCreate` adds nothing while costing a slot against the path
limit that macOS enforces by corrupting the process descriptor table
above roughly RLIMIT_NOFILE/10 paths.

Derive the CFArray handed to `FSEventStreamCreate` from the watch map in
`run()` instead of maintaining it alongside, dropping any watch whose
ancestors include another watch on the same volume. FSEvents does not
promise that a stream root reports events from a volume mounted below
it, so a covering ancestor across a mount point does not count.
Unwatching a covering directory promotes the nested watches back to
stream roots on the next run with no separate bookkeeping, and
`remove_cf_path` goes away.

One behavioural difference: with `kFSEventStreamCreateFlagWatchRoot`, a
nested watch no longer receives a root-changed event of its own when an
ancestor is renamed. A recursive covering watch still reports the
rename; a non-recursive one reports nothing, since `callback_impl`
accepts only direct children for those.

Covered by unit tests for the stream roots produced by nested and by
sibling watches, and by black-box tests that a nested watch still
delivers events after its covering watch is removed and that a covering
watch still delivers events outside the nested one.

Signed-off-by: Daan De Meyer <daan@amutable.com>

@JohnTitor JohnTitor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 馃憤 added a tweak around some coalescing condition.

@JohnTitor
JohnTitor merged commit 75d86e0 into notify-rs:main Aug 11, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants