Skip to content

Fix case sensitivity fswatch and users - #64210

Draft
Jake Bailey (jakebailey) wants to merge 16 commits into
microsoft:mainfrom
jakebailey:watch-invalidation-refactor
Draft

Fix case sensitivity fswatch and users#64210
Jake Bailey (jakebailey) wants to merge 16 commits into
microsoft:mainfrom
jakebailey:watch-invalidation-refactor

Conversation

@jakebailey

Copy link
Copy Markdown
Member

This is like the souped up version of #64191, due to my realization in #64191 (comment) meaning that it needed more work.

Unfortunately, a lot of work.... This is me letting Astra churn and churn and churn, as it found more and more broken up the stack. Not happy about how big this turns out to be, but there were a lot of problems found.

It's mostly testing, but it is about 1500 lines on top of #64191 of bookkeeping. Windows and Linux continue to pass.

Roughly:

  • Like Fix FSEvents routing for differently cased watch paths #64191, use system case matching for macOS watches.
  • Add a "watchalias" package which remembers the casing for new watches, so that when the fswatch delivers things of a different case, they can be matched.
  • A load of bookeeping in the compiler / project system to keep this info up to date and efficiently.

This has a perf impact, I'm trying to measure it.

Use the watched volume's case sensitivity when routing FSEvents and
filtering file watches. Preserve caller-visible root casing, including
shared callbacks, overflow matching, and root deletion handling.
Skip shared path prefixes eight bytes at a time and handle ASCII case
folding without component splitting. Retain the Unicode fallback for
case-equivalent paths with different UTF-8 lengths.

Reuse the parent directory's comparer for WatchFile instead of querying
filesystem case sensitivity twice. Add routing benchmarks and expand
boundary and Unicode alignment coverage.

Compare against the initial casing fix on Apple M1. Allocations are
unchanged: zero for exact matches and rejections, and one for rebasing
a differently cased event path.

goos: darwin
goarch: arm64
pkg: github.com/microsoft/TypeScript/tsc/internal/fswatch
cpu: Apple M1
                                           │    before     │                after                │
                                           │    sec/op     │    sec/op     vs base               │
FSEventsDisplayPath/exact-match-8             9.031n ± ∞ ¹   8.958n ± ∞ ¹        ~ (p=0.548 n=5)
FSEventsDisplayPath/case-mismatch-8          130.50n ± ∞ ¹   80.89n ± ∞ ¹  -38.02% (p=0.008 n=5)
FSEventsDisplayPath/sibling-miss-8           103.20n ± ∞ ¹   14.26n ± ∞ ¹  -86.18% (p=0.008 n=5)
FSEventsDisplayPath/unrelated-miss-8         26.120n ± ∞ ¹   6.676n ± ∞ ¹  -74.44% (p=0.008 n=5)
FSEventsDisplayPath/unicode-match-8          115.30n ± ∞ ¹   74.29n ± ∞ ¹  -35.57% (p=0.008 n=5)
FSEventsDisplayPath/unicode-length-match-8    98.83n ± ∞ ¹   52.34n ± ∞ ¹  -47.04% (p=0.008 n=5)
FSEventsRoutingFanout/100-8                  10.799µ ± ∞ ¹   1.634µ ± ∞ ¹  -84.87% (p=0.008 n=5)
FSEventsRoutingFanout/1000-8                 109.21µ ± ∞ ¹   15.10µ ± ∞ ¹  -86.18% (p=0.008 n=5)
geomean                                       284.3n         94.97n        -66.60%
¹ need >= 6 samples for confidence interval at level 0.95
Use CoreFoundation case folding and NFC normalization for non-ASCII
comparisons on case-insensitive Darwin watches. Recognize expanding
aliases such as sharp s and SS without merging distinct dotted-I,
dotless-I, circled-letter, or fullwidth spellings.

Preserve the ASCII fast path, prepare watch-root comparison forms, and
share lazy event folding across routing and callback filtering. Rebase
using original path boundaries rather than folded byte lengths.

Cover file and directory watches, shared callbacks, symlinks, overflow,
and root termination with filesystem regressions and routing benchmarks.
Keep fswatch isolated and leave Windows and case-sensitive behavior
unchanged.
Enable the existing Darwin path comparer for kqueue subscriptions so
WatchFile recognizes alternate casing, expanding Unicode aliases, and
normalization-equivalent names reported by directory enumeration.

