perf(cascade): relativise watcher paths without a resolve() per event - #459
Merged
Merged
Conversation
`_relative_to_root` called `Path(raw).resolve()` on every watchdog event to defend against an external symlink surfacing inside the tree. watchdog already reports absolute paths inside the watched root, so for the common case that is a filesystem round trip per path component for nothing — on Windows a `GetFinalPathNameByHandle` call that goes through Defender. py-spy on the 2-hour soak under raised write load: about 40 % of the worker threads' samples sat in `realpath` / `_getfinalpathname` / `stat` under this one function, GIL held, while the event loop waited. The textual `relative_to` now comes first; `resolve()` runs only when the path is not under the root as written or carries `..` segments, so the symlink case still maps and outside paths still return None. Two tests: an in-root path is relativised with `Path.resolve` patched to raise, and a path through an external symlink still resolves inside. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of the previous commit: the `..` guard was untested (a copy without it passed the whole file), and the docstring described the cost as "a round trip per path component" when on an existing file it is two GetFinalPathNameByHandle opens plus a stat(); the per-component walk only happens for paths that are already gone. One test pins the guard; the docstring says what the soak profile actually showed and names the 8.3 short-name residue the scanner's sweep reconciles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
arelchan
self-requested a review
September 24, 2026 08:32
0xKT
approved these changes
Sep 24, 2026
arelchan
approved these changes
Sep 24, 2026
This was referenced Sep 24, 2026
Merged
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.
Summary
_relative_to_rootin the cascade watcher calledPath(raw).resolve()on every watchdog event (create / modify / move / delete) to defend against an external symlink surfacing inside the tree. watchdog already hands us absolute paths inside the watched root, so in the common case that is a filesystem round trip per path component for nothing — on Windows aGetFinalPathNameByHandlecall per component, each through Defender.The textual
relative_tonow runs first;resolve()is kept only for a path that is not under the root as written, or that carries..segments. Same results for the symlink case (still maps) and for outside paths (stillNone).Evidence
py-spy, 25 s, the soak server under raised write load (three feeders ≈ 30 md rewrites/s + two
cascade syncstorms + a search storm, PR #454's box): of 1 082 worker-thread samples, 435 (40 %) were inrealpath/_getfinalpathname_nonstrict/stat, and 403 of those (93 %) came from_relative_to_rootvia_Handler._enqueue(on_modified221,on_created72,on_moved91,on_deleted19). The Python-level ntpath/pathlib frames around those calls hold the GIL; the Win32 calls themselves release it.Area
Verification
tests/unit/test_memory/test_cascade: 221 passed.test_relative_to_root_within_does_not_touch_the_filesystem:Path.resolvepatched to raise; red without the fast path (AssertionError: resolve() must not run for an in-root path), green with it.test_relative_to_root_via_symlink_still_resolves(POSIX): a path reaching the root through an external symlink still maps to the same relative path.make lintgreen.Checklist
..inputsNotes for Reviewers
rootisMemoryRoot.rootas configured; if it is itself given through a symlink or a different case than watchdog reports, the textualrelative_tofails and the oldresolve()path runs — so the fallback is exercised, not lost. WindowsPureWindowsPath.relative_tocompares case-insensitively.🤖 Generated with Claude Code