github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/filewatch/fsevent/coalesce.go (about)

     1  package fsevent
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/tilt-dev/tilt/internal/watch"
     7  )
     8  
     9  // BufferMinRestDuration bundles many file changes together by waiting a short amount of time after a file
    10  // change before emitting an event to the channel.
    11  //
    12  // 200ms is not the result of any kind of research or experimentation
    13  // it might end up being a significant part of deployment delay, if we get the total latency <2s
    14  // it might also be long enough that it misses some changes if the user has some operation involving a large file
    15  //
    16  //	(e.g., a binary dependency in git), but that's hopefully less of a problem since we'd get it in the next build
    17  const BufferMinRestDuration = 200 * time.Millisecond
    18  
    19  // BufferMaxDuration prevents excessive delays when bundling together file changes by emitting an event to the
    20  // channel if the threshold is reached even if new file changes are still coming in.
    21  const BufferMaxDuration = 10 * time.Second
    22  
    23  // Coalesce makes an attempt to read some events from `eventChan` so that multiple file changes
    24  // that happen at the same time from the user's perspective are grouped together.
    25  func Coalesce(timerMaker TimerMaker, eventChan <-chan watch.FileEvent) <-chan []watch.FileEvent {
    26  	ret := make(chan []watch.FileEvent)
    27  	go func() {
    28  		defer close(ret)
    29  
    30  		for {
    31  			event, ok := <-eventChan
    32  			if !ok {
    33  				return
    34  			}
    35  			events := []watch.FileEvent{event}
    36  
    37  			// keep grabbing changes until we've gone `BufferMinRestDuration` without seeing a change
    38  			minRestTimer := timerMaker(BufferMinRestDuration)
    39  
    40  			// but if we go too long before seeing a break (e.g., a process is constantly writing logs to that dir)
    41  			// then just send what we've got
    42  			timeout := timerMaker(BufferMaxDuration)
    43  
    44  			done := false
    45  			channelClosed := false
    46  			for !done && !channelClosed {
    47  				select {
    48  				case event, ok := <-eventChan:
    49  					if !ok {
    50  						channelClosed = true
    51  					} else {
    52  						minRestTimer = timerMaker(BufferMinRestDuration)
    53  						events = append(events, event)
    54  					}
    55  				case <-minRestTimer:
    56  					done = true
    57  				case <-timeout:
    58  					done = true
    59  				}
    60  			}
    61  			if len(events) > 0 {
    62  				ret <- events
    63  			}
    64  
    65  			if channelClosed {
    66  				return
    67  			}
    68  		}
    69  
    70  	}()
    71  	return ret
    72  }