33package vfswatch
44
55import (
6+ "fmt"
7+ "io"
68 "slices"
79 "sync"
810 "time"
@@ -27,6 +29,7 @@ type FileWatcher struct {
2729 watchState map [string ]WatchEntry
2830 wildcardDirectories map [string ]bool
2931 mu sync.Mutex
32+ debugLog io.Writer // nil = silent; non-nil = write timing lines here
3033}
3134
3235func NewFileWatcher (fs vfs.FS , pollInterval time.Duration , testing bool , callback func ()) * FileWatcher {
@@ -38,6 +41,14 @@ func NewFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callbac
3841 }
3942}
4043
44+ // SetDebugLog enables per-scan timing output written to w.
45+ // Pass nil to disable. Safe to call at any time.
46+ func (fw * FileWatcher ) SetDebugLog (w io.Writer ) {
47+ fw .mu .Lock ()
48+ defer fw .mu .Unlock ()
49+ fw .debugLog = w
50+ }
51+
4152func (fw * FileWatcher ) SetPollInterval (d time.Duration ) {
4253 fw .mu .Lock ()
4354 defer fw .mu .Unlock ()
@@ -70,15 +81,14 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) {
7081 return
7182 }
7283 fw .mu .Lock ()
73- wildcardDirs := fw .wildcardDirectories
7484 pollInterval := fw .pollInterval
7585 fw .mu .Unlock ()
7686 current := fw .currentState ()
7787 settledAt := now ()
7888 tick := min (pollInterval , debounceWait )
7989 for now ().Sub (settledAt ) < debounceWait {
8090 time .Sleep (tick )
81- if fw .hasChanges (current , wildcardDirs ) {
91+ if fw .hasChanges (current ) {
8292 current = fw .currentState ()
8393 settledAt = now ()
8494 }
@@ -118,12 +128,7 @@ func snapshotPaths(fs vfs.FS, paths []string, wildcardDirs map[string]bool) map[
118128 state := make (map [string ]WatchEntry , len (paths ))
119129 for _ , fn := range paths {
120130 if s := fs .Stat (fn ); s != nil {
121- entry := WatchEntry {ModTime : s .ModTime (), Exists : true }
122- if s .IsDir () {
123- entries := fs .GetAccessibleEntries (fn )
124- entry .ChildrenHash = hashEntries (entries )
125- }
126- state [fn ] = entry
131+ state [fn ] = WatchEntry {ModTime : s .ModTime (), Exists : true }
127132 } else {
128133 state [fn ] = WatchEntry {Exists : false }
129134 }
@@ -176,21 +181,26 @@ func hashEntries(entries vfs.Entries) uint64 {
176181 return h .Sum64 ()
177182}
178183
179- func dirChanged (fs vfs.FS , baseline map [string ]WatchEntry , dir string ) bool {
180- entry , ok := baseline [dir ]
181- if ! ok {
182- return true
183- }
184- if entry .ChildrenHash != 0 {
185- entries := fs .GetAccessibleEntries (dir )
186- if hashEntries (entries ) != entry .ChildrenHash {
187- return true
188- }
189- }
190- return false
191- }
192-
193- func (fw * FileWatcher ) hasChanges (baseline map [string ]WatchEntry , wildcardDirs map [string ]bool ) bool {
184+ // hasChanges compares the current filesystem state against baseline.
185+ //
186+ // Tracked entries fall into two categories:
187+ //
188+ // - Explicit paths (files the compiler depends on, plus directory paths
189+ // accessed via DirectoryExists/Stat/etc. during compilation). For these
190+ // we only need to know whether the path exists and, if it does, whether
191+ // its mtime has changed. We never depend on *what's inside* a directory
192+ // in this category — any specific file we care about is tracked
193+ // independently in this same map.
194+ //
195+ // - Wildcard tree directories. snapshotPaths walks every directory under
196+ // each recursive wildcard root and stores it with a ChildrenHash that
197+ // covers the directory's listing. Re-hashing here detects any new,
198+ // deleted, or renamed file or subdirectory in those trees.
199+ //
200+ // Iterating baseline once therefore covers both: a single fs.Stat per entry,
201+ // plus a fs.GetAccessibleEntries only for entries with ChildrenHash != 0
202+ // (i.e. wildcard tree members).
203+ func (fw * FileWatcher ) hasChanges (baseline map [string ]WatchEntry ) bool {
194204 for path , old := range baseline {
195205 s := fw .fs .Stat (path )
196206 if ! old .Exists {
@@ -209,52 +219,47 @@ func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs m
209219 }
210220 }
211221 }
212- for dir , recursive := range wildcardDirs {
213- if ! recursive {
214- if dirChanged (fw .fs , baseline , dir ) {
215- return true
216- }
217- continue
218- }
219- found := false
220- _ = fw .fs .WalkDir (dir , func (path string , d vfs.DirEntry , err error ) error {
221- if err != nil || ! d .IsDir () {
222- return nil
223- }
224- if dirChanged (fw .fs , baseline , path ) {
225- found = true
226- return vfs .SkipAll
227- }
228- return nil
229- })
230- if found {
231- return true
232- }
233- }
234222 return false
235223}
236224
237225// HasChangesFromWatchState compares the current filesystem against the
238- // stored watch state. Safe for concurrent use: watchState and
239- // wildcardDirectories are snapshotted under lock; the maps themselves
240- // are never mutated after creation (UpdateWatchState replaces them ).
226+ // stored watch state. Safe for concurrent use: watchState is snapshotted
227+ // under lock; the map itself is never mutated after creation
228+ // (UpdateWatchState replaces it ).
241229func (fw * FileWatcher ) HasChangesFromWatchState () bool {
242230 fw .mu .Lock ()
243231 ws := fw .watchState
244- wildcardDirs := fw .wildcardDirectories
245232 fw .mu .Unlock ()
246- return fw .hasChanges (ws , wildcardDirs )
233+ return fw .hasChanges (ws )
247234}
248235
249236func (fw * FileWatcher ) Run (now func () time.Time ) {
250237 for {
251238 fw .mu .Lock ()
252239 interval := fw .pollInterval
253240 ws := fw .watchState
254- wildcardDirs := fw .wildcardDirectories
241+ log := fw .debugLog
255242 fw .mu .Unlock ()
256243 time .Sleep (interval )
257- if ws == nil || fw .hasChanges (ws , wildcardDirs ) {
244+ start := now ()
245+ changed := ws == nil || fw .hasChanges (ws )
246+ if log != nil {
247+ elapsed := now ().Sub (start )
248+ files , dirs , missing := 0 , 0 , 0
249+ for _ , e := range ws {
250+ switch {
251+ case ! e .Exists :
252+ missing ++
253+ case e .ChildrenHash != 0 :
254+ dirs ++
255+ default :
256+ files ++
257+ }
258+ }
259+ fmt .Fprintf (log , "[vfswatch] scan: %d paths (%d files, %d dirs, %d missing), %.1fms, changed=%v\n " ,
260+ len (ws ), files , dirs , missing , float64 (elapsed .Microseconds ())/ 1000.0 , changed )
261+ }
262+ if changed {
258263 fw .WaitForSettled (now )
259264 fw .callback ()
260265 }
0 commit comments