]> go.fuhry.dev Git - fsnotify.git/commitdiff
kqueue: Make watcher.Close() O(n) instead of O(n^2) (#233)
authorNick Santos <nick.santos@docker.com>
Thu, 21 Jul 2022 10:20:14 +0000 (11:20 +0100)
committerGitHub <noreply@github.com>
Thu, 21 Jul 2022 10:20:14 +0000 (12:20 +0200)
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.

kqueue.go

index 1b46acbd4e37fd10c008b8292b0fd15e86256365..87c25b062f42b59bc109ba53c65491b51174816d 100644 (file)
--- 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()
        }