detached 778b92a

perf(daemon): avoid inert coordinator timer allocations

commit
778b92a4f2c3646df65b2a5f1751bb6b55ab2b71
author
brice · · 2026-07-11 18:08 UTC
parent
eca5562
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +35 -22
M internal/usecase/daemon/render_coordinator.go +10 -8
@@ -211,6 +211,15 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
211211 return
212212 }
213213 timer := clock.NewTimer(delay)
214+ timerC := timer.C()
215+ // A nil timer channel is inert, so it cannot need cancellation. Complete
216+ // synchronously before allocating a worker or its coordination channels.
217+ if timerC == nil {
218+ c.mu.Unlock()
219+ timer.Stop()
220+ c.fire(gen, false)
221+ return
222+ }
214223 cancel := make(chan struct{})
215224 done := make(chan struct{})
216225 c.normalTimer, c.normalCancel, c.normalWorkerDone = timer, cancel, done
@@ -218,18 +227,11 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
218227 go func() {
219228 defer close(done)
220229 select {
221- case <-timer.C():
230+ case <-timerC:
222231 c.fire(gen, false)
223232 case <-cancel:
224233 }
225234 }()
226- // A nil timer channel is an inert test clock, not a schedulable deadline.
227- // Complete through the coordinator so manual daemon fixtures retain their
228- // deterministic synchronous contract without a direct paint fallback.
229- if timer.C() == nil {
230- c.fire(gen, false)
231- return
232- }
233235 // Give fake-clock callbacks a chance to block on C before the test
234236 // advances it; real clocks are unaffected.
235237 runtime.Gosched()
@@ -211,6 +211,15 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
211 return211 return
212 }212 }
213 timer := clock.NewTimer(delay)213 timer := clock.NewTimer(delay)
214+ timerC := timer.C()
215+ // A nil timer channel is inert, so it cannot need cancellation. Complete
216+ // synchronously before allocating a worker or its coordination channels.
217+ if timerC == nil {
218+ c.mu.Unlock()
219+ timer.Stop()
220+ c.fire(gen, false)
221+ return
222+ }
214 cancel := make(chan struct{})223 cancel := make(chan struct{})
215 done := make(chan struct{})224 done := make(chan struct{})
216 c.normalTimer, c.normalCancel, c.normalWorkerDone = timer, cancel, done225 c.normalTimer, c.normalCancel, c.normalWorkerDone = timer, cancel, done
@@ -218,18 +227,11 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
218 go func() {227 go func() {
219 defer close(done)228 defer close(done)
220 select {229 select {
221- case <-timer.C():230+ case <-timerC:
222 c.fire(gen, false)231 c.fire(gen, false)
223 case <-cancel:232 case <-cancel:
224 }233 }
225 }()234 }()
226- // A nil timer channel is an inert test clock, not a schedulable deadline.
227- // Complete through the coordinator so manual daemon fixtures retain their
228- // deterministic synchronous contract without a direct paint fallback.
229- if timer.C() == nil {
230- c.fire(gen, false)
231- return
232- }
233 // Give fake-clock callbacks a chance to block on C before the test235 // Give fake-clock callbacks a chance to block on C before the test
234 // advances it; real clocks are unaffected.236 // advances it; real clocks are unaffected.
235 runtime.Gosched()237 runtime.Gosched()
M internal/usecase/daemon/render_coordinator_test.go +25 -14
@@ -814,7 +814,31 @@ func requireWorkerExit(t *testing.T, done <-chan struct{}) {
814814 t.Fatal("cancelled coordinator timer worker did not exit")
815815 }
816816
817-func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
817+func TestRenderCoordinatorInertTimerFiresSynchronouslyWithoutWorker(t *testing.T) {
818+ clock := portsmocks.NewMockClock(t)
819+ timer := portsmocks.NewMockTimer(t)
820+ clock.EXPECT().NewTimer(minOutputRenderDeadline).Return(timer).Once()
821+ timer.EXPECT().C().Return((<-chan time.Time)(nil)).Once()
822+ timer.EXPECT().Stop().Return(true).Once()
823+
824+ wakes := make(chan renderWake, 1)
825+ rc := newRenderCoordinator(renderCoordinatorOptions{
826+ clock: clock,
827+ ackReady: func() bool { return true },
828+ wake: func(w renderWake) { wakes <- w },
829+ })
830+ rc.invalidate(renderInvalidation{class: invalidateOutput})
831+
832+ require.Equal(t, renderWake{coalesced: 1}, <-wakes)
833+ require.Equal(t, renderCoordinatorBurstMetricsSnapshot{invalidations: 1, wakes: 1, coalesced: 1}, rc.burstMetricsSnapshot())
834+ rc.mu.Lock()
835+ require.Nil(t, rc.normalTimer)
836+ require.Nil(t, rc.normalCancel)
837+ require.Nil(t, rc.normalWorkerDone)
838+ rc.mu.Unlock()
839+}
840+
841+func TestRenderCoordinatorStopsInertSyncTimerWorkers(t *testing.T) {
818842 lifecycle := []struct {
819843 name string
820844 end func(*renderCoordinator, *attachedClient)
@@ -829,19 +853,6 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
829853 rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
830854 ac := &attachedClient{}
831855 rc.attach(ac)
832- rc.invalidate(renderInvalidation{class: invalidateOutput})
833- normal := <-clk.timers
834- rc.mu.Lock()
835- normalDone := rc.normalWorkerDone
836- rc.mu.Unlock()
837- tc.end(rc, ac)
838- normal.mock.AssertNumberOfCalls(t, "Stop", 1)
839- requireWorkerExit(t, normalDone)
840-
841- // Use a fresh coordinator because detach/teardown intentionally makes
842- // an attachment ineligible for a new synchronized batch.
843- rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
844- rc.attach(ac)
845856 rc.noteSyncBegin(1)
846857 syncTimer := <-clk.timers
847858 rc.mu.Lock()
@@ -814,7 +814,31 @@ func requireWorkerExit(t *testing.T, done <-chan struct{}) {
814 t.Fatal("cancelled coordinator timer worker did not exit")814 t.Fatal("cancelled coordinator timer worker did not exit")
815 }815 }
816 816
817-func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {817+func TestRenderCoordinatorInertTimerFiresSynchronouslyWithoutWorker(t *testing.T) {
818+ clock := portsmocks.NewMockClock(t)
819+ timer := portsmocks.NewMockTimer(t)
820+ clock.EXPECT().NewTimer(minOutputRenderDeadline).Return(timer).Once()
821+ timer.EXPECT().C().Return((<-chan time.Time)(nil)).Once()
822+ timer.EXPECT().Stop().Return(true).Once()
823+
824+ wakes := make(chan renderWake, 1)
825+ rc := newRenderCoordinator(renderCoordinatorOptions{
826+ clock: clock,
827+ ackReady: func() bool { return true },
828+ wake: func(w renderWake) { wakes <- w },
829+ })
830+ rc.invalidate(renderInvalidation{class: invalidateOutput})
831+
832+ require.Equal(t, renderWake{coalesced: 1}, <-wakes)
833+ require.Equal(t, renderCoordinatorBurstMetricsSnapshot{invalidations: 1, wakes: 1, coalesced: 1}, rc.burstMetricsSnapshot())
834+ rc.mu.Lock()
835+ require.Nil(t, rc.normalTimer)
836+ require.Nil(t, rc.normalCancel)
837+ require.Nil(t, rc.normalWorkerDone)
838+ rc.mu.Unlock()
839+}
840+
841+func TestRenderCoordinatorStopsInertSyncTimerWorkers(t *testing.T) {
818 lifecycle := []struct {842 lifecycle := []struct {
819 name string843 name string
820 end func(*renderCoordinator, *attachedClient)844 end func(*renderCoordinator, *attachedClient)
@@ -829,19 +853,6 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
829 rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})853 rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
830 ac := &attachedClient{}854 ac := &attachedClient{}
831 rc.attach(ac)855 rc.attach(ac)
832- rc.invalidate(renderInvalidation{class: invalidateOutput})
833- normal := <-clk.timers
834- rc.mu.Lock()
835- normalDone := rc.normalWorkerDone
836- rc.mu.Unlock()
837- tc.end(rc, ac)
838- normal.mock.AssertNumberOfCalls(t, "Stop", 1)
839- requireWorkerExit(t, normalDone)
840-
841- // Use a fresh coordinator because detach/teardown intentionally makes
842- // an attachment ineligible for a new synchronized batch.
843- rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
844- rc.attach(ac)
845 rc.noteSyncBegin(1)856 rc.noteSyncBegin(1)
846 syncTimer := <-clk.timers857 syncTimer := <-clk.timers
847 rc.mu.Lock()858 rc.mu.Lock()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close