diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go index d377fd0bca0..2530d7cfd28 100644 --- a/pkg/guestagent/guestagent_linux.go +++ b/pkg/guestagent/guestagent_linux.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "reflect" + "time" "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" @@ -48,13 +49,11 @@ func New(ctx context.Context, ticker ticker.Ticker, runtimeDir string) (Agent, e var _ Agent = (*agent)(nil) type agent struct { - // Ticker is like time.Ticker. - // We can't use inotify for /proc/net/tcp, so we need this ticker to - // reload /proc/net/tcp. ticker ticker.Ticker socketLister *sockets.Lister kubernetesServiceWatcher *kubernetesservice.ServiceWatcher runtimeDir string + recentChtimes map[string]time.Time } type eventState struct { @@ -211,13 +210,39 @@ func (a *agent) Info(ctx context.Context) (*api.Info, error) { func (a *agent) HandleInotify(event *api.Inotify) { location := event.MountPath - if _, err := os.Stat(location); err == nil { - local := event.Time.AsTime().Local() - err := os.Chtimes(location, local, local) - if err != nil { - logrus.Errorf("error in inotify handle. Event: %s, Error: %s", event, err) + fi, err := os.Stat(location) + if err != nil { + return + } + if fi.IsDir() { + return + } + + now := time.Now() + + // If we called Chtimes on this path recently, this is an echo: + // our Chtimes → virtiofs → macOS FSEvents → host agent → back here. + // Skip to prevent reload loops and reduce virtiofs traffic. + if lastTouch, ok := a.recentChtimes[location]; ok { + if now.Sub(lastTouch) < time.Second { + return } } + + local := event.Time.AsTime().Local() + if err := os.Chtimes(location, local, local); err != nil { + logrus.Errorf("error in inotify handle. Event: %s, Error: %s", event, err) + } + + if a.recentChtimes == nil { + a.recentChtimes = make(map[string]time.Time) + } + a.recentChtimes[location] = now + + // Prevent unbounded growth during bulk operations + if len(a.recentChtimes) > 10000 { + a.recentChtimes = make(map[string]time.Time) + } } func (a *agent) Close() error { diff --git a/pkg/hostagent/inotify.go b/pkg/hostagent/inotify.go index 3971b0cf4cb..3e3b352a7b2 100644 --- a/pkg/hostagent/inotify.go +++ b/pkg/hostagent/inotify.go @@ -9,6 +9,7 @@ import ( "path" "path/filepath" "strings" + "time" "github.com/rjeczalik/notify" "github.com/sirupsen/logrus" @@ -40,6 +41,15 @@ func (a *HostAgent) startInotify(ctx context.Context) error { return err } + // Trailing-edge debounce: accumulate events and only flush after + // a quiet period. During bulk operations (yarn install, nuxt build) + // the timer keeps resetting so no events are forwarded until the + // burst settles — preventing virtiofs virtqueue contention. + const quietPeriod = 200 * time.Millisecond + timer := time.NewTimer(quietPeriod) + timer.Stop() + pending := make(map[string]os.FileInfo) + for { select { case <-ctx.Done(): @@ -50,19 +60,25 @@ func (a *HostAgent) startInotify(ctx context.Context) error { if err != nil { continue } - + if stat.IsDir() { + continue + } if filterEvents(watchEvent, stat) { continue } - - watchPath = translateToGuestPath(watchPath, mountSymlinks, mountLocations) - - utcTimestamp := timestamppb.New(stat.ModTime().UTC()) - event := &guestagentapi.Inotify{MountPath: watchPath, Time: utcTimestamp} - err = inotifyClient.Send(event) - if err != nil { - logrus.WithError(err).Warn("failed to send inotify") + pending[watchPath] = stat + timer.Reset(quietPeriod) + + case <-timer.C: + for wp, st := range pending { + guestPath := translateToGuestPath(wp, mountSymlinks, mountLocations) + utcTimestamp := timestamppb.New(st.ModTime().UTC()) + event := &guestagentapi.Inotify{MountPath: guestPath, Time: utcTimestamp} + if err := inotifyClient.Send(event); err != nil { + logrus.WithError(err).Warn("failed to send inotify") + } } + pending = make(map[string]os.FileInfo) } } }