From 969ac6299e75878ca88d9aa00e82e1a9822973b0 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 14 Jan 2018 22:15:53 -0800 Subject: [PATCH] Avoid firing event if no files changed If the changes that triggered the event were only to known files, and all of those files have the same hashes as when we began, we shouldn't actually reload the command. This optimization does *not* short circuit command reload if any of these conditions hold: - a changed file is larger than 20MB, e.g. changes to these files always trigger a reload. - more than 500 files changed - a directory or an unknown file was changed. Fixes jmhodges/justrun#23. --- justrun.go | 36 +++++++++++++++++++++++++++++++++++- watch.go | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/justrun.go b/justrun.go index 7f6d652..491e916 100644 --- a/justrun.go +++ b/justrun.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "bytes" "errors" "flag" "fmt" @@ -77,7 +78,7 @@ func main() { go waitForInterrupt(sigCh, cmd) cmdCh := make(chan event, 100) - _, err := watch(inputPaths, ignoreFlag, cmdCh) + userPaths, err := watch(inputPaths, ignoreFlag, cmdCh) if err != nil { log.Fatal(err) } @@ -85,6 +86,8 @@ func main() { wasDelayed := false lastStartTime := time.Now() + changedFiles := make([]string, 0) + mustChange := false cmd.Reload() tick := time.NewTicker(*delayDur) for { @@ -93,6 +96,11 @@ func main() { if !ok { return } + if h, ok := userPaths[ev.Event.Name]; !ok || h == nil { + mustChange = true + } else { + changedFiles = append(changedFiles, ev.Event.Name) + } if lastStartTime.After(ev.Time) { continue } @@ -104,6 +112,8 @@ func main() { continue } wasDelayed = false + mustChange = false + changedFiles = changedFiles[:0] lastStartTime = time.Now() cmd.Reload() tick.Stop() @@ -112,6 +122,30 @@ func main() { if wasDelayed { wasDelayed = false lastStartTime = time.Now() + if mustChange == false && len(changedFiles) < 512 { + allEqual := true + // check hashes. + for i := range changedFiles { + d, err := digest(changedFiles[i]) + if err != nil { + allEqual = false + break + } + if !bytes.Equal(d, userPaths[changedFiles[i]]) { + // store the new digest, keep searching so we get + // new digest for all files. + userPaths[changedFiles[i]] = d + allEqual = false + } + } + mustChange = false + l := len(changedFiles) + changedFiles = changedFiles[:0] + if allEqual { + log.Printf("%d files changed but all have same contents, continuing", l) + continue + } + } cmd.Reload() } } diff --git a/watch.go b/watch.go index 0f262cb..8d58510 100644 --- a/watch.go +++ b/watch.go @@ -1,9 +1,13 @@ package main import ( + "bufio" + "crypto/sha256" "errors" "fmt" + "io" "log" + "os" "path/filepath" "strings" "time" @@ -11,9 +15,37 @@ import ( "github.com/fsnotify/fsnotify" ) +const maxHashedFileSize = 20 * 1024 * 1024 + +func digest(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + stat, err := f.Stat() + if err != nil { + return nil, err + } + if stat.IsDir() { + return nil, nil + } + h := sha256.New() + // TODO large file check. + br := bufio.NewReader(f) + if _, err := io.Copy(h, io.LimitReader(br, maxHashedFileSize)); err != nil { + return nil, err + } + if b, _ := br.Peek(1); len(b) > 0 { + // too big + return nil, nil + } + return h.Sum(nil), nil +} + // watch watches the input paths. The returned Watcher should only be used in // tests. -func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Watcher, error) { +func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (map[string][]byte, error) { // Creates an Ignorer that just ignores file paths the user // specifically asked to be ignored. ui, err := createUserIgnorer(ignoredPaths) @@ -29,7 +61,7 @@ func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Wat // Watch user-specified paths and create a set of them for walking // later. Paths that are both asked to be watched and ignored by // the user are ignored. - userPaths := make(map[string]bool) + userPaths := make(map[string][]byte) includedHiddenFiles := make(map[string]bool) for _, path := range inputPaths { fullPath, err := filepath.Abs(path) @@ -37,7 +69,8 @@ func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Wat w.Close() return nil, errors.New("unable to get current working directory while working with user-watched paths") } - if userPaths[fullPath] || ui.IsIgnored(path) { + _, found := userPaths[fullPath] + if found || ui.IsIgnored(path) { continue } err = w.Add(fullPath) @@ -45,7 +78,8 @@ func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Wat w.Close() return nil, fmt.Errorf("unable to watch '%s': %s", path, err) } - userPaths[fullPath] = true + d, _ := digest(fullPath) + userPaths[fullPath] = d } // Create some useful sets from the user-specified paths to be @@ -73,7 +107,8 @@ func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Wat } dirPath := filepath.Dir(fullPath) - if !userPaths[dirPath] && dirPath != "" { + _, foundDir := userPaths[dirPath] + if !foundDir && dirPath != "" { if !renameDirs[dirPath] { err = w.Add(dirPath) if err != nil { @@ -93,7 +128,7 @@ func watch(inputPaths, ignoredPaths []string, cmdCh chan<- event) (*fsnotify.Wat } go listenForEvents(w, cmdCh, ig) - return w, nil + return userPaths, nil } type event struct {