1package ansi
2
3import "testing"
4
5func markBenchmarkFrame(f *Frame) {
6 for y := range f.Height {
7 for x := range f.Width {
8 f.Set(x, y, Cell{Rune: rune('A' + (y*f.Width+x)%26), Style: DefaultStyle()})
9 }
10 }
11}
12
13func setBenchmarkShadow(r *Renderer, frame Frame) {
14 committed := r.committedFrame()
15 replaceFrame(&committed, frame)
16 r.setCommittedFrame(committed)
17}
18
19func BenchmarkRendererFullFrameDraw(b *testing.B) {
20 frame := NewFrame(120, 40)
21 markBenchmarkFrame(&frame)
22 r := New(Capabilities{})
23 damage := []Damage{FullRedraw()}
24 b.ReportAllocs()
25 var outBytes int64
26 for b.Loop() {
27 r.Reset()
28 out, err := r.Draw(frame, damage)
29 if err != nil {
30 b.Fatal(err)
31 }
32 if len(out) == 0 {
33 b.Fatal("expected renderer output")
34 }
35 outBytes += int64(len(out))
36 }
37 b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op")
38}
39
40func BenchmarkRendererFragmentedDamage(b *testing.B) {
41 const (
42 width = 120
43 height = 40
44 )
45
46 baseline := NewFrame(width, height)
47 markBenchmarkFrame(&baseline)
48 frame := NewFrame(width, height)
49 markBenchmarkFrame(&frame)
50
51 // Create 256 overlapping or vertically adjacent text rectangles in reverse
52 // row/column order. Canonical damage planning must sort and merge these into
53 // stable per-row spans before emission.
54 damage := make([]Damage, 0, 256)
55 for row := 15; row >= 0; row-- {
56 for col := 15; col >= 0; col-- {
57 x, y := col*7, row*2
58 damage = append(damage, Damage{Kind: DamageText, X: x, Y: y, Width: 8, Height: 2})
59 for dy := range 2 {
60 for dx := range 8 {
61 frame.Set(x+dx, y+dy, Cell{Rune: 'z', Style: DefaultStyle()})
62 }
63 }
64 }
65 }
66
67 r := New(Capabilities{})
68 b.ReportAllocs()
69 var outBytes int64
70 for b.Loop() {
71 // Each iteration starts with the identical pre-update terminal shadow.
72 setBenchmarkShadow(r, baseline)
73 out, err := r.Draw(frame, damage)
74 if err != nil {
75 b.Fatal(err)
76 }
77 if len(out) == 0 {
78 b.Fatal("expected renderer output")
79 }
80 outBytes += int64(len(out))
81 }
82 b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op")
83}
84
85func BenchmarkRendererIncrementalOneCell(b *testing.B) {
86 for _, tt := range []struct {
87 name string
88 prepareCommit bool
89 }{
90 {name: "PrepareCommit", prepareCommit: true},
91 {name: "Draw"},
92 } {
93 b.Run(tt.name, func(b *testing.B) {
94 frame := NewFrame(120, 40)
95 markBenchmarkFrame(&frame)
96 r := New(Capabilities{})
97 if tt.prepareCommit {
98 initial, err := r.Prepare(frame, []Damage{FullRedraw()}, false)
99 if err != nil {
100 b.Fatal(err)
101 }
102 initial.Commit()
103 } else if _, err := r.Draw(frame, []Damage{FullRedraw()}); err != nil {
104 b.Fatal(err)
105 }
106 damage := []Damage{{Kind: DamageText, X: 60, Y: 20, Width: 1, Height: 1}}
107
108 b.ReportAllocs()
109 var outBytes int64
110 changed := false
111 for b.Loop() {
112 changed = !changed
113 cell := Cell{Rune: 'X', Style: DefaultStyle()}
114 if changed {
115 cell.Rune = 'Y'
116 }
117 frame.Set(60, 20, cell)
118 if tt.prepareCommit {
119 prepared, err := r.Prepare(frame, damage, false)
120 if err != nil {
121 b.Fatal(err)
122 }
123 if len(prepared.Bytes()) == 0 {
124 b.Fatal("expected renderer output")
125 }
126 outBytes += int64(len(prepared.Bytes()))
127 prepared.Commit()
128 continue
129 }
130 out, err := r.Draw(frame, damage)
131 if err != nil {
132 b.Fatal(err)
133 }
134 if len(out) == 0 {
135 b.Fatal("expected renderer output")
136 }
137 outBytes += int64(len(out))
138 }
139 b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op")
140 })
141 }
142}
143
144func BenchmarkRendererIncrementalNoBytePrepareCommit(b *testing.B) {
145 frame := NewFrame(120, 40)
146 markBenchmarkFrame(&frame)
147 r := New(Capabilities{})
148 initial, err := r.Prepare(frame, []Damage{FullRedraw()}, false)
149 if err != nil {
150 b.Fatal(err)
151 }
152 initial.Commit()
153
154 b.ReportAllocs()
155 for b.Loop() {
156 prepared, err := r.Prepare(frame, nil, false)
157 if err != nil {
158 b.Fatal(err)
159 }
160 if len(prepared.Bytes()) != 0 {
161 b.Fatal("expected no output")
162 }
163 prepared.Commit()
164 }
165}
166
167func BenchmarkRendererBroadRegularDamage(b *testing.B) {
168 const (
169 width = 120
170 height = 40
171 )
172 baseline := NewFrame(width, height)
173 markBenchmarkFrame(&baseline)
174 frame := baseline.Clone()
175 for y := range height {
176 frame.Set(0, y, Cell{Rune: 'z', Style: DefaultStyle()})
177 }
178 damage := make([]Damage, height)
179 for y := range height {
180 damage[y] = Damage{Kind: DamageText, X: 0, Y: y, Width: width, Height: 1}
181 }
182
183 r := New(Capabilities{})
184 b.ReportAllocs()
185 var outBytes int64
186 for b.Loop() {
187 setBenchmarkShadow(r, baseline)
188 out, err := r.Draw(frame, damage)
189 if err != nil {
190 b.Fatal(err)
191 }
192 if len(out) == 0 {
193 b.Fatal("expected renderer output")
194 }
195 outBytes += int64(len(out))
196 }
197 b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op")
198}
199
200func BenchmarkRendererScrollFastPath(b *testing.B) {
201 frame := NewFrame(120, 40)
202 markBenchmarkFrame(&frame)
203 r := New(Capabilities{})
204 if _, err := r.Draw(frame, []Damage{FullRedraw()}); err != nil {
205 b.Fatal(err)
206 }
207
208 scrolled := NewFrame(120, 40)
209 copy(scrolled.Cells[0:39*120], frame.Cells[120:40*120])
210 for i := 39 * 120; i < 40*120; i++ {
211 scrolled.Cells[i] = BlankCell()
212 }
213 for x, r := range []rune("new output line") {
214 scrolled.Set(x, 39, Cell{Rune: r, Style: DefaultStyle()})
215 }
216 damage := []Damage{
217 {Kind: DamageScrollUp, X: 0, Y: 0, Width: 120, Height: 40, Count: 1},
218 {Kind: DamageText, X: 0, Y: 39, Width: 120, Height: 1, Count: 1},
219 }
220
221 b.ReportAllocs()
222 var outBytes int64
223 for b.Loop() {
224 setBenchmarkShadow(r, frame)
225 out, err := r.Draw(scrolled, damage)
226 if err != nil {
227 b.Fatal(err)
228 }
229 if len(out) == 0 {
230 b.Fatal("expected renderer output")
231 }
232 outBytes += int64(len(out))
233 }
234 b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op")
235}