detached de6d25b

perf(daemon): report render coordinator burst metrics

commit
de6d25bb520899680d766ba2fe463c520b35fc2a
author
brice · · 2026-07-11 17:49 UTC
parent
4d15883
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +102 -29
M internal/usecase/daemon/performance_benchmark_test.go +84 -29
@@ -51,6 +51,10 @@ func TestPerformanceFixtureCounters(t *testing.T) {
5151 require.Equal(t, uint64(2), metrics.outputFrames)
5252 require.Positive(t, metrics.outputBytes)
5353 require.Positive(t, metrics.outputPayloadBytes)
54+ require.Equal(t, uint64(2), metrics.coordinatorInvalidations)
55+ require.Equal(t, uint64(2), metrics.coordinatorWakes)
56+ require.Equal(t, uint64(2), metrics.coordinatorCoalesced)
57+ require.Positive(t, metrics.coordinatorInvalidations, "coordinator metric ratios require a nonzero denominator")
5458
5559 fixture.captureSnapshot()
5660 metrics = fixture.metrics()
@@ -86,6 +90,35 @@ func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
8690 }
8791 }
8892
93+func TestPerformanceFixtureCoordinatorMetricsForCanonicalTopologies(t *testing.T) {
94+ for _, topology := range daemonHistoryTopologies {
95+ t.Run(topology.name, func(t *testing.T) {
96+ fixture := newPerformanceFixture(t, performanceConfig{
97+ size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
98+ })
99+ fixture.paintLive()
100+ metrics := fixture.metrics()
101+ require.Equal(t, uint64(1), metrics.coordinatorInvalidations)
102+ require.Equal(t, uint64(1), metrics.coordinatorWakes)
103+ require.Equal(t, uint64(1), metrics.coordinatorCoalesced)
104+ require.Positive(t, metrics.coordinatorInvalidations, "coalescing ratio denominator")
105+ })
106+ }
107+}
108+
109+type daemonHistoryTopology struct {
110+ name string
111+ tabs, panes int
112+}
113+
114+var daemonHistoryTopologies = []daemonHistoryTopology{
115+ {name: "1tab-1pane-control", tabs: 1, panes: 1},
116+ {name: "1tab-4panes", tabs: 1, panes: 4},
117+ {name: "4tabs-1pane", tabs: 4, panes: 1},
118+ {name: "4tabs-4panes", tabs: 4, panes: 4},
119+ {name: "8tabs-1pane", tabs: 8, panes: 1},
120+}
121+
89122 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
90123 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
91124 }
@@ -104,16 +137,7 @@ func BenchmarkDaemonHistoryResize(b *testing.B) {
104137
105138 func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*performanceFixture)) {
106139 b.Helper()
107- for _, topology := range []struct {
108- name string
109- tabs, panes int
110- }{
111- {name: "1tab-1pane-control", tabs: 1, panes: 1},
112- {name: "1tab-4panes", tabs: 1, panes: 4},
113- {name: "4tabs-1pane", tabs: 4, panes: 1},
114- {name: "4tabs-4panes", tabs: 4, panes: 4},
115- {name: "8tabs-1pane", tabs: 8, panes: 1},
116- } {
140+ for _, topology := range daemonHistoryTopologies {
117141 b.Run(topology.name, func(b *testing.B) {
118142 fixture := newPerformanceFixture(b, performanceConfig{
119143 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
@@ -164,6 +188,14 @@ func benchmarkReportMetrics(b *testing.B, metrics performanceMetrics, operations
164188 b.ReportMetric(float64(metrics.outputPayloadBytes)/perOperation, "framepayloadbytes/op")
165189 b.ReportMetric(float64(metrics.snapshotWrites)/perOperation, "snapshotwrites/op")
166190 b.ReportMetric(float64(metrics.snapshotBytes)/perOperation, "snapshotbytes/op")
191+ b.ReportMetric(float64(metrics.coordinatorInvalidations)/perOperation, "coordinatorinvalidations/op")
192+ b.ReportMetric(float64(metrics.coordinatorWakes)/perOperation, "coordinatorwakes/op")
193+ b.ReportMetric(float64(metrics.coordinatorCoalesced)/perOperation, "coordinatorcoalesced/op")
194+ if metrics.coordinatorInvalidations > 0 {
195+ b.ReportMetric(float64(metrics.coordinatorCoalesced)/float64(metrics.coordinatorInvalidations), "coordinatorcoalescingratio")
196+ } else {
197+ b.ReportMetric(0, "coordinatorcoalescingratio")
198+ }
167199 b.ReportMetric(float64(historyRows), "historyrows/pane")
168200 }
169201
@@ -192,26 +224,30 @@ type performanceConfig struct {
192224 }
193225
194226 type performanceMetrics struct {
195- outputFrames uint64
196- outputBytes uint64
197- outputPayloadBytes uint64
198- snapshotWrites uint64
199- snapshotBytes uint64
227+ outputFrames uint64
228+ outputBytes uint64
229+ outputPayloadBytes uint64
230+ snapshotWrites uint64
231+ snapshotBytes uint64
232+ coordinatorInvalidations uint64
233+ coordinatorWakes uint64
234+ coordinatorCoalesced uint64
200235 }
201236
202237 type performanceFixture struct {
203- t testing.TB
204- d *Daemon
205- sess *session
206- ac *attachedClient
207- output *countingOutputTransport
208- snaps *countingSnapshotStore
209- activePane *pane
210- liveWrites [][]byte
211- paints int
212- resizeSizes [2]domain.Size
213- resizes int
214- resizedSize domain.Size
238+ t testing.TB
239+ d *Daemon
240+ sess *session
241+ ac *attachedClient
242+ output *countingOutputTransport
243+ snaps *countingSnapshotStore
244+ activePane *pane
245+ liveWrites [][]byte
246+ paints int
247+ resizeSizes [2]domain.Size
248+ resizes int
249+ resizedSize domain.Size
250+ coordinatorBaseline renderCoordinatorBurstMetricsSnapshot
215251 }
216252
217253 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {
@@ -253,6 +289,11 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
253289 }
254290 fixture.activePane = fixture.findActivePane()
255291
292+ // The fixture uses the production coordinator. stubClock's inert timer is
293+ // completed synchronously by the coordinator, keeping benchmark operations
294+ // deterministic while retaining their real invalidation path.
295+ d.attachCoordinator(sess, nil, ac)
296+
256297 // Prime the real renderer shadow before measurements. Subsequent paints use
257298 // actual production diffs rather than the initial full frame.
258299 d.paint(sess, ac, true)
@@ -311,7 +352,11 @@ func (f *performanceFixture) paintLive() {
311352 p.mu.Lock()
312353 p.screen.Write(f.liveWrites[f.paints%len(f.liveWrites)])
313354 p.mu.Unlock()
314- f.d.paint(f.sess, f.ac, false)
355+ if rc := f.sess.renderCoordinator(); rc != nil {
356+ rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "performance_benchmark_test.go"})
357+ } else {
358+ f.d.paint(f.sess, f.ac, false)
359+ }
315360 f.paints++
316361 }
317362
@@ -359,15 +404,25 @@ func (f *performanceFixture) resized() bool {
359404 func (f *performanceFixture) resetMetrics() {
360405 f.output.reset()
361406 f.snaps.reset()
407+ if rc := f.sess.renderCoordinator(); rc != nil {
408+ f.coordinatorBaseline = rc.burstMetricsSnapshot()
409+ }
362410 }
363411
364412 func (f *performanceFixture) metrics() performanceMetrics {
365413 output := f.output.metrics()
366414 snapshots := f.snaps.metrics()
367- return performanceMetrics{
415+ metrics := performanceMetrics{
368416 outputFrames: output.frames, outputBytes: output.bytes, outputPayloadBytes: output.payloadBytes,
369417 snapshotWrites: snapshots.writes, snapshotBytes: snapshots.bytes,
370418 }
419+ if rc := f.sess.renderCoordinator(); rc != nil {
420+ coordinator := rc.burstMetricsSnapshot()
421+ metrics.coordinatorInvalidations = coordinator.invalidations - f.coordinatorBaseline.invalidations
422+ metrics.coordinatorWakes = coordinator.wakes - f.coordinatorBaseline.wakes
423+ metrics.coordinatorCoalesced = coordinator.coalesced - f.coordinatorBaseline.coalesced
424+ }
425+ return metrics
371426 }
372427
373428 const outputPayloadHeaderBytes = 24
@@ -51,6 +51,10 @@ func TestPerformanceFixtureCounters(t *testing.T) {
51 require.Equal(t, uint64(2), metrics.outputFrames)51 require.Equal(t, uint64(2), metrics.outputFrames)
52 require.Positive(t, metrics.outputBytes)52 require.Positive(t, metrics.outputBytes)
53 require.Positive(t, metrics.outputPayloadBytes)53 require.Positive(t, metrics.outputPayloadBytes)
54+ require.Equal(t, uint64(2), metrics.coordinatorInvalidations)
55+ require.Equal(t, uint64(2), metrics.coordinatorWakes)
56+ require.Equal(t, uint64(2), metrics.coordinatorCoalesced)
57+ require.Positive(t, metrics.coordinatorInvalidations, "coordinator metric ratios require a nonzero denominator")
54 58
55 fixture.captureSnapshot()59 fixture.captureSnapshot()
56 metrics = fixture.metrics()60 metrics = fixture.metrics()
@@ -86,6 +90,35 @@ func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
86 }90 }
87 }91 }
88 92
93+func TestPerformanceFixtureCoordinatorMetricsForCanonicalTopologies(t *testing.T) {
94+ for _, topology := range daemonHistoryTopologies {
95+ t.Run(topology.name, func(t *testing.T) {
96+ fixture := newPerformanceFixture(t, performanceConfig{
97+ size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
98+ })
99+ fixture.paintLive()
100+ metrics := fixture.metrics()
101+ require.Equal(t, uint64(1), metrics.coordinatorInvalidations)
102+ require.Equal(t, uint64(1), metrics.coordinatorWakes)
103+ require.Equal(t, uint64(1), metrics.coordinatorCoalesced)
104+ require.Positive(t, metrics.coordinatorInvalidations, "coalescing ratio denominator")
105+ })
106+ }
107+}
108+
109+type daemonHistoryTopology struct {
110+ name string
111+ tabs, panes int
112+}
113+
114+var daemonHistoryTopologies = []daemonHistoryTopology{
115+ {name: "1tab-1pane-control", tabs: 1, panes: 1},
116+ {name: "1tab-4panes", tabs: 1, panes: 4},
117+ {name: "4tabs-1pane", tabs: 4, panes: 1},
118+ {name: "4tabs-4panes", tabs: 4, panes: 4},
119+ {name: "8tabs-1pane", tabs: 8, panes: 1},
120+}
121+
89 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {122 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
90 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })123 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
91 }124 }
@@ -104,16 +137,7 @@ func BenchmarkDaemonHistoryResize(b *testing.B) {
104 137
105 func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*performanceFixture)) {138 func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*performanceFixture)) {
106 b.Helper()139 b.Helper()
107- for _, topology := range []struct {140+ for _, topology := range daemonHistoryTopologies {
108- name string
109- tabs, panes int
110- }{
111- {name: "1tab-1pane-control", tabs: 1, panes: 1},
112- {name: "1tab-4panes", tabs: 1, panes: 4},
113- {name: "4tabs-1pane", tabs: 4, panes: 1},
114- {name: "4tabs-4panes", tabs: 4, panes: 4},
115- {name: "8tabs-1pane", tabs: 8, panes: 1},
116- } {
117 b.Run(topology.name, func(b *testing.B) {141 b.Run(topology.name, func(b *testing.B) {
118 fixture := newPerformanceFixture(b, performanceConfig{142 fixture := newPerformanceFixture(b, performanceConfig{
119 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,143 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
@@ -164,6 +188,14 @@ func benchmarkReportMetrics(b *testing.B, metrics performanceMetrics, operations
164 b.ReportMetric(float64(metrics.outputPayloadBytes)/perOperation, "framepayloadbytes/op")188 b.ReportMetric(float64(metrics.outputPayloadBytes)/perOperation, "framepayloadbytes/op")
165 b.ReportMetric(float64(metrics.snapshotWrites)/perOperation, "snapshotwrites/op")189 b.ReportMetric(float64(metrics.snapshotWrites)/perOperation, "snapshotwrites/op")
166 b.ReportMetric(float64(metrics.snapshotBytes)/perOperation, "snapshotbytes/op")190 b.ReportMetric(float64(metrics.snapshotBytes)/perOperation, "snapshotbytes/op")
191+ b.ReportMetric(float64(metrics.coordinatorInvalidations)/perOperation, "coordinatorinvalidations/op")
192+ b.ReportMetric(float64(metrics.coordinatorWakes)/perOperation, "coordinatorwakes/op")
193+ b.ReportMetric(float64(metrics.coordinatorCoalesced)/perOperation, "coordinatorcoalesced/op")
194+ if metrics.coordinatorInvalidations > 0 {
195+ b.ReportMetric(float64(metrics.coordinatorCoalesced)/float64(metrics.coordinatorInvalidations), "coordinatorcoalescingratio")
196+ } else {
197+ b.ReportMetric(0, "coordinatorcoalescingratio")
198+ }
167 b.ReportMetric(float64(historyRows), "historyrows/pane")199 b.ReportMetric(float64(historyRows), "historyrows/pane")
168 }200 }
169 201
@@ -192,26 +224,30 @@ type performanceConfig struct {
192 }224 }
193 225
194 type performanceMetrics struct {226 type performanceMetrics struct {
195- outputFrames uint64227+ outputFrames uint64
196- outputBytes uint64228+ outputBytes uint64
197- outputPayloadBytes uint64229+ outputPayloadBytes uint64
198- snapshotWrites uint64230+ snapshotWrites uint64
199- snapshotBytes uint64231+ snapshotBytes uint64
232+ coordinatorInvalidations uint64
233+ coordinatorWakes uint64
234+ coordinatorCoalesced uint64
200 }235 }
201 236
202 type performanceFixture struct {237 type performanceFixture struct {
203- t testing.TB238+ t testing.TB
204- d *Daemon239+ d *Daemon
205- sess *session240+ sess *session
206- ac *attachedClient241+ ac *attachedClient
207- output *countingOutputTransport242+ output *countingOutputTransport
208- snaps *countingSnapshotStore243+ snaps *countingSnapshotStore
209- activePane *pane244+ activePane *pane
210- liveWrites [][]byte245+ liveWrites [][]byte
211- paints int246+ paints int
212- resizeSizes [2]domain.Size247+ resizeSizes [2]domain.Size
213- resizes int248+ resizes int
214- resizedSize domain.Size249+ resizedSize domain.Size
250+ coordinatorBaseline renderCoordinatorBurstMetricsSnapshot
215 }251 }
216 252
217 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {253 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {
@@ -253,6 +289,11 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
253 }289 }
254 fixture.activePane = fixture.findActivePane()290 fixture.activePane = fixture.findActivePane()
255 291
292+ // The fixture uses the production coordinator. stubClock's inert timer is
293+ // completed synchronously by the coordinator, keeping benchmark operations
294+ // deterministic while retaining their real invalidation path.
295+ d.attachCoordinator(sess, nil, ac)
296+
256 // Prime the real renderer shadow before measurements. Subsequent paints use297 // Prime the real renderer shadow before measurements. Subsequent paints use
257 // actual production diffs rather than the initial full frame.298 // actual production diffs rather than the initial full frame.
258 d.paint(sess, ac, true)299 d.paint(sess, ac, true)
@@ -311,7 +352,11 @@ func (f *performanceFixture) paintLive() {
311 p.mu.Lock()352 p.mu.Lock()
312 p.screen.Write(f.liveWrites[f.paints%len(f.liveWrites)])353 p.screen.Write(f.liveWrites[f.paints%len(f.liveWrites)])
313 p.mu.Unlock()354 p.mu.Unlock()
314- f.d.paint(f.sess, f.ac, false)355+ if rc := f.sess.renderCoordinator(); rc != nil {
356+ rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "performance_benchmark_test.go"})
357+ } else {
358+ f.d.paint(f.sess, f.ac, false)
359+ }
315 f.paints++360 f.paints++
316 }361 }
317 362
@@ -359,15 +404,25 @@ func (f *performanceFixture) resized() bool {
359 func (f *performanceFixture) resetMetrics() {404 func (f *performanceFixture) resetMetrics() {
360 f.output.reset()405 f.output.reset()
361 f.snaps.reset()406 f.snaps.reset()
407+ if rc := f.sess.renderCoordinator(); rc != nil {
408+ f.coordinatorBaseline = rc.burstMetricsSnapshot()
409+ }
362 }410 }
363 411
364 func (f *performanceFixture) metrics() performanceMetrics {412 func (f *performanceFixture) metrics() performanceMetrics {
365 output := f.output.metrics()413 output := f.output.metrics()
366 snapshots := f.snaps.metrics()414 snapshots := f.snaps.metrics()
367- return performanceMetrics{415+ metrics := performanceMetrics{
368 outputFrames: output.frames, outputBytes: output.bytes, outputPayloadBytes: output.payloadBytes,416 outputFrames: output.frames, outputBytes: output.bytes, outputPayloadBytes: output.payloadBytes,
369 snapshotWrites: snapshots.writes, snapshotBytes: snapshots.bytes,417 snapshotWrites: snapshots.writes, snapshotBytes: snapshots.bytes,
370 }418 }
419+ if rc := f.sess.renderCoordinator(); rc != nil {
420+ coordinator := rc.burstMetricsSnapshot()
421+ metrics.coordinatorInvalidations = coordinator.invalidations - f.coordinatorBaseline.invalidations
422+ metrics.coordinatorWakes = coordinator.wakes - f.coordinatorBaseline.wakes
423+ metrics.coordinatorCoalesced = coordinator.coalesced - f.coordinatorBaseline.coalesced
424+ }
425+ return metrics
371 }426 }
372 427
373 const outputPayloadHeaderBytes = 24428 const outputPayloadHeaderBytes = 24
M internal/usecase/daemon/render_coordinator.go +18 -0
@@ -92,6 +92,14 @@ type renderCoordinatorBurstMetrics struct {
9292 coalesced atomic.Uint64
9393 }
9494
95+// renderCoordinatorBurstMetricsSnapshot is an immutable, internal view for
96+// benchmark reporting. Scheduling only writes the atomic counters above.
97+type renderCoordinatorBurstMetricsSnapshot struct {
98+ invalidations uint64
99+ wakes uint64
100+ coalesced uint64
101+}
102+
95103 type renderCoordinator struct {
96104 mu sync.Mutex
97105 opts renderCoordinatorOptions
@@ -413,6 +421,16 @@ func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata {
413421 return c.resize
414422 }
415423
424+// burstMetricsSnapshot returns a consistent read-only metric view for
425+// benchmark reporting without exposing coordinator policy outside this package.
426+func (c *renderCoordinator) burstMetricsSnapshot() renderCoordinatorBurstMetricsSnapshot {
427+ return renderCoordinatorBurstMetricsSnapshot{
428+ invalidations: c.metrics.invalidations.Load(),
429+ wakes: c.metrics.wakes.Load(),
430+ coalesced: c.metrics.coalesced.Load(),
431+ }
432+}
433+
416434 // renderCoordinator returns the coordinator installed for this session.
417435 func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() }
418436
@@ -92,6 +92,14 @@ type renderCoordinatorBurstMetrics struct {
92 coalesced atomic.Uint6492 coalesced atomic.Uint64
93 }93 }
94 94
95+// renderCoordinatorBurstMetricsSnapshot is an immutable, internal view for
96+// benchmark reporting. Scheduling only writes the atomic counters above.
97+type renderCoordinatorBurstMetricsSnapshot struct {
98+ invalidations uint64
99+ wakes uint64
100+ coalesced uint64
101+}
102+
95 type renderCoordinator struct {103 type renderCoordinator struct {
96 mu sync.Mutex104 mu sync.Mutex
97 opts renderCoordinatorOptions105 opts renderCoordinatorOptions
@@ -413,6 +421,16 @@ func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata {
413 return c.resize421 return c.resize
414 }422 }
415 423
424+// burstMetricsSnapshot returns a consistent read-only metric view for
425+// benchmark reporting without exposing coordinator policy outside this package.
426+func (c *renderCoordinator) burstMetricsSnapshot() renderCoordinatorBurstMetricsSnapshot {
427+ return renderCoordinatorBurstMetricsSnapshot{
428+ invalidations: c.metrics.invalidations.Load(),
429+ wakes: c.metrics.wakes.Load(),
430+ coalesced: c.metrics.coalesced.Load(),
431+ }
432+}
433+
416 // renderCoordinator returns the coordinator installed for this session.434 // renderCoordinator returns the coordinator installed for this session.
417 func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() }435 func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() }
418 436

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