detached ed9b078

perf(snapshot): benchmark chunked history persistence

commit
ed9b078fe01bec3db40b4636db9f01b78c6b3425
author
brice · · 2026-07-12 17:12 UTC
parent
55b4f02
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +523 -16
M internal/usecase/daemon/performance_benchmark_test.go +243 -15
@@ -152,6 +152,24 @@ func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
152152 }
153153 }
154154
155+func TestPerformanceFixtureSnapshotCaptureRetainsSealedHistory(t *testing.T) {
156+ fixture := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, historyRows: 10_000})
157+
158+ first, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
159+ require.True(t, ok)
160+ require.Len(t, first.tabs, 1)
161+ require.Len(t, first.tabs[0].panes, 2)
162+ require.Equal(t, 10_000, first.tabs[0].panes[0].history.Len())
163+ require.Equal(t, 40, first.tabs[0].panes[0].history.ChunkCount())
164+
165+ second, ok := fixture.d.captureSnapshotState(fixture.sess, 2)
166+ require.True(t, ok)
167+ secondPane := capturePaneByID(second, first.tabs[0].panes[0].id)
168+ if secondPane == nil || first.tabs[0].panes[0].history.Chunk(0) != secondPane.history.Chunk(0) {
169+ t.Fatal("unchanged sealed history chunk was not reused across captures")
170+ }
171+}
172+
155173 func TestPerformanceFixtureCoordinatorMetricsForCanonicalTopologies(t *testing.T) {
156174 for _, topology := range daemonHistoryTopologies {
157175 t.Run(topology.name, func(t *testing.T) {
@@ -181,12 +199,37 @@ var daemonHistoryTopologies = []daemonHistoryTopology{
181199 {name: "8tabs-1pane", tabs: 8, panes: 1},
182200 }
183201
202+// v3 bounds a decoded snapshot to 256 MiB. One 120-column 10k-row tab is
203+// representative and valid; multi-tab fixtures exceed that durable budget, so
204+// snapshot capture/encode benchmark only the valid one-tab topologies.
205+var daemonSnapshotTopologies = []daemonHistoryTopology{
206+ {name: "1tab-1pane-control", tabs: 1, panes: 1},
207+ {name: "1tab-4panes", tabs: 1, panes: 4},
208+}
209+
184210 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
185211 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
186212 }
213+
214+// BenchmarkDaemonHistorySnapshotCapture measures the synchronous immutable
215+// capture boundary only. Encoding and the bounded worker queue are deliberately
216+// excluded so scheduler latency cannot affect this benchmark.
187217 func BenchmarkDaemonHistorySnapshotCapture(b *testing.B) {
188- benchmarkDaemonLargeHistory(b, "snapshot-capture", func(f *performanceFixture) { f.captureSnapshot() })
218+ benchmarkDaemonSnapshotCapture(b)
219+}
220+
221+// BenchmarkDaemonHistorySnapshotEncode measures v3 encoding from an already
222+// immutable capture, without queueing or persistence.
223+func BenchmarkDaemonHistorySnapshotEncode(b *testing.B) {
224+ benchmarkDaemonSnapshotEncode(b)
225+}
226+
227+// BenchmarkDaemonHistorySnapshotRestore measures terminal-state restoration
228+// from a validated v3 pane manifest without spawning PTY or session workers.
229+func BenchmarkDaemonHistorySnapshotRestore(b *testing.B) {
230+ benchmarkDaemonSnapshotRestore(b)
189231 }
232+
190233 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {
191234 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })
192235 }
@@ -225,8 +268,12 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
225268 fixture := newPerformanceFixture(b, performanceConfig{
226269 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
227270 })
228- if workload == "copy-search" {
229- fixture.d.enterCopyMode(fixture.sess, fixture.ac)
271+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
272+ b.Fatal("invalid daemon history fixture")
273+ }
274+ if workload == "copy-enter" || workload == "copy-search" {
275+ benchmarkDaemonCopyOperation(b, fixture, workload, run)
276+ return
230277 }
231278 b.ReportAllocs()
232279 fixture.resetMetrics()
@@ -240,26 +287,177 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
240287 b.Fatalf("decode last output: %v", err)
241288 }
242289 }
243- if workload == "snapshot-capture" {
244- payload := fixture.snaps.lastPayload()
245- if payload == nil {
246- b.Fatal("snapshot capture retained no snapshot payload")
247- }
248- if _, err := snapcodec.Unmarshal(payload); err != nil {
249- b.Fatalf("decode last snapshot: %v", err)
250- }
251- }
252290 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {
253291 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)
254292 }
255- if workload == "snapshot-capture" && metrics.snapshotWrites != uint64(b.N) {
256- b.Fatalf("snapshot capture wrote %d snapshots for %d operations", metrics.snapshotWrites, b.N)
257- }
258293 benchmarkReportMetrics(b, metrics, b.N, 10_000)
259294 })
260295 }
261296 }
262297
298+func benchmarkDaemonCopyOperation(b *testing.B, fixture *performanceFixture, workload string, run func(*performanceFixture)) {
299+ b.Helper()
300+ b.ReportAllocs()
301+ b.ResetTimer()
302+ operations := 0
303+ for range b.N {
304+ b.StopTimer()
305+ fixture.d.exitCopyMode(fixture.ac)
306+ if workload == "copy-search" {
307+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
308+ }
309+ fixture.ac.ackOutputState(fixture.ac.output.next)
310+ b.StartTimer()
311+ run(fixture)
312+ b.StopTimer()
313+ if workload == "copy-enter" && !fixture.copyModeActive() {
314+ b.Fatal("copy enter did not install a copy mode")
315+ }
316+ if workload == "copy-search" && fixture.searchMatches() == 0 {
317+ b.Fatal("copy search produced no deterministic matches")
318+ }
319+ operations++
320+ }
321+ if operations != b.N {
322+ b.Fatalf("copy workload ran %d operations for %d iterations", operations, b.N)
323+ }
324+ b.ReportMetric(float64(operations)/float64(b.N), "copyoperations/op")
325+}
326+
327+func benchmarkDaemonSnapshotCapture(b *testing.B) {
328+ for _, topology := range daemonSnapshotTopologies {
329+ b.Run(topology.name, func(b *testing.B) {
330+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000})
331+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
332+ b.Fatal("invalid daemon snapshot capture fixture")
333+ }
334+ first, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
335+ if !ok || !validBenchmarkCapture(first) {
336+ b.Fatal("invalid snapshot capture fixture")
337+ }
338+ firstPane := first.tabs[0].panes[0]
339+ firstChunk := firstPane.history.Chunk(0)
340+ captures := 0
341+ var last *snapshotCapture
342+ b.ReportAllocs()
343+ b.ResetTimer()
344+ for range b.N {
345+ last, ok = fixture.d.captureSnapshotState(fixture.sess, uint64(captures+2))
346+ if !ok {
347+ b.Fatal("snapshot capture unexpectedly rejected fixture")
348+ }
349+ captures++
350+ }
351+ b.StopTimer()
352+ lastPane := capturePaneByID(last, firstPane.id)
353+ if captures != b.N || !validBenchmarkCapture(last) || lastPane == nil || lastPane.history.Chunk(0) != firstChunk {
354+ b.Fatal("snapshot capture did not preserve immutable sealed history")
355+ }
356+ b.ReportMetric(float64(capturePaneCount(last)), "capturepanes/op")
357+ })
358+ }
359+}
360+
361+func benchmarkDaemonSnapshotEncode(b *testing.B) {
362+ for _, topology := range daemonSnapshotTopologies {
363+ b.Run(topology.name, func(b *testing.B) {
364+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000})
365+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
366+ b.Fatal("invalid daemon snapshot encode fixture")
367+ }
368+ capture, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
369+ if !ok || !validBenchmarkCapture(capture) {
370+ b.Fatal("invalid snapshot encode fixture")
371+ }
372+ encoded, err := fixture.d.encodeSnapshotCapture(capture)
373+ if err != nil {
374+ b.Fatal(err)
375+ }
376+ if _, err := snapcodec.Unmarshal(encoded); err != nil {
377+ b.Fatalf("invalid v3 snapshot encode fixture: %v", err)
378+ }
379+ var last []byte
380+ b.ReportAllocs()
381+ b.SetBytes(int64(len(encoded)))
382+ b.ResetTimer()
383+ for range b.N {
384+ last, err = fixture.d.encodeSnapshotCapture(capture)
385+ if err != nil {
386+ b.Fatal(err)
387+ }
388+ }
389+ b.StopTimer()
390+ if _, err := snapcodec.Unmarshal(last); err != nil {
391+ b.Fatalf("snapshot encoder produced invalid v3 data: %v", err)
392+ }
393+ b.ReportMetric(float64(len(last)), "snapshotbytes/op")
394+ })
395+ }
396+}
397+
398+func benchmarkDaemonSnapshotRestore(b *testing.B) {
399+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, panes: 1, historyRows: 10_000})
400+ if !fixture.hasHistoryTopology(1, 1, 10_000) {
401+ b.Fatal("invalid daemon snapshot restore fixture")
402+ }
403+ capture, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
404+ if !ok || !validBenchmarkCapture(capture) {
405+ b.Fatal("invalid snapshot restore fixture")
406+ }
407+ encoded, err := fixture.d.encodeSnapshotCapture(capture)
408+ if err != nil {
409+ b.Fatal(err)
410+ }
411+ snapshot, err := snapcodec.Unmarshal(encoded)
412+ if err != nil || len(snapshot.Tabs) != 1 || len(snapshot.Tabs[0].Panes) == 0 {
413+ b.Fatalf("invalid v3 snapshot restore fixture: %v", err)
414+ }
415+ paneSnapshot := snapshot.Tabs[0].Panes[0]
416+ b.ReportAllocs()
417+ b.SetBytes(int64(len(encoded)))
418+ b.ResetTimer()
419+ for range b.N {
420+ if err := restorePaneTerminal(fixture.activePane, paneSnapshot); err != nil {
421+ b.Fatal(err)
422+ }
423+ }
424+ b.StopTimer()
425+ fixture.activePane.mu.Lock()
426+ restoredRows := fixture.activePane.history.Len()
427+ fixture.activePane.mu.Unlock()
428+ if restoredRows != 10_000 {
429+ b.Fatalf("snapshot restore retained %d history rows, want 10000", restoredRows)
430+ }
431+ b.ReportMetric(float64(restoredRows), "historyrows/restore")
432+}
433+
434+func validBenchmarkCapture(capture *snapshotCapture) bool {
435+ return capture != nil && len(capture.tabs) > 0 && len(capture.tabs[0].panes) > 0 && capture.tabs[0].panes[0].history.Len() == 10_000 && capture.tabs[0].panes[0].history.ChunkCount() > 0
436+}
437+
438+func capturePaneCount(capture *snapshotCapture) int {
439+ count := 0
440+ for _, tab := range capture.tabs {
441+ count += len(tab.panes)
442+ }
443+ return count
444+}
445+
446+func capturePaneByID(capture *snapshotCapture, id layout.PaneID) *snapshotCapturePane {
447+ if capture == nil {
448+ return nil
449+ }
450+ for tabIndex := range capture.tabs {
451+ for paneIndex := range capture.tabs[tabIndex].panes {
452+ pane := &capture.tabs[tabIndex].panes[paneIndex]
453+ if pane.id == id {
454+ return pane
455+ }
456+ }
457+ }
458+ return nil
459+}
460+
263461 func TestCoordinatorCoalescingRatioReportsInvalidationsPerWake(t *testing.T) {
264462 require.Equal(t, float64(3), coordinatorCoalescingRatio(performanceMetrics{
265463 coordinatorWakes: 1,
@@ -621,6 +819,36 @@ func (f *performanceFixture) enterCopySearch() {
621819 f.d.handleInput(f.sess, f.ac, []byte("/needle\r"))
622820 }
623821
822+func (f *performanceFixture) hasHistoryTopology(tabs, panes, historyRows int) bool {
823+ if len(f.sess.tabs) != tabs {
824+ return false
825+ }
826+ for _, tb := range f.sess.tabs {
827+ tb.mu.Lock()
828+ if len(tb.panes) != panes {
829+ tb.mu.Unlock()
830+ return false
831+ }
832+ for _, p := range tb.panes {
833+ p.mu.Lock()
834+ rows := p.history.Len()
835+ p.mu.Unlock()
836+ if rows != historyRows {
837+ tb.mu.Unlock()
838+ return false
839+ }
840+ }
841+ tb.mu.Unlock()
842+ }
843+ return true
844+}
845+
846+func (f *performanceFixture) copyModeActive() bool {
847+ f.ac.overlays.copyMu.Lock()
848+ defer f.ac.overlays.copyMu.Unlock()
849+ return f.ac.overlays.copyMode != nil
850+}
851+
624852 func (f *performanceFixture) searchMatches() int {
625853 f.ac.overlays.copyMu.Lock()
626854 defer f.ac.overlays.copyMu.Unlock()
@@ -152,6 +152,24 @@ func TestPerformanceFixtureLargeHistoryTopology(t *testing.T) {
152 }152 }
153 }153 }
154 154
155+func TestPerformanceFixtureSnapshotCaptureRetainsSealedHistory(t *testing.T) {
156+ fixture := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, historyRows: 10_000})
157+
158+ first, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
159+ require.True(t, ok)
160+ require.Len(t, first.tabs, 1)
161+ require.Len(t, first.tabs[0].panes, 2)
162+ require.Equal(t, 10_000, first.tabs[0].panes[0].history.Len())
163+ require.Equal(t, 40, first.tabs[0].panes[0].history.ChunkCount())
164+
165+ second, ok := fixture.d.captureSnapshotState(fixture.sess, 2)
166+ require.True(t, ok)
167+ secondPane := capturePaneByID(second, first.tabs[0].panes[0].id)
168+ if secondPane == nil || first.tabs[0].panes[0].history.Chunk(0) != secondPane.history.Chunk(0) {
169+ t.Fatal("unchanged sealed history chunk was not reused across captures")
170+ }
171+}
172+
155 func TestPerformanceFixtureCoordinatorMetricsForCanonicalTopologies(t *testing.T) {173 func TestPerformanceFixtureCoordinatorMetricsForCanonicalTopologies(t *testing.T) {
156 for _, topology := range daemonHistoryTopologies {174 for _, topology := range daemonHistoryTopologies {
157 t.Run(topology.name, func(t *testing.T) {175 t.Run(topology.name, func(t *testing.T) {
@@ -181,12 +199,37 @@ var daemonHistoryTopologies = []daemonHistoryTopology{
181 {name: "8tabs-1pane", tabs: 8, panes: 1},199 {name: "8tabs-1pane", tabs: 8, panes: 1},
182 }200 }
183 201
202+// v3 bounds a decoded snapshot to 256 MiB. One 120-column 10k-row tab is
203+// representative and valid; multi-tab fixtures exceed that durable budget, so
204+// snapshot capture/encode benchmark only the valid one-tab topologies.
205+var daemonSnapshotTopologies = []daemonHistoryTopology{
206+ {name: "1tab-1pane-control", tabs: 1, panes: 1},
207+ {name: "1tab-4panes", tabs: 1, panes: 4},
208+}
209+
184 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {210 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
185 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })211 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
186 }212 }
213+
214+// BenchmarkDaemonHistorySnapshotCapture measures the synchronous immutable
215+// capture boundary only. Encoding and the bounded worker queue are deliberately
216+// excluded so scheduler latency cannot affect this benchmark.
187 func BenchmarkDaemonHistorySnapshotCapture(b *testing.B) {217 func BenchmarkDaemonHistorySnapshotCapture(b *testing.B) {
188- benchmarkDaemonLargeHistory(b, "snapshot-capture", func(f *performanceFixture) { f.captureSnapshot() })218+ benchmarkDaemonSnapshotCapture(b)
219+}
220+
221+// BenchmarkDaemonHistorySnapshotEncode measures v3 encoding from an already
222+// immutable capture, without queueing or persistence.
223+func BenchmarkDaemonHistorySnapshotEncode(b *testing.B) {
224+ benchmarkDaemonSnapshotEncode(b)
225+}
226+
227+// BenchmarkDaemonHistorySnapshotRestore measures terminal-state restoration
228+// from a validated v3 pane manifest without spawning PTY or session workers.
229+func BenchmarkDaemonHistorySnapshotRestore(b *testing.B) {
230+ benchmarkDaemonSnapshotRestore(b)
189 }231 }
232+
190 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {233 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {
191 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })234 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })
192 }235 }
@@ -225,8 +268,12 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
225 fixture := newPerformanceFixture(b, performanceConfig{268 fixture := newPerformanceFixture(b, performanceConfig{
226 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,269 size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000,
227 })270 })
228- if workload == "copy-search" {271+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
229- fixture.d.enterCopyMode(fixture.sess, fixture.ac)272+ b.Fatal("invalid daemon history fixture")
273+ }
274+ if workload == "copy-enter" || workload == "copy-search" {
275+ benchmarkDaemonCopyOperation(b, fixture, workload, run)
276+ return
230 }277 }
231 b.ReportAllocs()278 b.ReportAllocs()
232 fixture.resetMetrics()279 fixture.resetMetrics()
@@ -240,26 +287,177 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
240 b.Fatalf("decode last output: %v", err)287 b.Fatalf("decode last output: %v", err)
241 }288 }
242 }289 }
243- if workload == "snapshot-capture" {
244- payload := fixture.snaps.lastPayload()
245- if payload == nil {
246- b.Fatal("snapshot capture retained no snapshot payload")
247- }
248- if _, err := snapcodec.Unmarshal(payload); err != nil {
249- b.Fatalf("decode last snapshot: %v", err)
250- }
251- }
252 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {290 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {
253 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)291 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)
254 }292 }
255- if workload == "snapshot-capture" && metrics.snapshotWrites != uint64(b.N) {
256- b.Fatalf("snapshot capture wrote %d snapshots for %d operations", metrics.snapshotWrites, b.N)
257- }
258 benchmarkReportMetrics(b, metrics, b.N, 10_000)293 benchmarkReportMetrics(b, metrics, b.N, 10_000)
259 })294 })
260 }295 }
261 }296 }
262 297
298+func benchmarkDaemonCopyOperation(b *testing.B, fixture *performanceFixture, workload string, run func(*performanceFixture)) {
299+ b.Helper()
300+ b.ReportAllocs()
301+ b.ResetTimer()
302+ operations := 0
303+ for range b.N {
304+ b.StopTimer()
305+ fixture.d.exitCopyMode(fixture.ac)
306+ if workload == "copy-search" {
307+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
308+ }
309+ fixture.ac.ackOutputState(fixture.ac.output.next)
310+ b.StartTimer()
311+ run(fixture)
312+ b.StopTimer()
313+ if workload == "copy-enter" && !fixture.copyModeActive() {
314+ b.Fatal("copy enter did not install a copy mode")
315+ }
316+ if workload == "copy-search" && fixture.searchMatches() == 0 {
317+ b.Fatal("copy search produced no deterministic matches")
318+ }
319+ operations++
320+ }
321+ if operations != b.N {
322+ b.Fatalf("copy workload ran %d operations for %d iterations", operations, b.N)
323+ }
324+ b.ReportMetric(float64(operations)/float64(b.N), "copyoperations/op")
325+}
326+
327+func benchmarkDaemonSnapshotCapture(b *testing.B) {
328+ for _, topology := range daemonSnapshotTopologies {
329+ b.Run(topology.name, func(b *testing.B) {
330+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000})
331+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
332+ b.Fatal("invalid daemon snapshot capture fixture")
333+ }
334+ first, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
335+ if !ok || !validBenchmarkCapture(first) {
336+ b.Fatal("invalid snapshot capture fixture")
337+ }
338+ firstPane := first.tabs[0].panes[0]
339+ firstChunk := firstPane.history.Chunk(0)
340+ captures := 0
341+ var last *snapshotCapture
342+ b.ReportAllocs()
343+ b.ResetTimer()
344+ for range b.N {
345+ last, ok = fixture.d.captureSnapshotState(fixture.sess, uint64(captures+2))
346+ if !ok {
347+ b.Fatal("snapshot capture unexpectedly rejected fixture")
348+ }
349+ captures++
350+ }
351+ b.StopTimer()
352+ lastPane := capturePaneByID(last, firstPane.id)
353+ if captures != b.N || !validBenchmarkCapture(last) || lastPane == nil || lastPane.history.Chunk(0) != firstChunk {
354+ b.Fatal("snapshot capture did not preserve immutable sealed history")
355+ }
356+ b.ReportMetric(float64(capturePaneCount(last)), "capturepanes/op")
357+ })
358+ }
359+}
360+
361+func benchmarkDaemonSnapshotEncode(b *testing.B) {
362+ for _, topology := range daemonSnapshotTopologies {
363+ b.Run(topology.name, func(b *testing.B) {
364+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: topology.tabs, panes: topology.panes, historyRows: 10_000})
365+ if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
366+ b.Fatal("invalid daemon snapshot encode fixture")
367+ }
368+ capture, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
369+ if !ok || !validBenchmarkCapture(capture) {
370+ b.Fatal("invalid snapshot encode fixture")
371+ }
372+ encoded, err := fixture.d.encodeSnapshotCapture(capture)
373+ if err != nil {
374+ b.Fatal(err)
375+ }
376+ if _, err := snapcodec.Unmarshal(encoded); err != nil {
377+ b.Fatalf("invalid v3 snapshot encode fixture: %v", err)
378+ }
379+ var last []byte
380+ b.ReportAllocs()
381+ b.SetBytes(int64(len(encoded)))
382+ b.ResetTimer()
383+ for range b.N {
384+ last, err = fixture.d.encodeSnapshotCapture(capture)
385+ if err != nil {
386+ b.Fatal(err)
387+ }
388+ }
389+ b.StopTimer()
390+ if _, err := snapcodec.Unmarshal(last); err != nil {
391+ b.Fatalf("snapshot encoder produced invalid v3 data: %v", err)
392+ }
393+ b.ReportMetric(float64(len(last)), "snapshotbytes/op")
394+ })
395+ }
396+}
397+
398+func benchmarkDaemonSnapshotRestore(b *testing.B) {
399+ fixture := newPerformanceFixture(b, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, panes: 1, historyRows: 10_000})
400+ if !fixture.hasHistoryTopology(1, 1, 10_000) {
401+ b.Fatal("invalid daemon snapshot restore fixture")
402+ }
403+ capture, ok := fixture.d.captureSnapshotState(fixture.sess, 1)
404+ if !ok || !validBenchmarkCapture(capture) {
405+ b.Fatal("invalid snapshot restore fixture")
406+ }
407+ encoded, err := fixture.d.encodeSnapshotCapture(capture)
408+ if err != nil {
409+ b.Fatal(err)
410+ }
411+ snapshot, err := snapcodec.Unmarshal(encoded)
412+ if err != nil || len(snapshot.Tabs) != 1 || len(snapshot.Tabs[0].Panes) == 0 {
413+ b.Fatalf("invalid v3 snapshot restore fixture: %v", err)
414+ }
415+ paneSnapshot := snapshot.Tabs[0].Panes[0]
416+ b.ReportAllocs()
417+ b.SetBytes(int64(len(encoded)))
418+ b.ResetTimer()
419+ for range b.N {
420+ if err := restorePaneTerminal(fixture.activePane, paneSnapshot); err != nil {
421+ b.Fatal(err)
422+ }
423+ }
424+ b.StopTimer()
425+ fixture.activePane.mu.Lock()
426+ restoredRows := fixture.activePane.history.Len()
427+ fixture.activePane.mu.Unlock()
428+ if restoredRows != 10_000 {
429+ b.Fatalf("snapshot restore retained %d history rows, want 10000", restoredRows)
430+ }
431+ b.ReportMetric(float64(restoredRows), "historyrows/restore")
432+}
433+
434+func validBenchmarkCapture(capture *snapshotCapture) bool {
435+ return capture != nil && len(capture.tabs) > 0 && len(capture.tabs[0].panes) > 0 && capture.tabs[0].panes[0].history.Len() == 10_000 && capture.tabs[0].panes[0].history.ChunkCount() > 0
436+}
437+
438+func capturePaneCount(capture *snapshotCapture) int {
439+ count := 0
440+ for _, tab := range capture.tabs {
441+ count += len(tab.panes)
442+ }
443+ return count
444+}
445+
446+func capturePaneByID(capture *snapshotCapture, id layout.PaneID) *snapshotCapturePane {
447+ if capture == nil {
448+ return nil
449+ }
450+ for tabIndex := range capture.tabs {
451+ for paneIndex := range capture.tabs[tabIndex].panes {
452+ pane := &capture.tabs[tabIndex].panes[paneIndex]
453+ if pane.id == id {
454+ return pane
455+ }
456+ }
457+ }
458+ return nil
459+}
460+
263 func TestCoordinatorCoalescingRatioReportsInvalidationsPerWake(t *testing.T) {461 func TestCoordinatorCoalescingRatioReportsInvalidationsPerWake(t *testing.T) {
264 require.Equal(t, float64(3), coordinatorCoalescingRatio(performanceMetrics{462 require.Equal(t, float64(3), coordinatorCoalescingRatio(performanceMetrics{
265 coordinatorWakes: 1,463 coordinatorWakes: 1,
@@ -621,6 +819,36 @@ func (f *performanceFixture) enterCopySearch() {
621 f.d.handleInput(f.sess, f.ac, []byte("/needle\r"))819 f.d.handleInput(f.sess, f.ac, []byte("/needle\r"))
622 }820 }
623 821
822+func (f *performanceFixture) hasHistoryTopology(tabs, panes, historyRows int) bool {
823+ if len(f.sess.tabs) != tabs {
824+ return false
825+ }
826+ for _, tb := range f.sess.tabs {
827+ tb.mu.Lock()
828+ if len(tb.panes) != panes {
829+ tb.mu.Unlock()
830+ return false
831+ }
832+ for _, p := range tb.panes {
833+ p.mu.Lock()
834+ rows := p.history.Len()
835+ p.mu.Unlock()
836+ if rows != historyRows {
837+ tb.mu.Unlock()
838+ return false
839+ }
840+ }
841+ tb.mu.Unlock()
842+ }
843+ return true
844+}
845+
846+func (f *performanceFixture) copyModeActive() bool {
847+ f.ac.overlays.copyMu.Lock()
848+ defer f.ac.overlays.copyMu.Unlock()
849+ return f.ac.overlays.copyMode != nil
850+}
851+
624 func (f *performanceFixture) searchMatches() int {852 func (f *performanceFixture) searchMatches() int {
625 f.ac.overlays.copyMu.Lock()853 f.ac.overlays.copyMu.Lock()
626 defer f.ac.overlays.copyMu.Unlock()854 defer f.ac.overlays.copyMu.Unlock()
A internal/usecase/snapshot/benchmark_test.go +122 -0
@@ -0,0 +1,122 @@
1+package snapshot
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/internal/usecase/layout"
7+ "github.com/bnema/vev/pkg/renderer"
8+ "github.com/bnema/vev/pkg/vt"
9+)
10+
11+var (
12+ benchmarkV3BytesSink []byte
13+ benchmarkV3SessionSink Session
14+)
15+
16+func BenchmarkSnapshotV3Marshal10KRows(b *testing.B) {
17+ session, encoded := benchmarkV3Session(b)
18+ b.ReportAllocs()
19+ b.SetBytes(int64(len(encoded)))
20+ b.ResetTimer()
21+ for b.Loop() {
22+ var err error
23+ benchmarkV3BytesSink, err = Marshal(session)
24+ if err != nil {
25+ b.Fatal(err)
26+ }
27+ }
28+ b.StopTimer()
29+ if _, err := Unmarshal(benchmarkV3BytesSink); err != nil {
30+ b.Fatalf("marshal produced an invalid v3 snapshot: %v", err)
31+ }
32+}
33+
34+func BenchmarkSnapshotV3Unmarshal10KRows(b *testing.B) {
35+ _, encoded := benchmarkV3Session(b)
36+ b.ReportAllocs()
37+ b.SetBytes(int64(len(encoded)))
38+ b.ResetTimer()
39+ for b.Loop() {
40+ var err error
41+ benchmarkV3SessionSink, err = Unmarshal(encoded)
42+ if err != nil {
43+ b.Fatal(err)
44+ }
45+ }
46+ b.StopTimer()
47+ if len(benchmarkV3SessionSink.Tabs) != 1 || len(benchmarkV3SessionSink.Tabs[0].Panes) != 1 || len(benchmarkV3SessionSink.Tabs[0].Panes[0].SealedChunks) != 40 {
48+ b.Fatal("unmarshal lost canonical 10k-row snapshot data")
49+ }
50+}
51+
52+func benchmarkV3Session(t testing.TB) (Session, []byte) {
53+ t.Helper()
54+ const (
55+ cols = 120
56+ rows = 40
57+ historyRows = 10_000
58+ chunkRows = 256
59+ )
60+ history := vt.NewHistory(vt.HistoryConfig{MaxRows: historyRows, ChunkRows: chunkRows})
61+ for row := range historyRows {
62+ cells := make([]renderer.Cell, cols)
63+ for col := range cells {
64+ cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
65+ }
66+ history.Append(cells)
67+ }
68+ sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
69+ if err != nil {
70+ t.Fatal(err)
71+ }
72+ visibleFrame := renderer.NewFrame(cols, rows)
73+ for y := range rows {
74+ for x := range cols {
75+ visibleFrame.Set(x, y, renderer.Cell{Rune: rune('A' + (x+y)%26)})
76+ }
77+ }
78+ visible, err := vt.MarshalVisible(visibleFrame)
79+ if err != nil {
80+ t.Fatal(err)
81+ }
82+ session := Session{
83+ Name: "benchmark",
84+ CreatedAt: 1,
85+ Tabs: []Tab{{
86+ StableID: "tab-1",
87+ Cols: cols,
88+ Rows: rows,
89+ NextPaneID: 2,
90+ Focus: "pane-1",
91+ Tree: layout.NewTree("pane-1"),
92+ Panes: []Pane{{
93+ ID: "pane-1",
94+ StableID: "pane-1",
95+ Cwd: "/benchmark",
96+ SealedChunks: sealed,
97+ Tail: tail,
98+ Visible: visible,
99+ }},
100+ }},
101+ }
102+ encoded, err := Marshal(session)
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ decoded, err := Unmarshal(encoded)
107+ if err != nil {
108+ t.Fatal(err)
109+ }
110+ paneCount := 0
111+ decodedChunks := 0
112+ if len(decoded.Tabs) == 1 {
113+ paneCount = len(decoded.Tabs[0].Panes)
114+ if paneCount == 1 {
115+ decodedChunks = len(decoded.Tabs[0].Panes[0].SealedChunks)
116+ }
117+ }
118+ if len(sealed) != (historyRows+chunkRows-1)/chunkRows || paneCount != 1 || decodedChunks != len(sealed) {
119+ t.Fatalf("invalid v3 benchmark fixture: chunks=%d tabs=%d panes=%d decoded chunks=%d", len(sealed), len(decoded.Tabs), paneCount, decodedChunks)
120+ }
121+ return session, encoded
122+}
@@ -0,0 +1,122 @@
1+package snapshot
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/internal/usecase/layout"
7+ "github.com/bnema/vev/pkg/renderer"
8+ "github.com/bnema/vev/pkg/vt"
9+)
10+
11+var (
12+ benchmarkV3BytesSink []byte
13+ benchmarkV3SessionSink Session
14+)
15+
16+func BenchmarkSnapshotV3Marshal10KRows(b *testing.B) {
17+ session, encoded := benchmarkV3Session(b)
18+ b.ReportAllocs()
19+ b.SetBytes(int64(len(encoded)))
20+ b.ResetTimer()
21+ for b.Loop() {
22+ var err error
23+ benchmarkV3BytesSink, err = Marshal(session)
24+ if err != nil {
25+ b.Fatal(err)
26+ }
27+ }
28+ b.StopTimer()
29+ if _, err := Unmarshal(benchmarkV3BytesSink); err != nil {
30+ b.Fatalf("marshal produced an invalid v3 snapshot: %v", err)
31+ }
32+}
33+
34+func BenchmarkSnapshotV3Unmarshal10KRows(b *testing.B) {
35+ _, encoded := benchmarkV3Session(b)
36+ b.ReportAllocs()
37+ b.SetBytes(int64(len(encoded)))
38+ b.ResetTimer()
39+ for b.Loop() {
40+ var err error
41+ benchmarkV3SessionSink, err = Unmarshal(encoded)
42+ if err != nil {
43+ b.Fatal(err)
44+ }
45+ }
46+ b.StopTimer()
47+ if len(benchmarkV3SessionSink.Tabs) != 1 || len(benchmarkV3SessionSink.Tabs[0].Panes) != 1 || len(benchmarkV3SessionSink.Tabs[0].Panes[0].SealedChunks) != 40 {
48+ b.Fatal("unmarshal lost canonical 10k-row snapshot data")
49+ }
50+}
51+
52+func benchmarkV3Session(t testing.TB) (Session, []byte) {
53+ t.Helper()
54+ const (
55+ cols = 120
56+ rows = 40
57+ historyRows = 10_000
58+ chunkRows = 256
59+ )
60+ history := vt.NewHistory(vt.HistoryConfig{MaxRows: historyRows, ChunkRows: chunkRows})
61+ for row := range historyRows {
62+ cells := make([]renderer.Cell, cols)
63+ for col := range cells {
64+ cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
65+ }
66+ history.Append(cells)
67+ }
68+ sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
69+ if err != nil {
70+ t.Fatal(err)
71+ }
72+ visibleFrame := renderer.NewFrame(cols, rows)
73+ for y := range rows {
74+ for x := range cols {
75+ visibleFrame.Set(x, y, renderer.Cell{Rune: rune('A' + (x+y)%26)})
76+ }
77+ }
78+ visible, err := vt.MarshalVisible(visibleFrame)
79+ if err != nil {
80+ t.Fatal(err)
81+ }
82+ session := Session{
83+ Name: "benchmark",
84+ CreatedAt: 1,
85+ Tabs: []Tab{{
86+ StableID: "tab-1",
87+ Cols: cols,
88+ Rows: rows,
89+ NextPaneID: 2,
90+ Focus: "pane-1",
91+ Tree: layout.NewTree("pane-1"),
92+ Panes: []Pane{{
93+ ID: "pane-1",
94+ StableID: "pane-1",
95+ Cwd: "/benchmark",
96+ SealedChunks: sealed,
97+ Tail: tail,
98+ Visible: visible,
99+ }},
100+ }},
101+ }
102+ encoded, err := Marshal(session)
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ decoded, err := Unmarshal(encoded)
107+ if err != nil {
108+ t.Fatal(err)
109+ }
110+ paneCount := 0
111+ decodedChunks := 0
112+ if len(decoded.Tabs) == 1 {
113+ paneCount = len(decoded.Tabs[0].Panes)
114+ if paneCount == 1 {
115+ decodedChunks = len(decoded.Tabs[0].Panes[0].SealedChunks)
116+ }
117+ }
118+ if len(sealed) != (historyRows+chunkRows-1)/chunkRows || paneCount != 1 || decodedChunks != len(sealed) {
119+ t.Fatalf("invalid v3 benchmark fixture: chunks=%d tabs=%d panes=%d decoded chunks=%d", len(sealed), len(decoded.Tabs), paneCount, decodedChunks)
120+ }
121+ return session, encoded
122+}
M pkg/vt/benchmark_test.go +158 -1
@@ -1,6 +1,163 @@
11 package vt
22
3-import "testing"
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/pkg/renderer"
7+)
8+
9+var (
10+ benchmarkHistoryViewSink HistoryView
11+ benchmarkHistoryBlobSink []byte
12+ benchmarkHistoryBlobsSink [][]byte
13+)
14+
15+// TestHistoryViewPartialTailAllocationBounded guards the capture property used
16+// by persistence: retained sealed chunks are shared, while only the small
17+// mutable tail is copied into a view.
18+func TestHistoryViewPartialTailAllocationBounded(t *testing.T) {
19+ const (
20+ width = 120
21+ chunkRows = 64
22+ tailRows = 7
23+ )
24+ sealed := benchmarkHistory(t, chunkRows*4, width, chunkRows)
25+ sealed.SealAndView()
26+ partial := benchmarkHistory(t, chunkRows*4+tailRows, width, chunkRows)
27+ partialBefore := partial.View()
28+
29+ sealedAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = sealed.View() })
30+ partialAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = partial.View() })
31+ if partialAllocs > sealedAllocs+tailRows+3 {
32+ t.Fatalf("partial-tail View allocations = %v, sealed=%v, tail rows=%d; sealed history was copied", partialAllocs, sealedAllocs, tailRows)
33+ }
34+ if partial.View().Chunk(0) != partialBefore.Chunk(0) {
35+ t.Fatal("unchanged sealed chunks must retain identity across partial-tail views")
36+ }
37+}
38+
39+func BenchmarkHistoryView10KRowsSealedReuse(b *testing.B) {
40+ history := benchmarkHistory(b, 10_000, 120, 256)
41+ sealed := history.SealAndView()
42+ if sealed.Len() != 10_000 || sealed.ChunkCount() != 40 || sealed.Chunk(0) == nil {
43+ b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", sealed.Len(), sealed.ChunkCount())
44+ }
45+
46+ b.ReportAllocs()
47+ b.ResetTimer()
48+ for b.Loop() {
49+ benchmarkHistoryViewSink = history.View()
50+ }
51+ b.StopTimer()
52+ if got := benchmarkHistoryViewSink; got.Len() != sealed.Len() || got.Chunk(0) != sealed.Chunk(0) {
53+ b.Fatal("unchanged sealed history chunks were not reused")
54+ }
55+ b.ReportMetric(float64(sealed.ChunkCount()), "sealedchunks/view")
56+}
57+
58+func BenchmarkHistoryView10KRowsPartialTail(b *testing.B) {
59+ history := benchmarkHistory(b, 10_000, 120, 256)
60+ before := history.View()
61+ if before.Len() != 10_000 || before.ChunkCount() != 40 || before.Chunk(0) == nil {
62+ b.Fatalf("invalid partial-tail history fixture: rows=%d chunks=%d", before.Len(), before.ChunkCount())
63+ }
64+
65+ b.ReportAllocs()
66+ b.ResetTimer()
67+ for b.Loop() {
68+ benchmarkHistoryViewSink = history.View()
69+ }
70+ b.StopTimer()
71+ if got := benchmarkHistoryViewSink; got.Len() != before.Len() || got.Chunk(0) != before.Chunk(0) {
72+ b.Fatal("partial-tail capture did not reuse its sealed chunks")
73+ }
74+ b.ReportMetric(float64(10_000%256), "tailrows/view")
75+}
76+
77+func BenchmarkMarshalHistoryChunk256x120(b *testing.B) {
78+ history := benchmarkHistory(b, 256, 120, 256)
79+ chunk := history.SealAndView().Chunk(0)
80+ if chunk == nil {
81+ b.Fatal("invalid history chunk fixture")
82+ }
83+
84+ b.ReportAllocs()
85+ b.ResetTimer()
86+ for b.Loop() {
87+ var err error
88+ benchmarkHistoryBlobSink, err = MarshalHistoryChunk(chunk)
89+ if err != nil {
90+ b.Fatal(err)
91+ }
92+ }
93+ b.StopTimer()
94+ if _, err := PreflightHistoryBlob(benchmarkHistoryBlobSink); err != nil {
95+ b.Fatalf("encoded history chunk is invalid: %v", err)
96+ }
97+ b.SetBytes(int64(len(benchmarkHistoryBlobSink)))
98+}
99+
100+func BenchmarkUnmarshalHistoryChunk256x120(b *testing.B) {
101+ history := benchmarkHistory(b, 256, 120, 256)
102+ blob, err := MarshalHistoryChunk(history.SealAndView().Chunk(0))
103+ if err != nil {
104+ b.Fatal(err)
105+ }
106+ if stats, err := PreflightHistoryBlob(blob); err != nil || stats.Rows != 256 || stats.Cells != 256*120 {
107+ b.Fatalf("invalid encoded history chunk fixture: stats=%+v err=%v", stats, err)
108+ }
109+
110+ b.ReportAllocs()
111+ b.SetBytes(int64(len(blob)))
112+ b.ResetTimer()
113+ for b.Loop() {
114+ var err error
115+ benchmarkHistoryViewSink, err = UnmarshalHistory(blob)
116+ if err != nil {
117+ b.Fatal(err)
118+ }
119+ }
120+ b.StopTimer()
121+ if benchmarkHistoryViewSink.Len() != 256 || benchmarkHistoryViewSink.ChunkCount() != 1 {
122+ b.Fatal("decoded history chunk lost rows")
123+ }
124+}
125+
126+func BenchmarkMarshalSealedHistory10KRows(b *testing.B) {
127+ history := benchmarkHistory(b, 10_000, 120, 256)
128+ view := history.SealAndView()
129+ if view.Len() != 10_000 || view.ChunkCount() != 40 {
130+ b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", view.Len(), view.ChunkCount())
131+ }
132+
133+ b.ReportAllocs()
134+ b.ResetTimer()
135+ for b.Loop() {
136+ var err error
137+ benchmarkHistoryBlobsSink, benchmarkHistoryBlobSink, err = MarshalSealedHistory(view)
138+ if err != nil {
139+ b.Fatal(err)
140+ }
141+ }
142+ b.StopTimer()
143+ if len(benchmarkHistoryBlobsSink) != view.ChunkCount() || len(benchmarkHistoryBlobSink) == 0 {
144+ b.Fatal("sealed-history encoder produced an incomplete capture")
145+ }
146+ b.ReportMetric(float64(len(benchmarkHistoryBlobsSink)), "sealedchunks/op")
147+}
148+
149+func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
150+ t.Helper()
151+ history := NewHistory(HistoryConfig{MaxRows: rows, ChunkRows: chunkRows})
152+ for row := range rows {
153+ cells := make([]renderer.Cell, width)
154+ for col := range cells {
155+ cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
156+ }
157+ history.Append(cells)
158+ }
159+ return history
160+}
4161
5162 func BenchmarkScreenShellRedrawBurst(b *testing.B) {
6163 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")
@@ -1,6 +1,163 @@
1 package vt1 package vt
2 2
3-import "testing"3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/pkg/renderer"
7+)
8+
9+var (
10+ benchmarkHistoryViewSink HistoryView
11+ benchmarkHistoryBlobSink []byte
12+ benchmarkHistoryBlobsSink [][]byte
13+)
14+
15+// TestHistoryViewPartialTailAllocationBounded guards the capture property used
16+// by persistence: retained sealed chunks are shared, while only the small
17+// mutable tail is copied into a view.
18+func TestHistoryViewPartialTailAllocationBounded(t *testing.T) {
19+ const (
20+ width = 120
21+ chunkRows = 64
22+ tailRows = 7
23+ )
24+ sealed := benchmarkHistory(t, chunkRows*4, width, chunkRows)
25+ sealed.SealAndView()
26+ partial := benchmarkHistory(t, chunkRows*4+tailRows, width, chunkRows)
27+ partialBefore := partial.View()
28+
29+ sealedAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = sealed.View() })
30+ partialAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = partial.View() })
31+ if partialAllocs > sealedAllocs+tailRows+3 {
32+ t.Fatalf("partial-tail View allocations = %v, sealed=%v, tail rows=%d; sealed history was copied", partialAllocs, sealedAllocs, tailRows)
33+ }
34+ if partial.View().Chunk(0) != partialBefore.Chunk(0) {
35+ t.Fatal("unchanged sealed chunks must retain identity across partial-tail views")
36+ }
37+}
38+
39+func BenchmarkHistoryView10KRowsSealedReuse(b *testing.B) {
40+ history := benchmarkHistory(b, 10_000, 120, 256)
41+ sealed := history.SealAndView()
42+ if sealed.Len() != 10_000 || sealed.ChunkCount() != 40 || sealed.Chunk(0) == nil {
43+ b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", sealed.Len(), sealed.ChunkCount())
44+ }
45+
46+ b.ReportAllocs()
47+ b.ResetTimer()
48+ for b.Loop() {
49+ benchmarkHistoryViewSink = history.View()
50+ }
51+ b.StopTimer()
52+ if got := benchmarkHistoryViewSink; got.Len() != sealed.Len() || got.Chunk(0) != sealed.Chunk(0) {
53+ b.Fatal("unchanged sealed history chunks were not reused")
54+ }
55+ b.ReportMetric(float64(sealed.ChunkCount()), "sealedchunks/view")
56+}
57+
58+func BenchmarkHistoryView10KRowsPartialTail(b *testing.B) {
59+ history := benchmarkHistory(b, 10_000, 120, 256)
60+ before := history.View()
61+ if before.Len() != 10_000 || before.ChunkCount() != 40 || before.Chunk(0) == nil {
62+ b.Fatalf("invalid partial-tail history fixture: rows=%d chunks=%d", before.Len(), before.ChunkCount())
63+ }
64+
65+ b.ReportAllocs()
66+ b.ResetTimer()
67+ for b.Loop() {
68+ benchmarkHistoryViewSink = history.View()
69+ }
70+ b.StopTimer()
71+ if got := benchmarkHistoryViewSink; got.Len() != before.Len() || got.Chunk(0) != before.Chunk(0) {
72+ b.Fatal("partial-tail capture did not reuse its sealed chunks")
73+ }
74+ b.ReportMetric(float64(10_000%256), "tailrows/view")
75+}
76+
77+func BenchmarkMarshalHistoryChunk256x120(b *testing.B) {
78+ history := benchmarkHistory(b, 256, 120, 256)
79+ chunk := history.SealAndView().Chunk(0)
80+ if chunk == nil {
81+ b.Fatal("invalid history chunk fixture")
82+ }
83+
84+ b.ReportAllocs()
85+ b.ResetTimer()
86+ for b.Loop() {
87+ var err error
88+ benchmarkHistoryBlobSink, err = MarshalHistoryChunk(chunk)
89+ if err != nil {
90+ b.Fatal(err)
91+ }
92+ }
93+ b.StopTimer()
94+ if _, err := PreflightHistoryBlob(benchmarkHistoryBlobSink); err != nil {
95+ b.Fatalf("encoded history chunk is invalid: %v", err)
96+ }
97+ b.SetBytes(int64(len(benchmarkHistoryBlobSink)))
98+}
99+
100+func BenchmarkUnmarshalHistoryChunk256x120(b *testing.B) {
101+ history := benchmarkHistory(b, 256, 120, 256)
102+ blob, err := MarshalHistoryChunk(history.SealAndView().Chunk(0))
103+ if err != nil {
104+ b.Fatal(err)
105+ }
106+ if stats, err := PreflightHistoryBlob(blob); err != nil || stats.Rows != 256 || stats.Cells != 256*120 {
107+ b.Fatalf("invalid encoded history chunk fixture: stats=%+v err=%v", stats, err)
108+ }
109+
110+ b.ReportAllocs()
111+ b.SetBytes(int64(len(blob)))
112+ b.ResetTimer()
113+ for b.Loop() {
114+ var err error
115+ benchmarkHistoryViewSink, err = UnmarshalHistory(blob)
116+ if err != nil {
117+ b.Fatal(err)
118+ }
119+ }
120+ b.StopTimer()
121+ if benchmarkHistoryViewSink.Len() != 256 || benchmarkHistoryViewSink.ChunkCount() != 1 {
122+ b.Fatal("decoded history chunk lost rows")
123+ }
124+}
125+
126+func BenchmarkMarshalSealedHistory10KRows(b *testing.B) {
127+ history := benchmarkHistory(b, 10_000, 120, 256)
128+ view := history.SealAndView()
129+ if view.Len() != 10_000 || view.ChunkCount() != 40 {
130+ b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", view.Len(), view.ChunkCount())
131+ }
132+
133+ b.ReportAllocs()
134+ b.ResetTimer()
135+ for b.Loop() {
136+ var err error
137+ benchmarkHistoryBlobsSink, benchmarkHistoryBlobSink, err = MarshalSealedHistory(view)
138+ if err != nil {
139+ b.Fatal(err)
140+ }
141+ }
142+ b.StopTimer()
143+ if len(benchmarkHistoryBlobsSink) != view.ChunkCount() || len(benchmarkHistoryBlobSink) == 0 {
144+ b.Fatal("sealed-history encoder produced an incomplete capture")
145+ }
146+ b.ReportMetric(float64(len(benchmarkHistoryBlobsSink)), "sealedchunks/op")
147+}
148+
149+func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
150+ t.Helper()
151+ history := NewHistory(HistoryConfig{MaxRows: rows, ChunkRows: chunkRows})
152+ for row := range rows {
153+ cells := make([]renderer.Cell, width)
154+ for col := range cells {
155+ cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
156+ }
157+ history.Append(cells)
158+ }
159+ return history
160+}
4 161
5 func BenchmarkScreenShellRedrawBurst(b *testing.B) {162 func BenchmarkScreenShellRedrawBurst(b *testing.B) {
6 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")163 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")

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