detached e53346f

perf(copy): reuse sealed history rows

commit
e53346fc8939f091668cb1e6f7d01c76adc32d03
author
brice · · 2026-07-12 21:51 UTC
parent
a4c844c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +52 -42
M internal/usecase/copy/mode.go +5 -3
@@ -27,11 +27,13 @@ type Snapshot struct {
2727 Height int
2828 }
2929
30-// NewSnapshot freezes the current VT history view and clones the visible screen.
30+// NewSnapshot seals the current VT history tail, then freezes that immutable
31+// view and clones the visible screen. Sealing prevents repeated copy entry from
32+// cloning every row in a partial tail.
3133 func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
3234 var history vt.HistoryView
3335 if historySource != nil {
34- history = historySource.View()
36+ history = historySource.SealAndView()
3537 }
3638 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}
3739 }
@@ -57,7 +59,7 @@ func (s Snapshot) Row(i int) []renderer.Cell {
5759 return nil
5860 }
5961 if i < s.history.Len() {
60- return s.history.Row(i)
62+ return s.history.BorrowedRow(i)
6163 }
6264 i -= s.history.Len()
6365 if i >= s.screen.Height {
@@ -27,11 +27,13 @@ type Snapshot struct {
27 Height int27 Height int
28 }28 }
29 29
30-// NewSnapshot freezes the current VT history view and clones the visible screen.30+// NewSnapshot seals the current VT history tail, then freezes that immutable
31+// view and clones the visible screen. Sealing prevents repeated copy entry from
32+// cloning every row in a partial tail.
31 func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {33 func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
32 var history vt.HistoryView34 var history vt.HistoryView
33 if historySource != nil {35 if historySource != nil {
34- history = historySource.View()36+ history = historySource.SealAndView()
35 }37 }
36 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}38 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}
37 }39 }
@@ -57,7 +59,7 @@ func (s Snapshot) Row(i int) []renderer.Cell {
57 return nil59 return nil
58 }60 }
59 if i < s.history.Len() {61 if i < s.history.Len() {
60- return s.history.Row(i)62+ return s.history.BorrowedRow(i)
61 }63 }
62 i -= s.history.Len()64 i -= s.history.Len()
63 if i >= s.screen.Height {65 if i >= s.screen.Height {
M internal/usecase/daemon/performance_benchmark_test.go +39 -38
@@ -230,6 +230,10 @@ func BenchmarkDaemonHistorySnapshotRestore(b *testing.B) {
230230 benchmarkDaemonSnapshotRestore(b)
231231 }
232232
233+// BenchmarkDaemonHistoryCopyEnter uses the parent benchmark's repeated-entry
234+// semantics: each operation replaces the active copy document. Exit cleanup is
235+// intentionally outside this benchmark, so its allocations cannot be reported
236+// as copy-entry work.
233237 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {
234238 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })
235239 }
@@ -271,8 +275,8 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
271275 if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
272276 b.Fatal("invalid daemon history fixture")
273277 }
274- if workload == "copy-enter" || workload == "copy-search" {
275- benchmarkDaemonCopyOperation(b, fixture, workload, run)
278+ if workload == "copy-search" {
279+ benchmarkDaemonCopySearch(b, fixture, run)
276280 return
277281 }
278282 b.ReportAllocs()
@@ -295,43 +299,21 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
295299 }
296300 }
297301
298-func benchmarkDaemonCopyOperation(b *testing.B, fixture *performanceFixture, workload string, run func(*performanceFixture)) {
302+func benchmarkDaemonCopySearch(b *testing.B, fixture *performanceFixture, run func(*performanceFixture)) {
299303 b.Helper()
304+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
305+ fixture.ac.ackOutputState(fixture.ac.output.next)
300306 b.ReportAllocs()
301- if workload == "copy-search" {
302- fixture.d.enterCopyMode(fixture.sess, fixture.ac)
303- fixture.ac.ackOutputState(fixture.ac.output.next)
304- b.ResetTimer()
305- for range b.N {
306- run(fixture)
307- fixture.ac.ackOutputState(fixture.ac.output.next)
308- }
309- b.StopTimer()
310- if fixture.searchMatches() == 0 {
311- b.Fatal("copy search produced no deterministic matches")
312- }
313- b.ReportMetric(1, "copyoperations/op")
314- return
315- }
316-
317307 b.ResetTimer()
318- operations := 0
319308 for range b.N {
320- b.StopTimer()
321- fixture.d.exitCopyMode(fixture.ac)
322- fixture.ac.ackOutputState(fixture.ac.output.next)
323- b.StartTimer()
324309 run(fixture)
325- b.StopTimer()
326- if !fixture.copyModeActive() {
327- b.Fatal("copy enter did not install a copy mode")
328- }
329- operations++
310+ fixture.ac.ackOutputState(fixture.ac.output.next)
330311 }
331- if operations != b.N {
332- b.Fatalf("copy workload ran %d operations for %d iterations", operations, b.N)
312+ b.StopTimer()
313+ if fixture.searchMatches() == 0 {
314+ b.Fatal("copy search produced no deterministic matches")
333315 }
334- b.ReportMetric(float64(operations)/float64(b.N), "copyoperations/op")
316+ b.ReportMetric(1, "copyoperations/op")
335317 }
336318
337319 func benchmarkDaemonSnapshotCapture(b *testing.B) {
@@ -541,6 +523,31 @@ func TestLivePaintAllocationBudget(t *testing.T) {
541523 }
542524 }
543525
526+// TestCopyEnterAllocationBudget protects the parent baseline plus 10%. Copy
527+// rendering borrows sealed VT history rows; allocating a copy per viewport row
528+// is a production regression even though the capture itself remains immutable.
529+func TestCopyEnterAllocationBudget(t *testing.T) {
530+ for _, tt := range []struct {
531+ name string
532+ tabs, panes, max int
533+ }{
534+ {name: "1tab-1pane", tabs: 1, panes: 1, max: 38},
535+ {name: "1tab-4panes", tabs: 1, panes: 4, max: 43},
536+ {name: "4tabs-1pane", tabs: 4, panes: 1, max: 42},
537+ {name: "4tabs-4panes", tabs: 4, panes: 4, max: 48},
538+ {name: "8tabs-1pane", tabs: 8, panes: 1, max: 50},
539+ } {
540+ t.Run(tt.name, func(t *testing.T) {
541+ fixture := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: tt.tabs, panes: tt.panes, historyRows: 10_000})
542+ allocs := testing.AllocsPerRun(20, func() {
543+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
544+ fixture.ac.ackOutputState(fixture.ac.output.next)
545+ })
546+ require.LessOrEqual(t, allocs, float64(tt.max), "copy enter must stay within 10%% of the parent allocation baseline")
547+ })
548+ }
549+}
550+
544551 func TestPerformanceFixturePaintLiveUsesPrecomputedAlternatingWrites(t *testing.T) {
545552 fixture := newPerformanceFixture(t, performanceConfig{})
546553
@@ -853,12 +860,6 @@ func (f *performanceFixture) hasHistoryTopology(tabs, panes, historyRows int) bo
853860 return true
854861 }
855862
856-func (f *performanceFixture) copyModeActive() bool {
857- f.ac.overlays.copyMu.Lock()
858- defer f.ac.overlays.copyMu.Unlock()
859- return f.ac.overlays.copyMode != nil
860-}
861-
862863 func (f *performanceFixture) searchMatches() int {
863864 f.ac.overlays.copyMu.Lock()
864865 defer f.ac.overlays.copyMu.Unlock()
@@ -230,6 +230,10 @@ func BenchmarkDaemonHistorySnapshotRestore(b *testing.B) {
230 benchmarkDaemonSnapshotRestore(b)230 benchmarkDaemonSnapshotRestore(b)
231 }231 }
232 232
233+// BenchmarkDaemonHistoryCopyEnter uses the parent benchmark's repeated-entry
234+// semantics: each operation replaces the active copy document. Exit cleanup is
235+// intentionally outside this benchmark, so its allocations cannot be reported
236+// as copy-entry work.
233 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {237 func BenchmarkDaemonHistoryCopyEnter(b *testing.B) {
234 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })238 benchmarkDaemonLargeHistory(b, "copy-enter", func(f *performanceFixture) { f.d.enterCopyMode(f.sess, f.ac) })
235 }239 }
@@ -271,8 +275,8 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
271 if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {275 if !fixture.hasHistoryTopology(topology.tabs, topology.panes, 10_000) {
272 b.Fatal("invalid daemon history fixture")276 b.Fatal("invalid daemon history fixture")
273 }277 }
274- if workload == "copy-enter" || workload == "copy-search" {278+ if workload == "copy-search" {
275- benchmarkDaemonCopyOperation(b, fixture, workload, run)279+ benchmarkDaemonCopySearch(b, fixture, run)
276 return280 return
277 }281 }
278 b.ReportAllocs()282 b.ReportAllocs()
@@ -295,43 +299,21 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
295 }299 }
296 }300 }
297 301
298-func benchmarkDaemonCopyOperation(b *testing.B, fixture *performanceFixture, workload string, run func(*performanceFixture)) {302+func benchmarkDaemonCopySearch(b *testing.B, fixture *performanceFixture, run func(*performanceFixture)) {
299 b.Helper()303 b.Helper()
304+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
305+ fixture.ac.ackOutputState(fixture.ac.output.next)
300 b.ReportAllocs()306 b.ReportAllocs()
301- if workload == "copy-search" {
302- fixture.d.enterCopyMode(fixture.sess, fixture.ac)
303- fixture.ac.ackOutputState(fixture.ac.output.next)
304- b.ResetTimer()
305- for range b.N {
306- run(fixture)
307- fixture.ac.ackOutputState(fixture.ac.output.next)
308- }
309- b.StopTimer()
310- if fixture.searchMatches() == 0 {
311- b.Fatal("copy search produced no deterministic matches")
312- }
313- b.ReportMetric(1, "copyoperations/op")
314- return
315- }
316-
317 b.ResetTimer()307 b.ResetTimer()
318- operations := 0
319 for range b.N {308 for range b.N {
320- b.StopTimer()
321- fixture.d.exitCopyMode(fixture.ac)
322- fixture.ac.ackOutputState(fixture.ac.output.next)
323- b.StartTimer()
324 run(fixture)309 run(fixture)
325- b.StopTimer()310+ fixture.ac.ackOutputState(fixture.ac.output.next)
326- if !fixture.copyModeActive() {
327- b.Fatal("copy enter did not install a copy mode")
328- }
329- operations++
330 }311 }
331- if operations != b.N {312+ b.StopTimer()
332- b.Fatalf("copy workload ran %d operations for %d iterations", operations, b.N)313+ if fixture.searchMatches() == 0 {
314+ b.Fatal("copy search produced no deterministic matches")
333 }315 }
334- b.ReportMetric(float64(operations)/float64(b.N), "copyoperations/op")316+ b.ReportMetric(1, "copyoperations/op")
335 }317 }
336 318
337 func benchmarkDaemonSnapshotCapture(b *testing.B) {319 func benchmarkDaemonSnapshotCapture(b *testing.B) {
@@ -541,6 +523,31 @@ func TestLivePaintAllocationBudget(t *testing.T) {
541 }523 }
542 }524 }
543 525
526+// TestCopyEnterAllocationBudget protects the parent baseline plus 10%. Copy
527+// rendering borrows sealed VT history rows; allocating a copy per viewport row
528+// is a production regression even though the capture itself remains immutable.
529+func TestCopyEnterAllocationBudget(t *testing.T) {
530+ for _, tt := range []struct {
531+ name string
532+ tabs, panes, max int
533+ }{
534+ {name: "1tab-1pane", tabs: 1, panes: 1, max: 38},
535+ {name: "1tab-4panes", tabs: 1, panes: 4, max: 43},
536+ {name: "4tabs-1pane", tabs: 4, panes: 1, max: 42},
537+ {name: "4tabs-4panes", tabs: 4, panes: 4, max: 48},
538+ {name: "8tabs-1pane", tabs: 8, panes: 1, max: 50},
539+ } {
540+ t.Run(tt.name, func(t *testing.T) {
541+ fixture := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 120, Rows: 40}, tabs: tt.tabs, panes: tt.panes, historyRows: 10_000})
542+ allocs := testing.AllocsPerRun(20, func() {
543+ fixture.d.enterCopyMode(fixture.sess, fixture.ac)
544+ fixture.ac.ackOutputState(fixture.ac.output.next)
545+ })
546+ require.LessOrEqual(t, allocs, float64(tt.max), "copy enter must stay within 10%% of the parent allocation baseline")
547+ })
548+ }
549+}
550+
544 func TestPerformanceFixturePaintLiveUsesPrecomputedAlternatingWrites(t *testing.T) {551 func TestPerformanceFixturePaintLiveUsesPrecomputedAlternatingWrites(t *testing.T) {
545 fixture := newPerformanceFixture(t, performanceConfig{})552 fixture := newPerformanceFixture(t, performanceConfig{})
546 553
@@ -853,12 +860,6 @@ func (f *performanceFixture) hasHistoryTopology(tabs, panes, historyRows int) bo
853 return true860 return true
854 }861 }
855 862
856-func (f *performanceFixture) copyModeActive() bool {
857- f.ac.overlays.copyMu.Lock()
858- defer f.ac.overlays.copyMu.Unlock()
859- return f.ac.overlays.copyMode != nil
860-}
861-
862 func (f *performanceFixture) searchMatches() int {863 func (f *performanceFixture) searchMatches() int {
863 f.ac.overlays.copyMu.Lock()864 f.ac.overlays.copyMu.Lock()
864 defer f.ac.overlays.copyMu.Unlock()865 defer f.ac.overlays.copyMu.Unlock()
M pkg/vt/history.go +8 -1
@@ -143,12 +143,19 @@ func (v HistoryView) Range(yield func([]renderer.Cell) bool) {
143143
144144 // Row returns a copy of the row at i, or nil when i is out of range.
145145 func (v HistoryView) Row(i int) []renderer.Cell {
146+ return append([]renderer.Cell(nil), v.BorrowedRow(i)...)
147+}
148+
149+// BorrowedRow returns immutable storage for the row at i, or nil when i is out
150+// of range. The caller must not mutate or retain the result after it no longer
151+// retains v. Consumers that need ownership should use Row instead.
152+func (v HistoryView) BorrowedRow(i int) []renderer.Cell {
146153 if i < 0 {
147154 return nil
148155 }
149156 for _, chunk := range v.chunks {
150157 if i < len(chunk.rows) {
151- return append([]renderer.Cell(nil), chunk.rows[i]...)
158+ return chunk.rows[i]
152159 }
153160 i -= len(chunk.rows)
154161 }
@@ -143,12 +143,19 @@ func (v HistoryView) Range(yield func([]renderer.Cell) bool) {
143 143
144 // Row returns a copy of the row at i, or nil when i is out of range.144 // Row returns a copy of the row at i, or nil when i is out of range.
145 func (v HistoryView) Row(i int) []renderer.Cell {145 func (v HistoryView) Row(i int) []renderer.Cell {
146+ return append([]renderer.Cell(nil), v.BorrowedRow(i)...)
147+}
148+
149+// BorrowedRow returns immutable storage for the row at i, or nil when i is out
150+// of range. The caller must not mutate or retain the result after it no longer
151+// retains v. Consumers that need ownership should use Row instead.
152+func (v HistoryView) BorrowedRow(i int) []renderer.Cell {
146 if i < 0 {153 if i < 0 {
147 return nil154 return nil
148 }155 }
149 for _, chunk := range v.chunks {156 for _, chunk := range v.chunks {
150 if i < len(chunk.rows) {157 if i < len(chunk.rows) {
151- return append([]renderer.Cell(nil), chunk.rows[i]...)158+ return chunk.rows[i]
152 }159 }
153 i -= len(chunk.rows)160 i -= len(chunk.rows)
154 }161 }

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