1package vt
2
3import (
4 "bytes"
5 "testing"
6
7 "github.com/bnema/vev/pkg/renderer"
8)
9
10var (
11 benchmarkHistoryViewSink HistoryView
12 benchmarkHistorySnapshotSink HistorySnapshotView
13 benchmarkHistoryBlobSink []byte
14 benchmarkHistoryBlobsSink [][]byte
15)
16
17// TestHistoryViewPartialTailAllocationBounded guards the capture property used
18// by persistence: retained sealed chunks are shared, while only the small
19// mutable tail is copied into a view.
20func TestHistoryViewPartialTailAllocationBounded(t *testing.T) {
21 const (
22 width = 120
23 chunkRows = 64
24 tailRows = 7
25 )
26 sealed := benchmarkHistory(t, chunkRows*4, width, chunkRows)
27 sealed.SealAndView()
28 partial := benchmarkHistory(t, chunkRows*4+tailRows, width, chunkRows)
29 partialBefore := partial.View()
30
31 // Copying a partial tail costs one allocation per row plus a fixed set of
32 // headers: the chunk slice, its growth for the tail chunk, the chunk itself,
33 // the row slice, and the bounds slice parallel to it.
34 const tailHeaders = 4
35
36 sealedAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = sealed.View() })
37 partialAllocs := testing.AllocsPerRun(20, func() { benchmarkHistoryViewSink = partial.View() })
38 if partialAllocs > sealedAllocs+tailRows+tailHeaders {
39 t.Fatalf("partial-tail View allocations = %v, sealed=%v, tail rows=%d; sealed history was copied", partialAllocs, sealedAllocs, tailRows)
40 }
41 if partial.View().Chunk(0) != partialBefore.Chunk(0) {
42 t.Fatal("unchanged sealed chunks must retain identity across partial-tail views")
43 }
44}
45
46func BenchmarkHistoryView10KRowsSealedReuse(b *testing.B) {
47 history := benchmarkHistory(b, 10_000, 120, 256)
48 sealed := history.SealAndView()
49 if sealed.Len() != 10_000 || sealed.ChunkCount() != 40 || sealed.Chunk(0) == nil {
50 b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", sealed.Len(), sealed.ChunkCount())
51 }
52
53 b.ReportAllocs()
54 b.ResetTimer()
55 for b.Loop() {
56 benchmarkHistoryViewSink = history.View()
57 }
58 b.StopTimer()
59 if got := benchmarkHistoryViewSink; got.Len() != sealed.Len() || got.Chunk(0) != sealed.Chunk(0) {
60 b.Fatal("unchanged sealed history chunks were not reused")
61 }
62 b.ReportMetric(float64(sealed.ChunkCount()), "sealedchunks/view")
63}
64
65func BenchmarkHistoryView10KRowsPartialTail(b *testing.B) {
66 history := benchmarkHistory(b, 10_000, 120, 256)
67 before := history.View()
68 if before.Len() != 10_000 || before.ChunkCount() != 40 || before.Chunk(0) == nil {
69 b.Fatalf("invalid partial-tail history fixture: rows=%d chunks=%d", before.Len(), before.ChunkCount())
70 }
71
72 b.ReportAllocs()
73 b.ResetTimer()
74 for b.Loop() {
75 benchmarkHistoryViewSink = history.View()
76 }
77 b.StopTimer()
78 if got := benchmarkHistoryViewSink; got.Len() != before.Len() || got.Chunk(0) != before.Chunk(0) {
79 b.Fatal("partial-tail capture did not reuse its sealed chunks")
80 }
81 b.ReportMetric(float64(10_000%256), "tailrows/view")
82}
83
84func BenchmarkHistorySnapshotView10KRowsPartialTail(b *testing.B) {
85 history := benchmarkHistory(b, 10_000, 120, 256)
86 if got, want := len(history.tail), 16; got != want {
87 b.Fatalf("partial tail rows = %d, want %d", got, want)
88 }
89 sealed := history.View().Chunk(0)
90
91 b.ReportAllocs()
92 b.ResetTimer()
93 for b.Loop() {
94 benchmarkHistorySnapshotSink = history.SnapshotView()
95 }
96 b.StopTimer()
97 if got := benchmarkHistorySnapshotSink; got.Chunk(0) != sealed || got.Tail().Len() != 16 {
98 b.Fatal("snapshot did not preserve chunk identity and tail rows")
99 }
100 b.ReportMetric(float64(benchmarkHistorySnapshotSink.ChunkCount()), "sealedchunks/view")
101}
102
103func BenchmarkMarshalHistoryChunk256x120(b *testing.B) {
104 history := benchmarkHistory(b, 256, 120, 256)
105 chunk := history.SealAndView().Chunk(0)
106 if chunk == nil {
107 b.Fatal("invalid history chunk fixture")
108 }
109
110 b.ReportAllocs()
111 b.ResetTimer()
112 for b.Loop() {
113 var err error
114 benchmarkHistoryBlobSink, err = MarshalHistoryChunk(chunk)
115 if err != nil {
116 b.Fatal(err)
117 }
118 }
119 b.StopTimer()
120 if _, err := PreflightHistoryBlob(benchmarkHistoryBlobSink); err != nil {
121 b.Fatalf("encoded history chunk is invalid: %v", err)
122 }
123 b.SetBytes(int64(len(benchmarkHistoryBlobSink)))
124}
125
126func BenchmarkUnmarshalHistoryChunk256x120(b *testing.B) {
127 history := benchmarkHistory(b, 256, 120, 256)
128 blob, err := MarshalHistoryChunk(history.SealAndView().Chunk(0))
129 if err != nil {
130 b.Fatal(err)
131 }
132 if stats, err := PreflightHistoryBlob(blob); err != nil || stats.Rows != 256 || stats.Cells != 256*120 {
133 b.Fatalf("invalid encoded history chunk fixture: stats=%+v err=%v", stats, err)
134 }
135
136 b.ReportAllocs()
137 b.SetBytes(int64(len(blob)))
138 b.ResetTimer()
139 for b.Loop() {
140 var err error
141 benchmarkHistoryViewSink, err = UnmarshalHistory(blob)
142 if err != nil {
143 b.Fatal(err)
144 }
145 }
146 b.StopTimer()
147 if benchmarkHistoryViewSink.Len() != 256 || benchmarkHistoryViewSink.ChunkCount() != 1 {
148 b.Fatal("decoded history chunk lost rows")
149 }
150}
151
152func BenchmarkMarshalSealedHistory10KRows(b *testing.B) {
153 history := benchmarkHistory(b, 10_000, 120, 256)
154 view := history.SealAndView()
155 if view.Len() != 10_000 || view.ChunkCount() != 40 {
156 b.Fatalf("invalid sealed history fixture: rows=%d chunks=%d", view.Len(), view.ChunkCount())
157 }
158
159 b.ReportAllocs()
160 b.ResetTimer()
161 for b.Loop() {
162 var err error
163 benchmarkHistoryBlobsSink, benchmarkHistoryBlobSink, err = MarshalSealedHistory(view)
164 if err != nil {
165 b.Fatal(err)
166 }
167 }
168 b.StopTimer()
169 if len(benchmarkHistoryBlobsSink) != view.ChunkCount() || len(benchmarkHistoryBlobSink) == 0 {
170 b.Fatal("sealed-history encoder produced an incomplete capture")
171 }
172 b.ReportMetric(float64(len(benchmarkHistoryBlobsSink)), "sealedchunks/op")
173}
174
175func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
176 t.Helper()
177 history := NewHistory(HistoryConfig{MaxRows: rows, ChunkRows: chunkRows})
178 for row := range rows {
179 cells := make([]renderer.Cell, width)
180 for col := range cells {
181 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
182 }
183 requireHistoryAppend(t, history, cells)
184 }
185 return history
186}
187
188// BenchmarkScreenResizeReflowViewport verifies resize only visits the live
189// viewport. The 10K-history variant must track the control rather than history
190// length; history is deliberately populated without changing the viewport.
191func BenchmarkScreenResizeReflowViewport(b *testing.B) {
192 b.Run("control", func(b *testing.B) { benchmarkScreenResizeReflow(b, 0) })
193 b.Run("10k-history", func(b *testing.B) { benchmarkScreenResizeReflow(b, 10_000) })
194}
195
196func benchmarkScreenResizeReflow(b *testing.B, historyRows int) {
197 b.Helper()
198 s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500})
199 if historyRows > 0 {
200 for range historyRows {
201 requireHistoryAppend(b, s.history, make([]renderer.Cell, 120))
202 }
203 }
204 s.Write(bytes.Repeat([]byte("x"), 120*39))
205 s.ClearDamage()
206 sizes := [2][2]int{{80, 40}, {120, 40}}
207
208 b.ReportAllocs()
209 b.ResetTimer()
210 for i := 0; b.Loop(); i++ {
211 s.Resize(sizes[i%len(sizes)][0], sizes[i%len(sizes)][1])
212 s.ClearDamage()
213 }
214 b.StopTimer()
215 if got := s.History(); historyRows > 0 && got.Len() < historyRows {
216 b.Fatalf("resize discarded retained history rows: got %d want at least %d", got.Len(), historyRows)
217 }
218}
219
220func BenchmarkScreenShellRedrawBurst(b *testing.B) {
221 chunk := []byte("\r\x1b[K❯ abc\x1b[90m autosuggestion\x1b[39m\r\x1b[5C")
222 b.ReportAllocs()
223 for b.Loop() {
224 s := NewScreen(120, 40)
225 for range 200 {
226 s.Write(chunk)
227 s.ClearDamage()
228 }
229 }
230}
231
232func BenchmarkScreenFullscreenScrollRegion(b *testing.B) {
233 chunk := []byte("\x1b[2;39r\x1b[39;1Hline\n")
234 b.ReportAllocs()
235 for b.Loop() {
236 s := NewScreen(120, 40)
237 for range 500 {
238 s.Write(chunk)
239 s.ClearDamage()
240 }
241 }
242}