detached 48dc9c8

test(theme): add accent rendering benchmarks

commit
48dc9c82392e1654c4e325fda583dc5df582f737
author
brice · · 2026-07-18 06:01 UTC
parent
15085f3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 (
1010 "github.com/stretchr/testify/require"
1111
1212 "github.com/bnema/vev/internal/domain"
13+ "github.com/bnema/vev/internal/usecase/layout"
1314 themeui "github.com/bnema/vev/internal/usecase/theme"
1415 "github.com/bnema/vev/pkg/renderer"
1516 )
@@ -356,6 +357,91 @@ func TestComposeCapturedFloatingFrameCachedAllocationsAreOnlyFrameClone(t *testi
356357 require.LessOrEqual(t, allocs, float64(2))
357358 }
358359
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+
359445 func BenchmarkComposeCapturedFloatingFrameCached(b *testing.B) {
360446 base := renderer.NewFrame(80, 24)
361447 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close