Add shared Darwin coverage for creation, modification, deletion, and
distinct filenames. Preserve kqueue directory-event spelling and leave
case-sensitive comparison and other platforms unchanged.
Describe volume-aware matching for FSEvents and kqueue, native Unicode
folding, backend-specific event spelling, and cached comparison forms.
Clarify the scope of the APFS observations and unchanged platform behavior.
Preserve the caller-side alias implementation before simplifying its
state ownership and invalidation lifecycle. Keep compiler path identity
unchanged while matching filesystem notifications to original spellings.
Use authoritative Darwin leaf metadata with cached parent resolution. Keep

other filesystems on their existing resolver and preserve failed lookup

semantics. Invalidate cached resolutions after expanding each event batch,

rather than retaining a second pending event collection.
Use one original-name traversal for registration and immutable index reuse.

Shared lookup sets can be skipped; removing names does not require a new

index. Filesystem changes still rebuild, and symlink mappings are checked

independently. Cover import removal and subsequent source notifications.
Read leaf metadata into a native stat buffer rather than allocating an

os.FileInfo. Verify parent-assisted resolution remains opt-in through

filesystem wrappers, and apply the repository lint and formatting rules.
Register original names and observed physical endpoints in the shared
watch index. Derive physical subtree matching from those registrations
instead of maintaining project reverse maps and deletion indexes.

Keep physical observations with snapshot files, including direct API
hosts without native watches. Refresh affected observations before
content filtering so unchanged-text retargets invalidate resolution.
Reuse immutable registration coverage across overlay-only edits.
Use shared registration matching for aliases and affected subtrees.
Replace separate stale-target maps and generation visitation marks
with cached resolution records and ordinary registration sets.

Pass build filesystems explicitly and refresh once before decisions.
Return retarget results directly to compiler and build paths rather
than retaining cross-layer flags. Publish refreshed matching state
even when the cycle does not build, and retain physical config watches
after a failed config read so target-only events can recover.
Keep received notifications raw until the snapshot update lock is held.
Use the same matching composition for scheduling previews and direct API
updates, then carry affected observations through overlay coalescing
without matching the projected notifications again.

Serialize background adoption with preparation and publication. Preserve
alias disk invalidation for saves without marking other editor documents
saved. Cover queued generation changes, physical saves, and adoption.
Join rebased suffixes with path separators rather than concatenating
root endpoints. Use explicit directory facts when classifying namespace
updates, including watched directories with no registered children.

Cover both POSIX and drive roots, along with empty-directory updates.
Expand notification spellings before the existing overlay coalescer,
but defer descendant deletions to matching the coalesced summary.
Keep affected realpath observations independently so canceled events
still refresh bindings. Share the subtree matcher without expanding
projected aliases again.

Use existing compiler path identity for lifecycle coalescing, retaining
the notification spelling. Cover recreation, final deletion, native
normalization variants, insensitive hosts, interleaved saves and edits,
API merges, and canceled-event retargets.
Move related watch cases into existing compiler-watch, snapshot, and
native watcher suites while preserving package and platform boundaries.

Keep every test, benchmark, helper, assertion, and fixture. Update the
regular-file pathconf fixture to refer to its surviving test file.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad changes to native watcher routing, snapshot synchronization, realpath caching, and incremental builds require final human validation, especially on macOS.

Pull request overview

Extends macOS watch handling so differently cased or normalized filesystem paths are routed back to compiler-visible names without changing compiler identity.

Changes:

  • Adds native Darwin path comparison and case-aware watch routing.
  • Introduces watch-alias indexing across project, CLI, and build watches.
  • Adds realpath caching support and broad regression/performance coverage.
