Skip to content

Watch files through the OS instead of re-hashing them every second - #6377

Draft
ondrejmirtes wants to merge 1 commit into
2.3.xfrom
native-file-monitors
Draft

Watch files through the OS instead of re-hashing them every second#6377
ondrejmirtes wants to merge 1 commit into
2.3.xfrom
native-file-monitors

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

PHPStan Pro's FileMonitor re-hashes every analysed and scanned file on every poll — 2772 files and ~230 ms on a mid-sized project, once a second, for as long as the browser tab is open. That is also the floor on how fast an edit can be noticed.

What changed

FileMonitor becomes an interface. The old implementation is HashingFileMonitor and stays the answer for Windows and for anything the native backends cannot do. FsEventsFileMonitor (macOS) and InotifyFileMonitor (Linux) reach the kernel through FFI.

The native monitors are a gate in front of the hashing one, not a replacement. The kernel is only asked "was anything touched"; the wrapped HashingFileMonitor still decides which files that means, so the reported FileMonitorResult is exactly what it always was — exclude rules and the touch-without-content-change case stay in one place. An idle poll costs one non-blocking syscall instead of reading the project, which is what lets the poll interval drop from 1 s to 50 ms.

Measurements

2760-file tree, hashing vs native:

idle poll detection (median)
macOS hashing 81.9 ms 1110 ms
macOS FSEvents 0.002 ms 143 ms
Linux hashing 34.2 ms 1065 ms
Linux inotify 0.002 ms 104 ms

End to end in Pro on a real project (edit → analysis starts): 0.749 s → 0.250 s, and full hash passes over a 40 s session went from 26 to 4 — one per edit, none while idle.

Design notes

  • kqueue was tried on macOS and rejected. EVFILT_VNODE on a directory reports directory-entry changes only; a file rewritten in place — which is what most editor saves are — produces no event at all. FSEvents reports it, watches subtrees recursively, and needs no extra extension.
  • A watch can register successfully and then never deliver. A monitor that silently sees nothing would leave Pro looking frozen, so initialize() writes a probe file into a watched directory and refuses the backend unless the kernel reports it. Costs ~17 ms once, and turns that failure into a fallback.
  • Scattered monitored files (composer.lock, config files) are polled with stat() rather than pulling their parent — otherwise the project root becomes a recursive watch and every result-cache write reopens the gate.

Compatibility

PHP floor is unchanged (7.4); FFI has existed since 7.4. Verified in real images:

environment native path
Homebrew macOS, Debian/Ubuntu php-cli yes
Alpine needs phpXX-ffi
official php:* Docker images no (no ext-ffi)
Windows no

ffi.enable=preload is not a blocker — the CLI SAPI is exempt. Every failure path (no FFI, watch limits, unwritable tree, a filesystem that never delivers) falls back to HashingFileMonitor, so no configuration is worse off than before. PHPSTAN_DISABLE_NATIVE_FILE_MONITOR forces the hashing monitor.

Docker Desktop bind mounts were tested and do work; the 1–3 s there is VirtioFS propagation, which the hashing monitor pays identically.

Tests

tests/PHPStan/File/FileMonitorTest.php runs the same 9 scenarios against every backend available on the host, asserting identical results — 18 tests / 198 assertions green on macOS (FSEvents) and in Docker on Linux (inotify).

Open question for review

PHPStan cannot model FFI's methods (they are the C functions from the cdef string), so build/phpstan.neon carries a path-scoped method.notFound ignore for the two monitor classes. The proper fix would be an FFI methods-reflection extension — flagged rather than built, since it changes analysis for every FFI user.

🤖 Generated with Claude Code

PHPStan Pro's FileMonitor re-hashed every analysed and scanned file on
every poll - 2772 files and ~230ms on a mid-sized project, once a second,
for as long as the browser tab is open. That is also the floor on how
fast an edit can be noticed.

FileMonitor becomes an interface. The old implementation is
HashingFileMonitor and stays the answer for Windows and for anything the
native backends cannot do. FsEventsFileMonitor (macOS) and
InotifyFileMonitor (Linux) reach the kernel through FFI, which needs no
extra extension and works in CLI whatever ffi.enable says.

The native monitors are a gate in front of the hashing one rather than a
replacement: the kernel is only asked "was anything touched", and the
wrapped HashingFileMonitor still decides which files that means, so the
reported FileMonitorResult is what it always was. An idle poll costs one
non-blocking syscall instead of reading the project, which is what lets
the poll interval drop from 1s to 50ms.

kqueue would have been the smaller dependency on macOS but EVFILT_VNODE
on a directory reports directory-entry changes only - a file rewritten in
place, which is what most editor saves are, produces no event at all.

Registering a watch can succeed on a filesystem that then never delivers
anything, and a monitor that silently sees nothing would leave Pro
looking frozen. So initialize() writes a probe file into a watched
directory and refuses the backend unless the kernel reports it, which
costs ~17ms once and turns that failure into a fallback.

Measured on a 2760-file tree, hashing vs native:
  macOS   idle poll 81.9ms -> 0.002ms, detection 1110ms -> 143ms
  Linux   idle poll 34.2ms -> 0.002ms, detection 1065ms -> 104ms
End to end in Pro on a real project, edit to analysis start: 0.749s ->
0.250s, and the full hash passes over a 40s session went from 26 to 4 -
one per edit, none while idle.

PHPSTAN_DISABLE_NATIVE_FILE_MONITOR forces the hashing monitor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LD1Zy3qXBVk6ajYdn6QPKu
@phpstan-bot

Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.3.x. PHPStan 2.3 is not going to be released for months. If your code is relevant on 2.2.x and you want it to be released sooner, please rebase your pull request and change its target to 2.2.x.

@staabm

staabm commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

FileMonitor becomes an interface. The old implementation is HashingFileMonitor and stays the answer for Windows and for anything the native backends cannot do. FsEventsFileMonitor (macOS) and InotifyFileMonitor (Linux) reach the kernel through FFI.

I really like this PR and the improvements we get out of it. I fear that most default-php installations will not ship with the necessary php-extensions and therefore its usefullness is limited.

we might need a CLI PRO-startup-warning or message in the PRO-UI, which informs people how to get these extensions ... or maybe we can ship the extensions similar to turbo .. or a pie installer integration .. or something similar to make sure the extensions will be easily available/installable

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.

3 participants