v0.7.0 36c429d

benchmark_test.go RawHistoryFolder

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

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