1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
package api
// This file implements a polling file watcher for esbuild (i.e. it detects
// when files are changed by repeatedly checking their contents). Polling is
// used instead of more efficient platform-specific file system APIs because:
//
// * Go's standard library doesn't have built-in APIs for file watching
// * Using platform-specific APIs means using cgo, which I want to avoid
// * Polling is cross-platform and esbuild needs to work on 20+ platforms
// * Platform-specific APIs might be unreliable and could introduce bugs
//
// That said, this polling system is designed to use relatively little CPU vs.
// a more traditional polling system that scans the whole directory tree at
// once. The file system is still scanned regularly but each scan only checks
// a random subset of your files, which means a change to a file will be picked
// up soon after the change is made but not necessarily instantly.
//
// With the current heuristics, large projects should be completely scanned
// around every 2 seconds so in the worst case it could take up to 2 seconds
// for a change to be noticed. However, after a change has been noticed the
// change's path goes on a short list of recently changed paths which are
// checked on every scan, so further changes to recently changed files should
// be noticed almost instantly.
import (
"fmt"
"math/rand"
"os"
"sync"
"sync/atomic"
"time"
"github.com/evanw/esbuild/internal/fs"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/resolver"
)
// The time to wait between watch intervals
const watchIntervalSleep = 100 * time.Millisecond
// The maximum number of recently-edited items to check every interval
const maxRecentItemCount = 16
// The minimum number of non-recent items to check every interval
const minItemCountPerIter = 64
// The maximum number of intervals before a change is detected
const maxIntervalsBeforeUpdate = 20
type watcher struct {
data fs.WatchData
fs fs.FS
rebuild func() fs.WatchData
delayInMS time.Duration
recentItems []string
itemsToScan []string
mutex sync.Mutex
itemsPerIteration int
shouldStop int32
shouldLog bool
useColor logger.UseColor
pathStyle logger.PathStyle
stopWaitGroup sync.WaitGroup
}
func (w *watcher) setWatchData(data fs.WatchData) {
defer w.mutex.Unlock()
w.mutex.Lock()
// Print something for the end of the first build
if w.shouldLog && w.data.Paths == nil {
logger.PrintTextWithColor(os.Stderr, w.useColor, func(colors logger.Colors) string {
var delay string
if w.delayInMS > 0 {
delay = fmt.Sprintf(" with a %dms delay", w.delayInMS)
}
return fmt.Sprintf("%s[watch] build finished, watching for changes%s...%s\n", colors.Dim, delay, colors.Reset)
})
}
w.data = data
w.itemsToScan = w.itemsToScan[:0] // Reuse memory
// Remove any recent items that weren't a part of the latest build
end := 0
for _, path := range w.recentItems {
if data.Paths[path] != nil {
w.recentItems[end] = path
end++
}
}
w.recentItems = w.recentItems[:end]
}
func (w *watcher) start() {
w.stopWaitGroup.Add(1)
go func() {
// Note: Do not change these log messages without a breaking version change.
// People want to run regexes over esbuild's stderr stream to look for these
// messages instead of using esbuild's API.
for atomic.LoadInt32(&w.shouldStop) == 0 {
// Sleep for the watch interval
time.Sleep(watchIntervalSleep)
// Rebuild if we're dirty
if absPath := w.tryToFindDirtyPath(); absPath != "" {
// Optionally wait before rebuilding
if w.delayInMS > 0 {
time.Sleep(w.delayInMS * time.Millisecond)
}
if w.shouldLog {
logger.PrintTextWithColor(os.Stderr, w.useColor, func(colors logger.Colors) string {
prettyPaths := resolver.MakePrettyPaths(w.fs, logger.Path{Text: absPath, Namespace: "file"})
return fmt.Sprintf("%s[watch] build started (change: %q)%s\n",
colors.Dim, prettyPaths.Select(w.pathStyle), colors.Reset)
})
}
// Run the build
w.setWatchData(w.rebuild())
if w.shouldLog {
logger.PrintTextWithColor(os.Stderr, w.useColor, func(colors logger.Colors) string {
return fmt.Sprintf("%s[watch] build finished%s\n", colors.Dim, colors.Reset)
})
}
}
}
w.stopWaitGroup.Done()
}()
}
func (w *watcher) stop() {
atomic.StoreInt32(&w.shouldStop, 1)
w.stopWaitGroup.Wait()
}
func (w *watcher) tryToFindDirtyPath() string {
defer w.mutex.Unlock()
w.mutex.Lock()
// If we ran out of items to scan, fill the items back up in a random order
if len(w.itemsToScan) == 0 {
items := w.itemsToScan[:0] // Reuse memory
for path := range w.data.Paths {
items = append(items, path)
}
rand.Seed(time.Now().UnixNano())
for i := int32(len(items) - 1); i > 0; i-- { // Fisher-Yates shuffle
j := rand.Int31n(i + 1)
items[i], items[j] = items[j], items[i]
}
w.itemsToScan = items
// Determine how many items to check every iteration, rounded up
perIter := (len(items) + maxIntervalsBeforeUpdate - 1) / maxIntervalsBeforeUpdate
if perIter < minItemCountPerIter {
perIter = minItemCountPerIter
}
w.itemsPerIteration = perIter
}
// Always check all recent items every iteration
for i, path := range w.recentItems {
if dirtyPath := w.data.Paths[path](); dirtyPath != "" {
// Move this path to the back of the list (i.e. the "most recent" position)
copy(w.recentItems[i:], w.recentItems[i+1:])
w.recentItems[len(w.recentItems)-1] = path
return dirtyPath
}
}
// Check a constant number of items every iteration
remainingCount := len(w.itemsToScan) - w.itemsPerIteration
if remainingCount < 0 {
remainingCount = 0
}
toCheck, remaining := w.itemsToScan[remainingCount:], w.itemsToScan[:remainingCount]
w.itemsToScan = remaining
// Check if any of the entries in this iteration have been modified
for _, path := range toCheck {
if dirtyPath := w.data.Paths[path](); dirtyPath != "" {
// Mark this item as recent by adding it to the back of the list
w.recentItems = append(w.recentItems, path)
if len(w.recentItems) > maxRecentItemCount {
// Remove items from the front of the list when we hit the limit
copy(w.recentItems, w.recentItems[1:])
w.recentItems = w.recentItems[:maxRecentItemCount]
}
return dirtyPath
}
}
return ""
}
|