test(renderer): benchmark fragmented damage planning
- commit
- 616526c7729897d6cd4e07aed08780bd354f5c2c
- author
- brice · · 2026-07-15 07:18 UTC
- parent
- ce6cefd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +45 -0
M pkg/renderer/benchmark_test.go +45 -0
| @@ -31,6 +31,51 @@ func BenchmarkRendererFullFrameDraw(b *testing.B) { | |||
| 31 | 31 | b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op") | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | + | func BenchmarkRendererFragmentedDamage(b *testing.B) { | |
| 35 | + | const ( | |
| 36 | + | width = 120 | |
| 37 | + | height = 40 | |
| 38 | + | ) | |
| 39 | + | ||
| 40 | + | baseline := NewFrame(width, height) | |
| 41 | + | markBenchmarkFrame(&baseline) | |
| 42 | + | frame := NewFrame(width, height) | |
| 43 | + | markBenchmarkFrame(&frame) | |
| 44 | + | ||
| 45 | + | // Create 256 overlapping or vertically adjacent text rectangles in reverse | |
| 46 | + | // row/column order. Canonical damage planning must sort and merge these into | |
| 47 | + | // stable per-row spans before emission. | |
| 48 | + | damage := make([]Damage, 0, 256) | |
| 49 | + | for row := 15; row >= 0; row-- { | |
| 50 | + | for col := 15; col >= 0; col-- { | |
| 51 | + | x, y := col*7, row*2 | |
| 52 | + | damage = append(damage, Damage{Kind: DamageText, X: x, Y: y, Width: 8, Height: 2}) | |
| 53 | + | for dy := range 2 { | |
| 54 | + | for dx := range 8 { | |
| 55 | + | frame.Set(x+dx, y+dy, Cell{Rune: 'z', Style: DefaultStyle()}) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | r := New(Capabilities{}) | |
| 62 | + | b.ReportAllocs() | |
| 63 | + | var outBytes int64 | |
| 64 | + | for b.Loop() { | |
| 65 | + | // Each iteration starts with the identical pre-update terminal shadow. | |
| 66 | + | r.replaceShadow(baseline) | |
| 67 | + | out, err := r.Draw(frame, damage) | |
| 68 | + | if err != nil { | |
| 69 | + | b.Fatal(err) | |
| 70 | + | } | |
| 71 | + | if len(out) == 0 { | |
| 72 | + | b.Fatal("expected renderer output") | |
| 73 | + | } | |
| 74 | + | outBytes += int64(len(out)) | |
| 75 | + | } | |
| 76 | + | b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op") | |
| 77 | + | } | |
| 78 | + | ||
| 34 | 79 | func BenchmarkRendererScrollFastPath(b *testing.B) { | |
| 35 | 80 | frame := NewFrame(120, 40) | |
| 36 | 81 | markBenchmarkFrame(&frame) | |
| @@ -31,6 +31,51 @@ func BenchmarkRendererFullFrameDraw(b *testing.B) { | |||||
| 31 | b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op") | 31 | b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op") | ||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | + | func BenchmarkRendererFragmentedDamage(b *testing.B) { | |||
| 35 | + | const ( | |||
| 36 | + | width = 120 | |||
| 37 | + | height = 40 | |||
| 38 | + | ) | |||
| 39 | + | ||||
| 40 | + | baseline := NewFrame(width, height) | |||
| 41 | + | markBenchmarkFrame(&baseline) | |||
| 42 | + | frame := NewFrame(width, height) | |||
| 43 | + | markBenchmarkFrame(&frame) | |||
| 44 | + | ||||
| 45 | + | // Create 256 overlapping or vertically adjacent text rectangles in reverse | |||
| 46 | + | // row/column order. Canonical damage planning must sort and merge these into | |||
| 47 | + | // stable per-row spans before emission. | |||
| 48 | + | damage := make([]Damage, 0, 256) | |||
| 49 | + | for row := 15; row >= 0; row-- { | |||
| 50 | + | for col := 15; col >= 0; col-- { | |||
| 51 | + | x, y := col*7, row*2 | |||
| 52 | + | damage = append(damage, Damage{Kind: DamageText, X: x, Y: y, Width: 8, Height: 2}) | |||
| 53 | + | for dy := range 2 { | |||
| 54 | + | for dx := range 8 { | |||
| 55 | + | frame.Set(x+dx, y+dy, Cell{Rune: 'z', Style: DefaultStyle()}) | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | r := New(Capabilities{}) | |||
| 62 | + | b.ReportAllocs() | |||
| 63 | + | var outBytes int64 | |||
| 64 | + | for b.Loop() { | |||
| 65 | + | // Each iteration starts with the identical pre-update terminal shadow. | |||
| 66 | + | r.replaceShadow(baseline) | |||
| 67 | + | out, err := r.Draw(frame, damage) | |||
| 68 | + | if err != nil { | |||
| 69 | + | b.Fatal(err) | |||
| 70 | + | } | |||
| 71 | + | if len(out) == 0 { | |||
| 72 | + | b.Fatal("expected renderer output") | |||
| 73 | + | } | |||
| 74 | + | outBytes += int64(len(out)) | |||
| 75 | + | } | |||
| 76 | + | b.ReportMetric(float64(outBytes)/float64(b.N), "outbytes/op") | |||
| 77 | + | } | |||
| 78 | + | ||||
| 34 | func BenchmarkRendererScrollFastPath(b *testing.B) { | 79 | func BenchmarkRendererScrollFastPath(b *testing.B) { | ||
| 35 | frame := NewFrame(120, 40) | 80 | frame := NewFrame(120, 40) | ||
| 36 | markBenchmarkFrame(&frame) | 81 | markBenchmarkFrame(&frame) | ||