From: Nick Santos Date: Thu, 21 Jul 2022 10:20:14 +0000 (+0100) Subject: kqueue: Make watcher.Close() O(n) instead of O(n^2) (#233) X-Git-Tag: v1.7.2~110 X-Git-Url: https://go.fuhry.dev/?a=commitdiff_plain;h=6ae56b78f545a5be2fc9e9bb6013443a35716fc4;p=fsnotify.git kqueue: Make watcher.Close() O(n) instead of O(n^2) (#233) In the old implementation watcher.Close() would clone the list of watches and then run Remove() on that, and every Remove() call would iterate over the full list of watches. This stores the watches more efficiently so that Remove() doesn't need to iterate over the full list: instead of merely a path → fd map, it also stores a parent-path → list-of-files. No functional changes, just a performance improvement. --- diff --git a/kqueue.go b/kqueue.go index 1b46acb..87c25b0 100644 --- a/kqueue.go +++ b/kqueue.go @@ -27,13 +27,14 @@ type Watcher struct { kq int // File descriptor (as returned by the kqueue() syscall). - mu sync.Mutex // Protects access to watcher data - watches map[string]int // Map of watched file descriptors (key: path). - externalWatches map[string]bool // Map of watches added by user of the library. - dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue. - paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events. - fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). - isClosed bool // Set to true when Close() is first called + mu sync.Mutex // Protects access to watcher data + watches map[string]int // Map of watched file descriptors (key: path). + watchesByDir map[string]map[int]struct{} // Map of watched file descriptors indexed by the parent directory (key: dirname(path)). + externalWatches map[string]bool // Map of watches added by user of the library. + dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue. + paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events. + fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). + isClosed bool // Set to true when Close() is first called } type pathInfo struct { @@ -51,6 +52,7 @@ func NewWatcher() (*Watcher, error) { w := &Watcher{ kq: kq, watches: make(map[string]int), + watchesByDir: make(map[string]map[int]struct{}), dirFlags: make(map[string]uint32), paths: make(map[int]pathInfo), fileExists: make(map[string]bool), @@ -120,6 +122,14 @@ func (w *Watcher) Remove(name string) error { w.mu.Lock() isDir := w.paths[watchfd].isDir delete(w.watches, name) + + parentName := filepath.Dir(name) + delete(w.watchesByDir[parentName], watchfd) + + if len(w.watchesByDir[parentName]) == 0 { + delete(w.watchesByDir, parentName) + } + delete(w.paths, watchfd) delete(w.dirFlags, name) w.mu.Unlock() @@ -128,12 +138,10 @@ func (w *Watcher) Remove(name string) error { if isDir { var pathsToRemove []string w.mu.Lock() - for _, path := range w.paths { - wdir, _ := filepath.Split(path.name) - if filepath.Clean(wdir) == name { - if !w.externalWatches[path.name] { - pathsToRemove = append(pathsToRemove, path.name) - } + for fd := range w.watchesByDir[name] { + path := w.paths[fd] + if !w.externalWatches[path.name] { + pathsToRemove = append(pathsToRemove, path.name) } } w.mu.Unlock() @@ -245,7 +253,16 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { if !alreadyWatching { w.mu.Lock() + parentName := filepath.Dir(name) w.watches[name] = watchfd + + watchesByDir, ok := w.watchesByDir[parentName] + if !ok { + watchesByDir = make(map[int]struct{}, 1) + w.watchesByDir[parentName] = watchesByDir + } + watchesByDir[watchfd] = struct{}{} + w.paths[watchfd] = pathInfo{name: name, isDir: isDir} w.mu.Unlock() }