1package vt
2
3import (
4 "runtime"
5 "testing"
6
7 "github.com/bnema/vev-vt/core"
8)
9
10const (
11 storageBenchmarkRows = 10_000
12 storageBenchmarkWidth = 120
13)
14
15var benchmarkStorageHistorySink *History
16
17type storageBenchmarkScenario struct {
18 name string
19 fill func([]core.Cell, int)
20}
21
22func BenchmarkHistoryBuild10Kx120(b *testing.B) {
23 for _, scenario := range historyStorageScenarios() {
24 b.Run(scenario.name, func(b *testing.B) {
25 b.ReportAllocs()
26 for b.Loop() {
27 benchmarkStorageHistorySink = buildStorageBenchmarkHistory(b, scenario.fill)
28 }
29 b.ReportMetric(storageBenchmarkRows*storageBenchmarkWidth, "cells/op")
30 })
31 }
32}
33
34// BenchmarkHistoryRetained10Kx120 isolates each sample with garbage collection
35// and reports the live heap retained by one history. Use -benchtime=1x when
36// comparing representations; elapsed time includes the measurement GCs.
37func BenchmarkHistoryRetained10Kx120(b *testing.B) {
38 for _, scenario := range historyStorageScenarios() {
39 b.Run(scenario.name, func(b *testing.B) {
40 var retained uint64
41 for b.Loop() {
42 benchmarkStorageHistorySink = nil
43 runtime.GC()
44 var before runtime.MemStats
45 runtime.ReadMemStats(&before)
46
47 benchmarkStorageHistorySink = buildStorageBenchmarkHistory(b, scenario.fill)
48 runtime.GC()
49 var after runtime.MemStats
50 runtime.ReadMemStats(&after)
51 if after.HeapAlloc >= before.HeapAlloc {
52 retained += after.HeapAlloc - before.HeapAlloc
53 }
54 }
55 benchmarkStorageHistorySink = nil
56 if b.N > 0 {
57 b.ReportMetric(float64(retained)/float64(b.N), "retained-B/op")
58 }
59 })
60 }
61}
62
63// BenchmarkColdHistoryRetained10Kx120 measures the same scenarios after two
64// bounded idle passes. The newest sealed page and mutable tail remain hot.
65func BenchmarkColdHistoryRetained10Kx120(b *testing.B) {
66 for _, scenario := range historyStorageScenarios() {
67 b.Run(scenario.name, func(b *testing.B) {
68 var retained uint64
69 for b.Loop() {
70 benchmarkStorageHistorySink = nil
71 runtime.GC()
72 var before, after runtime.MemStats
73 runtime.ReadMemStats(&before)
74 h := buildStorageBenchmarkHistory(b, scenario.fill)
75 for range 2 {
76 if _, err := h.CompressIdle(len(h.chunks)); err != nil {
77 b.Fatal(err)
78 }
79 }
80 benchmarkStorageHistorySink = h
81 // Two cycles drain the compressor sync.Pool, so only the
82 // history itself is measured.
83 runtime.GC()
84 runtime.GC()
85 runtime.ReadMemStats(&after)
86 if after.HeapAlloc >= before.HeapAlloc {
87 retained += after.HeapAlloc - before.HeapAlloc
88 }
89 }
90 stats := benchmarkStorageHistorySink.CompressionStats()
91 b.ReportMetric(float64(stats.CompressedBytes), "compressed-B")
92 b.ReportMetric(float64(stats.ColdPages), "cold-pages")
93 b.ReportMetric(float64(retained)/float64(b.N), "retained-B/op")
94 benchmarkStorageHistorySink = nil
95 })
96 }
97}
98
99func historyStorageScenarios() []storageBenchmarkScenario {
100 defaultStyle := core.DefaultStyle()
101 repeatedIndexed := core.Style{
102 Bold: true,
103 Foreground: 196,
104 Background: 17,
105 UnderlineStyle: core.UnderlineSingle,
106 }
107 repeatedRGB := core.Style{
108 Foreground: -1,
109 Background: -1,
110 HasForegroundRGB: true,
111 ForegroundRGB: core.RGB{R: 224, G: 108, B: 117},
112 HasBackgroundRGB: true,
113 BackgroundRGB: core.RGB{R: 40, G: 44, B: 52},
114 }
115 return []storageBenchmarkScenario{
116 {
117 name: "plain-ascii",
118 fill: func(row []core.Cell, y int) {
119 for x := range row {
120 row[x] = core.Cell{Rune: rune('a' + (x+y)%26), Style: defaultStyle}
121 }
122 },
123 },
124 {
125 name: "repeated-indexed-style",
126 fill: func(row []core.Cell, y int) {
127 for x := range row {
128 row[x] = core.Cell{Rune: rune('a' + (x+y)%26), Style: repeatedIndexed}
129 }
130 },
131 },
132 {
133 name: "repeated-rgb-style",
134 fill: func(row []core.Cell, y int) {
135 for x := range row {
136 row[x] = core.Cell{Rune: rune('a' + (x+y)%26), Style: repeatedRGB}
137 }
138 },
139 },
140 {
141 name: "high-cardinality-rgb",
142 fill: func(row []core.Cell, y int) {
143 for x := range row {
144 value := uint32(y*len(row) + x)
145 style := core.Style{
146 Foreground: -1,
147 Background: -1,
148 HasForegroundRGB: true,
149 ForegroundRGB: core.RGB{
150 R: uint8(value >> 16),
151 G: uint8(value >> 8),
152 B: uint8(value),
153 },
154 }
155 row[x] = core.Cell{Rune: 'x', Style: style}
156 }
157 },
158 },
159 {
160 name: "wide-unicode",
161 fill: func(row []core.Cell, _ int) {
162 for x := 0; x < len(row); x += 2 {
163 row[x] = core.Cell{Rune: '界', Style: defaultStyle}
164 row[x+1] = core.Cell{Style: defaultStyle, Continuation: true}
165 }
166 },
167 },
168 {
169 name: "styled-blanks",
170 fill: func(row []core.Cell, y int) {
171 style := core.Style{Foreground: -1, Background: (y % 16) + 232}
172 for x := range row {
173 row[x] = core.Cell{Rune: ' ', Style: style}
174 }
175 },
176 },
177 }
178}
179
180func buildStorageBenchmarkHistory(b *testing.B, fill func([]core.Cell, int)) *History {
181 b.Helper()
182 history := NewHistory(HistoryConfig{
183 MaxRows: storageBenchmarkRows,
184 MaxBytes: 1 << 30,
185 ChunkRows: 256,
186 })
187 row := make([]core.Cell, storageBenchmarkWidth)
188 for y := range storageBenchmarkRows {
189 fill(row, y)
190 if err := history.Append(row, LineBound{End: len(row)}); err != nil {
191 b.Fatal(err)
192 }
193 }
194 return history
195}