File summaries
File Description
tsc/testdata/baselines/reference/tsbuildWatch/dependencyUpdate/watches-absolute-non-root-dependency-updates.js Updates preserved watch-path casing.
tsc/internal/watchalias/registration.go Implements alias registration and event matching.
tsc/internal/watchalias/index.go Adds the watch-alias index.
tsc/internal/watchalias/index_test.go Tests alias and realpath behavior.
tsc/internal/watchalias/index_darwin_test.go Tests native Darwin aliases.
tsc/internal/watchalias/capability_test.go Tests capability propagation.
tsc/internal/vfs/vfs.go Adds parent-assisted realpath resolution.
tsc/internal/vfs/vfs_test.go Tests realpath fallback behavior.
tsc/internal/vfs/osvfs/realpath_darwin.go Optimizes Darwin realpath resolution.
tsc/internal/vfs/osvfs/realpath_darwin_test.go Tests Darwin realpath semantics.
tsc/internal/vfs/osvfs/os.go Exposes native watch comparison.
tsc/internal/vfs/cachedvfs/cachedvfs.go Propagates new VFS capabilities.
tsc/internal/project/watchnotifications.go Prepares and expands watch notifications.
tsc/internal/project/watchalias.go Integrates aliases into snapshots.
tsc/internal/project/watchalias_darwin_test.go Adds end-to-end Darwin watch tests.
tsc/internal/project/watchalias_coalescing_darwin_test.go Tests native lifecycle coalescing.
tsc/internal/project/watch.go Preserves original watch spellings.
tsc/internal/project/snapshotfs.go Tracks realpath observations.
tsc/internal/project/snapshot.go Refreshes aliases during cloning.
tsc/internal/project/session.go Serializes watch preparation and publication.
tsc/internal/project/session_test.go Updates expected watch casing.
tsc/internal/project/projectcollectionbuilder.go Reuses missing-directory tracking.
tsc/internal/project/project.go Stores original watched filenames.
tsc/internal/project/overlayfs.go Coalesces compiler-equivalent notifications.
tsc/internal/project/filechange.go Retains filesystem-change metadata.
tsc/internal/project/configfileregistrybuilder.go Supports full config invalidation.
tsc/internal/project/configfileregistry.go Retains config lookup spelling.
tsc/internal/project/api.go Expands API-provided watch summaries.
tsc/internal/fswatch/watcher.go Adds comparer-aware routing and filtering.
tsc/internal/fswatch/watcher_test.go Tests comparer and callback behavior.
tsc/internal/fswatch/README.md Documents macOS path semantics.
tsc/internal/fswatch/pathkey.go Exposes immutable path comparers.
tsc/internal/fswatch/pathcompare.go Implements path comparison and rebasing.
tsc/internal/fswatch/fsevents_darwin.go Applies native comparison to FSEvents.
tsc/internal/fswatch/fsevents_darwin_shared_test.go Tests FSEvents routing and performance.
tsc/internal/fswatch/fsevents_darwin_nfd_test.go Tests Darwin Unicode aliases.
tsc/internal/fswatch/fsevents_darwin_ffi.s Adds the CFStringFold trampoline.
tsc/internal/fswatch/fsevents_darwin_ffi.go Implements CoreFoundation folding.
tsc/internal/fswatch/CHANGES.md Records macOS comparison behavior.
tsc/internal/fswatch/canonicalize_other.go Defines exact non-Darwin comparison.
tsc/internal/fswatch/canonicalize_darwin.go Queries Darwin volume sensitivity.
tsc/internal/execute/watchmanager/watchmanager.go Adds alias and resolution lifecycle management.
tsc/internal/execute/watchmanager/watchmanager_test.go Tests watch generations and deletions.
tsc/internal/execute/watchmanager/watchalias_darwin_test.go Tests native alias resolution.
tsc/internal/execute/watcher.go Integrates aliases into CLI watch mode.
tsc/internal/execute/tsctests/contentmapper_watch_test.go Tests manifest casing changes.
tsc/internal/execute/build/watchchanges_test.go Tests ancestor deletion matching.
tsc/internal/execute/build/orchestrator.go Integrates aliases into build watch mode.
tsc/internal/execute/build/compilerHost.go Tracks build filesystem reads.
tsc/internal/execute/build/buildtask.go Retains watched build dependencies.
tsc/internal/bundled/embed.go Propagates VFS watch capabilities.
Review details
  • Files reviewed: 55/55 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Set ordinary symlink dependency edits newer than the build-info output.
Consecutive writes can otherwise share a filesystem timestamp, causing
build mode to reuse its previous result and making these tests flaky.

Keep the equal-timestamp symlink-retarget cases unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants