detached 25d9db5

perf(copy): gate large-history allocations

commit
25d9db5d5a6d5563cddae780982ab63c3bc61999
author
brice · · 2026-07-11 06:24 UTC
parent
70ae2be
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +61 -0
M docs/performance.md +18 -0
@@ -59,6 +59,24 @@ context:
5959 } > /tmp/vev-history-before.txt 2>&1
6060 ```
6161
62+## Immutable document allocation gate
63+
64+The copy-mode document retains an immutable view of existing scrollback rows and
65+clones only the visible frame. Visual-search models share that document; cloning
66+a model copies only its mutable input and match state. Check those two paths at
67+10,000 history rows with:
68+
69+```sh
70+go test ./internal/usecase/copy ./internal/usecase/visualsearch -run '^$' -bench '^(BenchmarkNewSnapshot10KRows|BenchmarkVisualSearchClone10KRows)$' -benchtime=1s -count=5 -benchmem
71+```
72+
73+Treat `allocs/op` and `B/op` as allocation gates: compare the complete repeated
74+output with the accepted baseline before merging, and investigate any increase.
75+This scope does not yet include an incremental/full-text search index: queries
76+still scan the immutable document and produce a fresh match list. Coordinator
77+behavior, chunk encoding, compression, GC tuning, and real Unix-socket, SSH, or
78+UDP impairment/latency remain out of scope.
79+
6280 ## Lower-level smoke baseline
6381
6482 Use this command to check renderer, VT, IPC, and daemon microbenchmarks without
@@ -59,6 +59,24 @@ context:
59 } > /tmp/vev-history-before.txt 2>&159 } > /tmp/vev-history-before.txt 2>&1
60 ```60 ```
61 61
62+## Immutable document allocation gate
63+
64+The copy-mode document retains an immutable view of existing scrollback rows and
65+clones only the visible frame. Visual-search models share that document; cloning
66+a model copies only its mutable input and match state. Check those two paths at
67+10,000 history rows with:
68+
69+```sh
70+go test ./internal/usecase/copy ./internal/usecase/visualsearch -run '^$' -bench '^(BenchmarkNewSnapshot10KRows|BenchmarkVisualSearchClone10KRows)$' -benchtime=1s -count=5 -benchmem
71+```
72+
73+Treat `allocs/op` and `B/op` as allocation gates: compare the complete repeated
74+output with the accepted baseline before merging, and investigate any increase.
75+This scope does not yet include an incremental/full-text search index: queries
76+still scan the immutable document and produce a fresh match list. Coordinator
77+behavior, chunk encoding, compression, GC tuning, and real Unix-socket, SSH, or
78+UDP impairment/latency remain out of scope.
79+
62 ## Lower-level smoke baseline80 ## Lower-level smoke baseline
63 81
64 Use this command to check renderer, VT, IPC, and daemon microbenchmarks without82 Use this command to check renderer, VT, IPC, and daemon microbenchmarks without
M internal/usecase/copy/mode_test.go +25 -0
@@ -10,6 +10,8 @@ import (
1010 "github.com/stretchr/testify/require"
1111 )
1212
13+var benchmarkSnapshotSink Snapshot
14+
1315 func snapshot(lines []string, height int) Snapshot {
1416 rows := make([][]renderer.Cell, len(lines))
1517 for i, line := range lines {
@@ -476,6 +478,29 @@ func TestOSC52FromBase64(t *testing.T) {
476478 }
477479 }
478480
481+func BenchmarkNewSnapshot10KRows(b *testing.B) {
482+ const (
483+ width = 120
484+ height = 40
485+ historyRows = 10_000
486+ )
487+
488+ scrollback := NewScrollback(historyRows)
489+ for range historyRows {
490+ scrollback.Append(row(strings.Repeat("x", width)))
491+ }
492+ screen := renderer.NewFrame(width, height)
493+ for y := range height {
494+ copy(screen.Row(y), row(strings.Repeat("s", width)))
495+ }
496+
497+ b.ReportAllocs()
498+ b.ResetTimer()
499+ for b.Loop() {
500+ benchmarkSnapshotSink = NewSnapshot(scrollback, screen)
501+ }
502+}
503+
479504 func decodeOSC52Chunk(t *testing.T, chunk []byte) []byte {
480505 t.Helper()
481506 s := string(chunk)
@@ -10,6 +10,8 @@ import (
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 )11 )
12 12
13+var benchmarkSnapshotSink Snapshot
14+
13 func snapshot(lines []string, height int) Snapshot {15 func snapshot(lines []string, height int) Snapshot {
14 rows := make([][]renderer.Cell, len(lines))16 rows := make([][]renderer.Cell, len(lines))
15 for i, line := range lines {17 for i, line := range lines {
@@ -476,6 +478,29 @@ func TestOSC52FromBase64(t *testing.T) {
476 }478 }
477 }479 }
478 480
481+func BenchmarkNewSnapshot10KRows(b *testing.B) {
482+ const (
483+ width = 120
484+ height = 40
485+ historyRows = 10_000
486+ )
487+
488+ scrollback := NewScrollback(historyRows)
489+ for range historyRows {
490+ scrollback.Append(row(strings.Repeat("x", width)))
491+ }
492+ screen := renderer.NewFrame(width, height)
493+ for y := range height {
494+ copy(screen.Row(y), row(strings.Repeat("s", width)))
495+ }
496+
497+ b.ReportAllocs()
498+ b.ResetTimer()
499+ for b.Loop() {
500+ benchmarkSnapshotSink = NewSnapshot(scrollback, screen)
501+ }
502+}
503+
479 func decodeOSC52Chunk(t *testing.T, chunk []byte) []byte {504 func decodeOSC52Chunk(t *testing.T, chunk []byte) []byte {
480 t.Helper()505 t.Helper()
481 s := string(chunk)506 s := string(chunk)
M internal/usecase/visualsearch/model_test.go +18 -0
@@ -11,6 +11,8 @@ import (
1111 "github.com/bnema/vev/pkg/renderer"
1212 )
1313
14+var benchmarkModelSink *Model
15+
1416 func testSnapshot(lines ...string) scopy.Snapshot {
1517 rows := make([][]renderer.Cell, len(lines))
1618 for i, line := range lines {
@@ -105,6 +107,22 @@ func TestVisualSearchRenderShowsInputAndLineResults(t *testing.T) {
105107 require.Contains(t, text, "2:6 beta alpha")
106108 }
107109
110+func BenchmarkVisualSearchClone10KRows(b *testing.B) {
111+ const historyRows = 10_000
112+
113+ lines := make([]string, historyRows)
114+ for i := range lines {
115+ lines[i] = "immutable history row"
116+ }
117+ model := New(testSnapshot(lines...))
118+
119+ b.ReportAllocs()
120+ b.ResetTimer()
121+ for b.Loop() {
122+ benchmarkModelSink = model.Clone()
123+ }
124+}
125+
108126 func rowText(row []renderer.Cell) string {
109127 var b strings.Builder
110128 for _, c := range row {
@@ -11,6 +11,8 @@ import (
11 "github.com/bnema/vev/pkg/renderer"11 "github.com/bnema/vev/pkg/renderer"
12 )12 )
13 13
14+var benchmarkModelSink *Model
15+
14 func testSnapshot(lines ...string) scopy.Snapshot {16 func testSnapshot(lines ...string) scopy.Snapshot {
15 rows := make([][]renderer.Cell, len(lines))17 rows := make([][]renderer.Cell, len(lines))
16 for i, line := range lines {18 for i, line := range lines {
@@ -105,6 +107,22 @@ func TestVisualSearchRenderShowsInputAndLineResults(t *testing.T) {
105 require.Contains(t, text, "2:6 beta alpha")107 require.Contains(t, text, "2:6 beta alpha")
106 }108 }
107 109
110+func BenchmarkVisualSearchClone10KRows(b *testing.B) {
111+ const historyRows = 10_000
112+
113+ lines := make([]string, historyRows)
114+ for i := range lines {
115+ lines[i] = "immutable history row"
116+ }
117+ model := New(testSnapshot(lines...))
118+
119+ b.ReportAllocs()
120+ b.ResetTimer()
121+ for b.Loop() {
122+ benchmarkModelSink = model.Clone()
123+ }
124+}
125+
108 func rowText(row []renderer.Cell) string {126 func rowText(row []renderer.Cell) string {
109 var b strings.Builder127 var b strings.Builder
110 for _, c := range row {128 for _, c := range row {

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