test(theme): add accent rendering benchmarks
- commit
- 48dc9c82392e1654c4e325fda583dc5df582f737
- author
- brice · · 2026-07-18 06:01 UTC
- parent
- 15085f3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +134 -0
M internal/usecase/daemon/floating_render_test.go +86 -0
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "github.com/stretchr/testify/require" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | + | "github.com/bnema/vev/internal/usecase/layout" | |
| 13 | 14 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 14 | 15 | "github.com/bnema/vev/pkg/renderer" | |
| 15 | 16 | ) | |
| @@ -356,6 +357,91 @@ func TestComposeCapturedFloatingFrameCachedAllocationsAreOnlyFrameClone(t *testi | |||
| 356 | 357 | require.LessOrEqual(t, allocs, float64(2)) | |
| 357 | 358 | } | |
| 358 | 359 | ||
| 360 | + | var ( | |
| 361 | + | benchmarkComposeSink composedRenderFrame | |
| 362 | + | benchmarkOutputSink []byte | |
| 363 | + | ) | |
| 364 | + | ||
| 365 | + | // benchmarkCapturedRenderState is a stable, post-capture production input: one | |
| 366 | + | // 80x24 pane, both chrome bars, and no pending pane damage. The primed cache | |
| 367 | + | // below makes each benchmark operation exercise the cached compose path. | |
| 368 | + | func benchmarkCapturedRenderState() capturedRenderState { | |
| 369 | + | pane := renderer.NewFrame(80, 24) | |
| 370 | + | for y := range pane.Height { | |
| 371 | + | for x := range pane.Width { | |
| 372 | + | pane.Set(x, y, renderer.Cell{Rune: rune('a' + (x+y)%26)}) | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | terminalTheme := themeui.Theme{ | |
| 376 | + | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, | |
| 377 | + | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |
| 378 | + | Palette: [16]renderer.RGB{ | |
| 379 | + | 2: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 380 | + | 4: {R: 0x6c, G: 0x9b, B: 0xd9}, | |
| 381 | + | 10: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 382 | + | 12: {R: 0x6c, G: 0x9b, B: 0xd9}, | |
| 383 | + | 14: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 384 | + | }, | |
| 385 | + | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |
| 386 | + | HasFG: true, | |
| 387 | + | HasBG: true, | |
| 388 | + | TrueColor: true, | |
| 389 | + | Known: true, | |
| 390 | + | SchemeKnown: true, | |
| 391 | + | UsePalette: true, | |
| 392 | + | } | |
| 393 | + | placement := layout.Placement{ID: "benchmark-pane", Content: domain.Rect{Width: 80, Height: 24}} | |
| 394 | + | return capturedRenderState{ | |
| 395 | + | layout: capturedTabLayout{ | |
| 396 | + | area: domain.Rect{Width: 80, Height: 24}, | |
| 397 | + | focus: placement.ID, | |
| 398 | + | placements: []layout.Placement{placement}, | |
| 399 | + | fingerprint: "benchmark-one-pane", | |
| 400 | + | valid: true, | |
| 401 | + | }, | |
| 402 | + | panes: []capturedPaneRenderState{{id: placement.ID, frame: pane, placement: placement, focused: true}}, | |
| 403 | + | bars: barState{ | |
| 404 | + | status: statusSnapshot{session: "main", tabs: []statusTab{{name: "main", paneTitle: "shell", active: true}}}, | |
| 405 | + | topRight: "vev", | |
| 406 | + | bottomRight: "main", | |
| 407 | + | mru: []recentSession{{name: "work"}, {name: "logs"}}, | |
| 408 | + | theme: terminalTheme, | |
| 409 | + | }, | |
| 410 | + | theme: terminalTheme, | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | func benchmarkComposeCacheInput(state capturedRenderState) composeCacheInput { | |
| 415 | + | priming := state | |
| 416 | + | priming.reset = true | |
| 417 | + | priming.panes = append([]capturedPaneRenderState(nil), state.panes...) | |
| 418 | + | priming.panes[0].damage = []renderer.Damage{renderer.FullRedraw()} | |
| 419 | + | return composeFrame(priming, composeCacheInput{}).cache | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | func BenchmarkComposeCapturedFrame(b *testing.B) { | |
| 423 | + | state := benchmarkCapturedRenderState() | |
| 424 | + | cache := benchmarkComposeCacheInput(state) | |
| 425 | + | scratch := composeCacheInput{} | |
| 426 | + | draw := renderer.New(renderer.Capabilities{}) | |
| 427 | + | var totalBytes int64 | |
| 428 | + | ||
| 429 | + | b.ReportAllocs() | |
| 430 | + | b.ResetTimer() | |
| 431 | + | for b.Loop() { | |
| 432 | + | composed := composeFrame(state, cache, scratch) | |
| 433 | + | scratch = composed.cache | |
| 434 | + | draw.Reset() | |
| 435 | + | out, err := draw.Draw(composed.frame, composed.damage) | |
| 436 | + | if err != nil { | |
| 437 | + | b.Fatal(err) | |
| 438 | + | } | |
| 439 | + | totalBytes += int64(len(out)) | |
| 440 | + | benchmarkComposeSink, benchmarkOutputSink = composed, out | |
| 441 | + | } | |
| 442 | + | b.ReportMetric(float64(totalBytes)/float64(b.N), "output-bytes/op") | |
| 443 | + | } | |
| 444 | + | ||
| 359 | 445 | func BenchmarkComposeCapturedFloatingFrameCached(b *testing.B) { | |
| 360 | 446 | base := renderer.NewFrame(80, 24) | |
| 361 | 447 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 11 | 11 | ||||
| 12 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | + | "github.com/bnema/vev/internal/usecase/layout" | |||
| 13 | themeui "github.com/bnema/vev/internal/usecase/theme" | 14 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 14 | "github.com/bnema/vev/pkg/renderer" | 15 | "github.com/bnema/vev/pkg/renderer" | ||
| 15 | ) | 16 | ) | ||
| @@ -356,6 +357,91 @@ func TestComposeCapturedFloatingFrameCachedAllocationsAreOnlyFrameClone(t *testi | |||||
| 356 | require.LessOrEqual(t, allocs, float64(2)) | 357 | require.LessOrEqual(t, allocs, float64(2)) | ||
| 357 | } | 358 | } | ||
| 358 | 359 | ||||
| 360 | + | var ( | |||
| 361 | + | benchmarkComposeSink composedRenderFrame | |||
| 362 | + | benchmarkOutputSink []byte | |||
| 363 | + | ) | |||
| 364 | + | ||||
| 365 | + | // benchmarkCapturedRenderState is a stable, post-capture production input: one | |||
| 366 | + | // 80x24 pane, both chrome bars, and no pending pane damage. The primed cache | |||
| 367 | + | // below makes each benchmark operation exercise the cached compose path. | |||
| 368 | + | func benchmarkCapturedRenderState() capturedRenderState { | |||
| 369 | + | pane := renderer.NewFrame(80, 24) | |||
| 370 | + | for y := range pane.Height { | |||
| 371 | + | for x := range pane.Width { | |||
| 372 | + | pane.Set(x, y, renderer.Cell{Rune: rune('a' + (x+y)%26)}) | |||
| 373 | + | } | |||
| 374 | + | } | |||
| 375 | + | terminalTheme := themeui.Theme{ | |||
| 376 | + | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, | |||
| 377 | + | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |||
| 378 | + | Palette: [16]renderer.RGB{ | |||
| 379 | + | 2: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 380 | + | 4: {R: 0x6c, G: 0x9b, B: 0xd9}, | |||
| 381 | + | 10: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 382 | + | 12: {R: 0x6c, G: 0x9b, B: 0xd9}, | |||
| 383 | + | 14: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 384 | + | }, | |||
| 385 | + | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |||
| 386 | + | HasFG: true, | |||
| 387 | + | HasBG: true, | |||
| 388 | + | TrueColor: true, | |||
| 389 | + | Known: true, | |||
| 390 | + | SchemeKnown: true, | |||
| 391 | + | UsePalette: true, | |||
| 392 | + | } | |||
| 393 | + | placement := layout.Placement{ID: "benchmark-pane", Content: domain.Rect{Width: 80, Height: 24}} | |||
| 394 | + | return capturedRenderState{ | |||
| 395 | + | layout: capturedTabLayout{ | |||
| 396 | + | area: domain.Rect{Width: 80, Height: 24}, | |||
| 397 | + | focus: placement.ID, | |||
| 398 | + | placements: []layout.Placement{placement}, | |||
| 399 | + | fingerprint: "benchmark-one-pane", | |||
| 400 | + | valid: true, | |||
| 401 | + | }, | |||
| 402 | + | panes: []capturedPaneRenderState{{id: placement.ID, frame: pane, placement: placement, focused: true}}, | |||
| 403 | + | bars: barState{ | |||
| 404 | + | status: statusSnapshot{session: "main", tabs: []statusTab{{name: "main", paneTitle: "shell", active: true}}}, | |||
| 405 | + | topRight: "vev", | |||
| 406 | + | bottomRight: "main", | |||
| 407 | + | mru: []recentSession{{name: "work"}, {name: "logs"}}, | |||
| 408 | + | theme: terminalTheme, | |||
| 409 | + | }, | |||
| 410 | + | theme: terminalTheme, | |||
| 411 | + | } | |||
| 412 | + | } | |||
| 413 | + | ||||
| 414 | + | func benchmarkComposeCacheInput(state capturedRenderState) composeCacheInput { | |||
| 415 | + | priming := state | |||
| 416 | + | priming.reset = true | |||
| 417 | + | priming.panes = append([]capturedPaneRenderState(nil), state.panes...) | |||
| 418 | + | priming.panes[0].damage = []renderer.Damage{renderer.FullRedraw()} | |||
| 419 | + | return composeFrame(priming, composeCacheInput{}).cache | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | func BenchmarkComposeCapturedFrame(b *testing.B) { | |||
| 423 | + | state := benchmarkCapturedRenderState() | |||
| 424 | + | cache := benchmarkComposeCacheInput(state) | |||
| 425 | + | scratch := composeCacheInput{} | |||
| 426 | + | draw := renderer.New(renderer.Capabilities{}) | |||
| 427 | + | var totalBytes int64 | |||
| 428 | + | ||||
| 429 | + | b.ReportAllocs() | |||
| 430 | + | b.ResetTimer() | |||
| 431 | + | for b.Loop() { | |||
| 432 | + | composed := composeFrame(state, cache, scratch) | |||
| 433 | + | scratch = composed.cache | |||
| 434 | + | draw.Reset() | |||
| 435 | + | out, err := draw.Draw(composed.frame, composed.damage) | |||
| 436 | + | if err != nil { | |||
| 437 | + | b.Fatal(err) | |||
| 438 | + | } | |||
| 439 | + | totalBytes += int64(len(out)) | |||
| 440 | + | benchmarkComposeSink, benchmarkOutputSink = composed, out | |||
| 441 | + | } | |||
| 442 | + | b.ReportMetric(float64(totalBytes)/float64(b.N), "output-bytes/op") | |||
| 443 | + | } | |||
| 444 | + | ||||
| 359 | func BenchmarkComposeCapturedFloatingFrameCached(b *testing.B) { | 445 | func BenchmarkComposeCapturedFloatingFrameCached(b *testing.B) { | ||
| 360 | base := renderer.NewFrame(80, 24) | 446 | base := renderer.NewFrame(80, 24) | ||
| 361 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | 447 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | ||
A internal/usecase/theme/benchmark_test.go +48 -0
| @@ -0,0 +1,48 @@ | |||
| 1 | + | package theme | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | var benchmarkStylesSink Styles | |
| 10 | + | ||
| 11 | + | func benchmarkPaletteTheme() Theme { | |
| 12 | + | return Theme{ | |
| 13 | + | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, | |
| 14 | + | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |
| 15 | + | Palette: [16]renderer.RGB{ | |
| 16 | + | 2: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 17 | + | 4: {R: 0x6c, G: 0x9b, B: 0xd9}, | |
| 18 | + | 10: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 19 | + | 12: {R: 0x6c, G: 0x9b, B: 0xd9}, | |
| 20 | + | 14: {R: 0x7d, G: 0xb5, B: 0xb5}, | |
| 21 | + | }, | |
| 22 | + | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |
| 23 | + | HasFG: true, | |
| 24 | + | HasBG: true, | |
| 25 | + | TrueColor: true, | |
| 26 | + | Known: true, | |
| 27 | + | SchemeKnown: true, | |
| 28 | + | UsePalette: true, | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func TestNewStylesAllocations(t *testing.T) { | |
| 33 | + | theme := benchmarkPaletteTheme() | |
| 34 | + | allocs := testing.AllocsPerRun(1000, func() { | |
| 35 | + | benchmarkStylesSink = NewStyles(theme) | |
| 36 | + | }) | |
| 37 | + | if allocs != 0 { | |
| 38 | + | t.Fatalf("NewStyles allocations/op = %v, want 0", allocs) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func BenchmarkNewStyles(b *testing.B) { | |
| 43 | + | theme := benchmarkPaletteTheme() | |
| 44 | + | b.ReportAllocs() | |
| 45 | + | for b.Loop() { | |
| 46 | + | benchmarkStylesSink = NewStyles(theme) | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package theme | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | var benchmarkStylesSink Styles | |||
| 10 | + | ||||
| 11 | + | func benchmarkPaletteTheme() Theme { | |||
| 12 | + | return Theme{ | |||
| 13 | + | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, | |||
| 14 | + | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |||
| 15 | + | Palette: [16]renderer.RGB{ | |||
| 16 | + | 2: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 17 | + | 4: {R: 0x6c, G: 0x9b, B: 0xd9}, | |||
| 18 | + | 10: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 19 | + | 12: {R: 0x6c, G: 0x9b, B: 0xd9}, | |||
| 20 | + | 14: {R: 0x7d, G: 0xb5, B: 0xb5}, | |||
| 21 | + | }, | |||
| 22 | + | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |||
| 23 | + | HasFG: true, | |||
| 24 | + | HasBG: true, | |||
| 25 | + | TrueColor: true, | |||
| 26 | + | Known: true, | |||
| 27 | + | SchemeKnown: true, | |||
| 28 | + | UsePalette: true, | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func TestNewStylesAllocations(t *testing.T) { | |||
| 33 | + | theme := benchmarkPaletteTheme() | |||
| 34 | + | allocs := testing.AllocsPerRun(1000, func() { | |||
| 35 | + | benchmarkStylesSink = NewStyles(theme) | |||
| 36 | + | }) | |||
| 37 | + | if allocs != 0 { | |||
| 38 | + | t.Fatalf("NewStyles allocations/op = %v, want 0", allocs) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func BenchmarkNewStyles(b *testing.B) { | |||
| 43 | + | theme := benchmarkPaletteTheme() | |||
| 44 | + | b.ReportAllocs() | |||
| 45 | + | for b.Loop() { | |||
| 46 | + | benchmarkStylesSink = NewStyles(theme) | |||
| 47 | + | } | |||
| 48 | + | } | |||