Skip to content

Introduce an alternative macOS implementation that uses kqueue#4

Draft
savetheclocktower wants to merge 2 commits intomasterfrom
kqueue-implementation
Draft

Introduce an alternative macOS implementation that uses kqueue#4
savetheclocktower wants to merge 2 commits intomasterfrom
kqueue-implementation

Conversation

@savetheclocktower
Copy link

@savetheclocktower savetheclocktower commented Mar 1, 2026

…to better match this library's pre-v9 behavior.

pulsar#1459 is vexing because I've not yet found a good way to reproduce the conditions in which it manifests. The outward symptom is that Pulsar fails to recognize when one of the open buffers has its contents changed on disk by another program.

So far, this is only affecting macOS. That tracks; even though this library was nearly entirely rewritten for version 9, Windows and Linux still use the exact same mechanisms for file-watching as they did before (ReadDirectoryChangesW and inotify, respectively). But on macOS, we moved from kqueue to FSEvents on the theory that the latter is newer and theoretically better for file-watching. (It supports recursive file-watching, for instance; while this isn't something that pathwatcher specifically needs, it opens up opportunities for streamlining of watchers.)

But it's different — which means that it will behave differently in some edge cases! And I'm starting to think that pathwatcher was right to stick with kqueue for its use cases.

There are a few reasons that this could happen:

  1. A stream could fail to start. Since a single stream can watch an arbitrary number of paths, we need only two: the one that's currently running, plus a backup stream. When we add a new path to the list of paths to watch, we start up the backup stream, promote it to the primary stream, then stop the other stream and demote it to backup status. This staggered approach is designed to ensure we don't miss anything.

    But the stream could fail to start! The API docs say this “isn't supposed to happen,” but that API consumers should check the return value and fall back to another approach. In practice, I've seen this happen when fseventsd is maxed-out on the number of streams (I think it's 1024).

  2. A stream could successfully start… but still somehow miss a filesystem event. I don't know under what circumstances this could happen… but it's definitely happening, and it's hard to debug. For instance, this comment suggests that iCloud Drive sharing permissions may factor into it; Claude theorizes that a read-only file might actually be stored elsewhere on the filesystem, and that fseventsd might fail to detect edits made by others.

Without a good ability to diagnose this, I decided to introduce a kqueue implementation of file-watching on macOS that could be swapped in. The main implementation difference in kqueue is that you must subscribe to a specific file to detect changes to the file; with FSEvents (and other mechanisms) you can listen on the parent directory to pick up those changes. In practice, this doesn't make much of a difference, though.

It was surprisingly easy to make these tests pass with the kqueue implementation. I don't think any of the changes I had to make in the JS affect the other platforms, but I suppose I'll find out when I look at the CI job results.


Anyway: I can't promise that this will fix all file-watching bugs. But kqueue is probably better for this use case: it's a lower-level API built into the kernel, it fires events with less latency, and it's much easier to find out why the watcher has triggered. And though I didn't rely on any of the previous pathwatcher code when adding this kqueue implementation, I think it's much more likely to behave like the v8-and-below pathwatcher.

There is a theoretical limit to how many files can be monitored this way by kqueue — but the limit is per-process. Since each window is its own renderer process, this gives us a lot of flexibility. And though the default limit is 256, that's actually fine in most scenarios — and we can increase it if need be via setrlimit.

If the specs pass, I think we should give this a try in the next regular release, or at least open a draft PR so that those affected by pulsar#1459 can try it out for a while and tell us if it helps. I wouldn't say I'm ready to abandon the FSEvents adapter altogether, so it's worth keeping around while we're experimenting. (Originally I wanted the ability to opt into one implementation or another at runtime, but that would've ramped up the complexity of this PR, and I really just wanted to get something out there.)

@savetheclocktower
Copy link
Author

…OK. Yeah, no regressions on the other platforms. I will spin up a draft PR for Pulsar and see if this improves things.

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.

1 participant