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, bounds, and logical-style metadata parallel to it.
36 const tailHeaders = 6
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 i := range apc {
252 s.Write(apc[i : i+1 : i+1])
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 s := NewScreen(120, 40)
305 s.ClearDamage()
306 b.ReportAllocs()
307 for b.Loop() {
308 for range 200 {
309 s.Write(chunk)
310 s.ClearDamage()
311 }
312 }
313}
314
315func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {
316 chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")
317 s := NewScreen(120, 40)
318 s.ClearDamage()
319 b.ReportAllocs()
320 for b.Loop() {
321 for range 500 {
322 s.Write(chunk)
323 s.ClearDamage()
324 }
325 }
326}