diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 103c90483..bb9fc9874 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -603,6 +603,13 @@ func (g *Gui) SetRenderSearchStatusFunc(renderSearchStatusFunc func(*View, int, g.renderSearchStatusFunc = renderSearchStatusFunc } +// SetUpdateQueueHighWaterMarkHandler registers a diagnostic callback invoked +// with the new depth whenever the queue of pending Update callbacks reaches a +// new maximum. It may be called from any goroutine. +func (g *Gui) SetUpdateQueueHighWaterMarkHandler(f func(depth int)) { + g.userEvents.setHighWaterMarkHandler(f) +} + // userEvent represents an event triggered by the user. type userEvent struct { f func(*Gui) error @@ -639,6 +646,13 @@ type userEventQueue struct { mutex sync.Mutex events []userEvent doorbell chan struct{} + + // highWaterMark is the deepest the queue has ever been, and + // onHighWaterMark (if set) is called with the new depth each time that + // record is broken. Purely diagnostic: it lets us see how deep the queue + // gets in practice (see SetUpdateQueueHighWaterMarkHandler). + highWaterMark int + onHighWaterMark func(int) } func newUserEventQueue() *userEventQueue { @@ -649,14 +663,32 @@ func newUserEventQueue() *userEventQueue { func (q *userEventQueue) enqueue(ev userEvent) { q.mutex.Lock() q.events = append(q.events, ev) + newHighWaterMark := 0 + if len(q.events) > q.highWaterMark { + q.highWaterMark = len(q.events) + newHighWaterMark = q.highWaterMark + } + onHighWaterMark := q.onHighWaterMark q.mutex.Unlock() + // Report outside the lock: the handler does I/O (logging) and must not + // stall other producers or the draining loop. + if newHighWaterMark > 0 && onHighWaterMark != nil { + onHighWaterMark(newHighWaterMark) + } + select { case q.doorbell <- struct{}{}: default: } } +func (q *userEventQueue) setHighWaterMarkHandler(f func(int)) { + q.mutex.Lock() + q.onHighWaterMark = f + q.mutex.Unlock() +} + // dequeue pops the oldest event, reporting false when the queue is empty. func (q *userEventQueue) dequeue() (userEvent, bool) { q.mutex.Lock() diff --git a/pkg/gocui/user_event_queue_test.go b/pkg/gocui/user_event_queue_test.go index 10e86eef8..e547debb4 100644 --- a/pkg/gocui/user_event_queue_test.go +++ b/pkg/gocui/user_event_queue_test.go @@ -36,6 +36,37 @@ func TestUpdateIsUnboundedAndPreservesOrder(t *testing.T) { assert.Equal(t, want, got) } +// The high-water-mark handler fires only when the queue reaches a new maximum +// depth, reporting that depth. It does not reset when the queue drains. +func TestUpdateQueueHighWaterMark(t *testing.T) { + g := newTestGui(t) + + var marks []int + g.SetUpdateQueueHighWaterMarkHandler(func(depth int) { marks = append(marks, depth) }) + + noop := func(*Gui) error { return nil } + + // Three enqueues with no drain: new highs 1, 2, 3. + g.Update(noop) + g.Update(noop) + g.Update(noop) + _, err := g.processRemainingEvents() + assert.NoError(t, err) + + // Two enqueues stay below the previous high of 3: no new marks. + g.Update(noop) + g.Update(noop) + _, err = g.processRemainingEvents() + assert.NoError(t, err) + + // Four enqueues with no drain: only depth 4 beats the previous high. + for range 4 { + g.Update(noop) + } + + assert.Equal(t, []int{1, 2, 3, 4}, marks) +} + // Concurrent producers must be able to enqueue safely (run under -race). Only // same-goroutine order is guaranteed, so we check that every event is delivered // exactly once and that each producer's own events stay in order. diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index e23afd124..61d4a90e2 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -414,6 +414,10 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context return nil }) + gui.g.SetUpdateQueueHighWaterMarkHandler(func(depth int) { + gui.c.Log.Infof("User-event queue reached a new high-water mark: %d", depth) + }) + gui.g.SetOnSelectSearchResultFunc(func(v *gocui.View, selectedLineIdx int) { ctx, ok := gui.helpers.View.ContextForView(v.Name()) if ok {