v0.4.1 03674e0

benchmark_test.go RawHistoryFolder

1package vt
2
3import (
4	"bytes"
5	"encoding/base64"
6	"testing"
7
8	renderer "github.com/bnema/vev-vt/core"
9)
10
11var (
12	benchmarkHistoryViewSink     HistoryView
13	benchmarkHistorySnapshotSink HistorySnapshotView
14	benchmarkDamageCaptureSink   DamageCapture
15	benchmarkHistoryBlobSink     []byte
16	benchmarkHistoryBlobsSink    [][]byte
17)
18
19// TestHistoryViewPartialTailAllocationBounded guards the capture property used
20// by persistence: retained sealed chunks are shared, while only the small
21// mutable tail is copied into a view.
22func TestHistoryViewPartialTailAllocationBounded(t *testing.T) {
23	const (
24		width     = 120
25		chunkRows = 64
26		tailRows  = 7
27	)
28	sealed := benchmarkHistory(t, chunkRows*4, width, chunkRows)
29	sealed.SealAndView()
30	partial := benchmarkHistory(t, chunkRows*4+tailRows, width, chunkRows)
31	partialBefore := partial.View()
32
33	// Copying a partial tail costs one allocation per row plus a fixed set of
34	// headers: the chunk slice, its growth for the tail chunk, the chunk itself,
35	// the row slice, and the bounds slice parallel to it.
36	const tailHeaders = 4
37
38	sealedAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = sealed.View() })
39	partialAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = partial.View() })
40	if partialAllocs > sealedAllocs+tailRows+tailHeaders {
41		t.Fatalf("partial-tail View allocations = %v, sealed=%v, tail rows=%d; sealed history was copied", partialAllocs, sealedAllocs, tailRows)
42	}
43	if partial.View().Chunk(0) != partialBefore.Chunk(0) {
44		t.Fatal("unchanged sealed chunks must retain identity across partial-tail views")
45	}
46}
47
48func BenchmarkHistoryView10KRowsSealedReuse(b *testing.B) {
49	history := benchmarkHistory(b, 10_000, 120, 256)
50	sealed := history.SealAndView()
51	if sealed.Len() != 10_000 || sealed.ChunkCount() != 40 || sealed.Chunk(0) == nil {
52		b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", sealed.Len(), sealed.ChunkCount())
53	}
54
55	b.ReportAllocs()
56	b.ResetTimer()
57	for b.Loop() {
58		benchmarkHistoryViewSink = history.View()
59	}
60	b.StopTimer()
61	if got := benchmarkHistoryViewSink; got.Len() != sealed.Len() || got.Chunk(0) != sealed.Chunk(0) {
62		b.Fatal("unchanged sealed history chunks were not reused")
63	}
64	b.ReportMetric(float64(sealed.ChunkCount()), "sealedchunks/view")
65}
66
67func BenchmarkHistoryView10KRowsPartialTail(b *testing.B) {
68	history := benchmarkHistory(b, 10_000, 120, 256)
69	before := history.View()
70	if before.Len() != 10_000 || before.ChunkCount() != 40 || before.Chunk(0) == nil {
71		b.Fatalf("invalid partial-tail history fixture: rows=%d chunks=%d", before.Len(), before.ChunkCount())
72	}
73
74	b.ReportAllocs()
75	b.ResetTimer()
76	for b.Loop() {
77		benchmarkHistoryViewSink = history.View()
78	}
79	b.StopTimer()
80	if got := benchmarkHistoryViewSink; got.Len() != before.Len() || got.Chunk(0) != before.Chunk(0) {
81		b.Fatal("partial-tail capture did not reuse its sealed chunks")
82	}
83	b.ReportMetric(float64(10_000%256), "tailrows/view")
84}
85
86func BenchmarkHistorySnapshotView10KRowsPartialTail(b *testing.B) {
87	history := benchmarkHistory(b, 10_000, 120, 256)
88	if got, want := len(history.tail), 16; got != want {
89		b.Fatalf("partial tail rows = %d, want %d", got, want)
90	}
91	sealed := history.View().Chunk(0)
92
93	b.ReportAllocs()
94	b.ResetTimer()
95	for b.Loop() {
96		benchmarkHistorySnapshotSink = history.SnapshotView()
97	}
98	b.StopTimer()
99	if got := benchmarkHistorySnapshotSink; got.Chunk(0) != sealed || got.Tail().Len() != 16 {
100		b.Fatal("snapshot did not preserve chunk identity and tail rows")
101	}
102	b.ReportMetric(float64(benchmarkHistorySnapshotSink.ChunkCount()), "sealedchunks/view")
103}
104
105func BenchmarkMarshalHistoryChunk256x120(b *testing.B) {
106	history := benchmarkHistory(b, 256, 120, 256)
107	chunk := history.SealAndView().Chunk(0)
108	if chunk == nil {
109		b.Fatal("invalid history chunk fixture")
110	}
111
112	b.ReportAllocs()
113	b.ResetTimer()
114	for b.Loop() {
115		var err error
116		benchmarkHistoryBlobSink, err = MarshalHistoryChunk(chunk)
117		if err != nil {
118			b.Fatal(err)
119		}
120	}
121	b.StopTimer()
122	if _, err := PreflightHistoryBlob(benchmarkHistoryBlobSink); err != nil {
123		b.Fatalf("encoded history chunk is invalid: %v", err)
124	}
125	b.SetBytes(int64(len(benchmarkHistoryBlobSink)))
126}
127
128func BenchmarkUnmarshalHistoryChunk256x120(b *testing.B) {
129	history := benchmarkHistory(b, 256, 120, 256)
130	blob, err := MarshalHistoryChunk(history.SealAndView().Chunk(0))
131	if err != nil {
132		b.Fatal(err)
133	}
134	if stats, err := PreflightHistoryBlob(blob); err != nil || stats.Rows != 256 || stats.Cells != 256*120 {
135		b.Fatalf("invalid encoded history chunk fixture: stats=%+v err=%v", stats, err)
136	}
137
138	b.ReportAllocs()
139	b.SetBytes(int64(len(blob)))
140	b.ResetTimer()
141	for b.Loop() {
142		var err error
143		benchmarkHistoryViewSink, err = UnmarshalHistory(blob)
144		if err != nil {
145			b.Fatal(err)
146		}
147	}
148	b.StopTimer()
149	if benchmarkHistoryViewSink.Len() != 256 || benchmarkHistoryViewSink.ChunkCount() != 1 {
150		b.Fatal("decoded history chunk lost rows")
151	}
152}
153
154func BenchmarkMarshalSealedHistory10KRows(b *testing.B) {
155	history := benchmarkHistory(b, 10_000, 120, 256)
156	view := history.SealAndView()
157	if view.Len() != 10_000 || view.ChunkCount() != 40 {
158		b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", view.Len(), view.ChunkCount())
159	}
160
161	b.ReportAllocs()
162	b.ResetTimer()
163	for b.Loop() {
164		var err error
165		benchmarkHistoryBlobsSink, benchmarkHistoryBlobSink, err = MarshalSealedHistory(view)
166		if err != nil {
167			b.Fatal(err)
168		}
169	}
170	b.StopTimer()
171	if len(benchmarkHistoryBlobsSink) != view.ChunkCount() || len(benchmarkHistoryBlobSink) == 0 {
172		b.Fatal("sealed-history encoder produced an incomplete capture")
173	}
174	b.ReportMetric(float64(len(benchmarkHistoryBlobsSink)), "sealedchunks/op")
175}
176
177func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
178	t.Helper()
179	history := NewHistory(HistoryConfig{MaxRows: rows, ChunkRows: chunkRows})
180	for row := range rows {
181		cells := make([]renderer.Cell, width)
182		for col := range cells {
183			cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
184		}
185		requireHistoryAppend(t, history, cells)
186	}
187	return history
188}
189
190// BenchmarkScreenResizeReflowViewport verifies resize only visits the live
191// viewport. The 10K-history variant must track the control rather than history
192// length; history is deliberately populated without changing the viewport.
193func BenchmarkScreenResizeReflowViewport(b *testing.B) {
194	b.Run("control", func(b *testing.B) { benchmarkScreenResizeReflow(b, 0) })
195	b.Run("10k-history", func(b *testing.B) { benchmarkScreenResizeReflow(b, 10_000) })
196}
197
198func benchmarkScreenResizeReflow(b *testing.B, historyRows int) {
199	b.Helper()
200	s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500})
201	if historyRows > 0 {
202		for range historyRows {
203			requireHistoryAppend(b, s.history, make([]renderer.Cell, 120))
204		}
205		// History normally receives rows evicted from this screen, whose IDs are
206		// allocated after every existing live or historical row. This fixture
207		// prepopulates history directly, so advance and refresh the live IDs to
208		// preserve that invariant before exercising resize eviction.
209		s.nextRowID = s.history.nextRowID - 1
210		for y := range s.buffer.rowIDs {
211			s.buffer.rowIDs[y] = s.nextRowIDValue()
212		}
213	}
214	s.Write(bytes.Repeat([]byte("x"), 120*39))
215	s.ClearDamage()
216	sizes := [2][2]int{{80, 40}, {120, 40}}
217
218	b.ReportAllocs()
219	b.ResetTimer()
220	for i := 0; b.Loop(); i++ {
221		s.Resize(sizes[i%len(sizes)][0], sizes[i%len(sizes)][1])
222		s.ClearDamage()
223	}
224	b.StopTimer()
225	if got := s.History(); historyRows > 0 && got.Len() < historyRows {
226		b.Fatalf("resize discarded retained history rows: got %d want at least %d", got.Len(), historyRows)
227	}
228}
229
230func BenchmarkScreenPrintableASCII(b *testing.B) {
231	s := NewScreen(120, 40)
232	chunk := append(bytes.Repeat([]byte("x"), 119), '\r')
233	b.SetBytes(int64(len(chunk)))
234	b.ReportAllocs()
235	for b.Loop() {
236		s.Write(chunk)
237		s.ClearDamage()
238	}
239}
240
241func BenchmarkScreenKittyAPC(b *testing.B) {
242	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 256*256*4))
243	apc := []byte("\x1b_Ga=t,i=1,f=32,s=256,v=256,q=2;" + payload + "\x1b\\")
244	b.SetBytes(int64(len(apc)))
245	for _, fragmented := range []bool{false, true} {
246		b.Run(map[bool]string{false: "single-write", true: "byte-by-byte"}[fragmented], func(b *testing.B) {
247			s := NewScreen(120, 40)
248			b.ReportAllocs()
249			for b.Loop() {
250				if fragmented {
251					for _, part := range apc {
252						s.Write([]byte{part})
253					}
254				} else {
255					s.Write(apc)
256				}
257				s.ClearDamage()
258			}
259		})
260	}
261}
262
263func BenchmarkScreenMixedUTF8(b *testing.B) {
264	s := NewScreen(120, 40)
265	chunk := append(bytes.Repeat([]byte("Aé界"), 20), '\r')
266	b.SetBytes(int64(len(chunk)))
267	b.ReportAllocs()
268	for b.Loop() {
269		s.Write(chunk)
270		s.ClearDamage()
271	}
272}
273
274func BenchmarkScreenCSIHeavy(b *testing.B) {
275	s := NewScreen(120, 40)
276	chunk := []byte("\x1b[1;1H\x1b[2K\x1b[31;48;2;1;2;3mstatus\x1b[0m\x1b[10C\x1b[?25l\x1b[?25h\r")
277	b.SetBytes(int64(len(chunk)))
278	b.ReportAllocs()
279	for b.Loop() {
280		s.Write(chunk)
281		s.ClearDamage()
282	}
283}
284
285func BenchmarkScreenCaptureDamage(b *testing.B) {
286	s := NewScreen(120, 40)
287	chunk := []byte("x")
288	s.ClearDamage()
289	b.ReportAllocs()
290	for b.Loop() {
291		s.Write(chunk)
292		benchmarkDamageCaptureSink = s.CaptureDamage()
293		if len(benchmarkDamageCaptureSink.Damage) == 0 {
294			b.Fatal("expected captured damage")
295		}
296		if !s.AcknowledgeDamage(benchmarkDamageCaptureSink.Generation) {
297			b.Fatal("expected current damage acknowledgement")
298		}
299	}
300}
301
302func BenchmarkScreenShellRedrawBurst(b *testing.B) {
303	chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")
304	b.ReportAllocs()
305	for b.Loop() {
306		s := NewScreen(120, 40)
307		for range 200 {
308			s.Write(chunk)
309			s.ClearDamage()
310		}
311	}
312}
313
314func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {
315	chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")
316	b.ReportAllocs()
317	for b.Loop() {
318		s := NewScreen(120, 40)
319		for range 500 {
320			s.Write(chunk)
321			s.ClearDamage()
322		}
323	}
324}

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 2

Tags 13

↑↓ moveenter openesc close