Skip to content

Commit c08e077

Browse files
nicksndeloof
authored andcommitted
watch: optimization to help avoid inotify nodes for large file trees (docker#5769)
fixes tilt-dev/tilt#5764
1 parent cf31462 commit c08e077

File tree

2 files changed

+63
-6
lines changed

2 files changed

+63
-6
lines changed

pkg/watch/watcher_naive.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type naiveNotify struct {
2626
// Paths that we're watching that should be passed up to the caller.
2727
// Note that we may have to watch ancestors of these paths
2828
// in order to fulfill the API promise.
29+
//
30+
// We often need to check if paths are a child of a path in
31+
// the notify list. It might be better to store this in a tree
32+
// structure, so we can filter the list quickly.
2933
notifyList map[string]bool
3034

3135
ignore PathMatcher
@@ -226,7 +230,7 @@ func (d *naiveNotify) shouldNotify(path string) bool {
226230
}
227231
return true
228232
}
229-
// TODO(dmiller): maybe use a prefix tree here?
233+
230234
for root := range d.notifyList {
231235
if ospath.IsChild(root, path) {
232236
return true
@@ -245,7 +249,29 @@ func (d *naiveNotify) shouldSkipDir(path string) (bool, error) {
245249
if err != nil {
246250
return false, errors.Wrap(err, "shouldSkipDir")
247251
}
248-
return skip, nil
252+
253+
if skip {
254+
return true, nil
255+
}
256+
257+
// Suppose we're watching
258+
// /src/.tiltignore
259+
// but the .tiltignore file doesn't exist.
260+
//
261+
// Our watcher will create an inotify watch on /src/.
262+
//
263+
// But then we want to make sure we don't recurse from /src/ down to /src/node_modules.
264+
//
265+
// To handle this case, we only want to traverse dirs that are:
266+
// - A child of a directory that's in our notify list, or
267+
// - A parent of a directory that's in our notify list
268+
// (i.e., to cover the "path doesn't exist" case).
269+
for root := range d.notifyList {
270+
if ospath.IsChild(root, path) || ospath.IsChild(path, root) {
271+
return false, nil
272+
}
273+
}
274+
return true, nil
249275
}
250276

251277
func (d *naiveNotify) add(path string) error {

pkg/watch/watcher_naive_test.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import (
77
"fmt"
88
"os"
99
"os/exec"
10+
"path/filepath"
1011
"runtime"
1112
"strconv"
1213
"strings"
1314
"testing"
15+
16+
"github.com/stretchr/testify/require"
1417
)
1518

1619
func TestDontWatchEachFile(t *testing.T) {
@@ -96,20 +99,48 @@ func TestDontWatchEachFile(t *testing.T) {
9699
}
97100
f.events = nil
98101

102+
n, err := inotifyNodes()
103+
require.NoError(t, err)
104+
if n > 10 {
105+
t.Fatalf("watching more than 10 files: %d", n)
106+
}
107+
}
108+
109+
func inotifyNodes() (int, error) {
99110
pid := os.Getpid()
100111

101112
output, err := exec.Command("bash", "-c", fmt.Sprintf(
102113
"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
103114
if err != nil {
104-
t.Fatalf("error running command to determine number of watched files: %v", err)
115+
return 0, fmt.Errorf("error running command to determine number of watched files: %v", err)
105116
}
106117

107118
n, err := strconv.Atoi(strings.TrimSpace(string(output)))
108119
if err != nil {
109-
t.Fatalf("couldn't parse number of watched files: %v", err)
120+
return 0, fmt.Errorf("couldn't parse number of watched files: %v", err)
110121
}
122+
return n, nil
123+
}
111124

112-
if n > 10 {
113-
t.Fatalf("watching more than 10 files: %d", n)
125+
func TestDontRecurseWhenWatchingParentsOfNonExistentFiles(t *testing.T) {
126+
if runtime.GOOS != "linux" {
127+
t.Skip("This test uses linux-specific inotify checks")
128+
}
129+
130+
f := newNotifyFixture(t)
131+
132+
watched := f.TempDir("watched")
133+
f.watch(filepath.Join(watched, ".tiltignore"))
134+
135+
excludedDir := f.JoinPath(watched, "excluded")
136+
for i := 0; i < 10; i++ {
137+
f.WriteFile(f.JoinPath(excludedDir, fmt.Sprintf("%d", i), "data.txt"), "initial data")
138+
}
139+
f.fsync()
140+
141+
n, err := inotifyNodes()
142+
require.NoError(t, err)
143+
if n > 5 {
144+
t.Fatalf("watching more than 5 files: %d", n)
114145
}
115146
}

0 commit comments

Comments
 (0)