detached 6ab8757

test(core): measure publish allocations on the flip path and the opening overview

The frame budget drops to 32 (timer no longer allocated) and is measured
on both step paths (fallback timer and a page flip while another screen
animates), with fade, dim and scale motions running. The overview test
toggles through snapshot/transition so its cards' motions are in flight
(70). The leaving budget is its measured value, 24.
commit
6ab8757f652f3c669ceae7d082b5c4a5f9e3ca6d
author
brice · · 2026-10-05 01:53 UTC
parent
7d23734
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +81 -49
M internal/core/publish_alloc_test.go +81 -49
@@ -9,16 +9,20 @@ import (
99 )
1010
1111 // Allocation budgets of one animation frame (step) on two screens of six
12-// windows each, with camera and rect motions running, and on one screen with
13-// the overview open. Every scene sent is freshly built (immutable once sent),
14-// and the layouts are rebuilt for every publish, so the floor is not 0.
15-// Measured: 43 before presizing the scene windows and separators, 36 after;
16-// 70 with the overview open (its rows and card layouts are rebuilt too).
17-// Leaving entries (a closed window still fading) add none: the settled
18-// layout with them goes into a reused buffer.
12+// windows each, with camera and rect motions (position, fade, dim, scale)
13+// running, and on one screen with the overview opening (its cards' scale
14+// and fade motions running). Every scene sent is freshly built (immutable
15+// once sent), and the layouts are rebuilt for every publish, so the floor
16+// is not 0. Measured: 43 before presizing the scene windows and separators,
17+// 36 after, 32 once the fallback timer is made once and reset per frame
18+// (the fallback path, step(ctx, nil), re-arms it; the flip path with
19+// another screen animating keeps it and costs the same). Leaving entries
20+// (a closed window still fading) add none: 24, less than a frame with six
21+// moving windows (fewer configure targets). The opening overview costs 70
22+// (its rows and card layouts are rebuilt too).
1923 const (
20- publishAllocBudget = 36
21- publishLeavingAllocBudget = publishAllocBudget
24+ publishAllocBudget = 32
25+ publishLeavingAllocBudget = 24
2226 publishOverviewAllocBudget = 70
2327 )
2428
@@ -56,11 +60,12 @@ func publishRig(t *testing.T, screens int) *Core {
5660 return c
5761 }
5862
59-// publishStep is one animation frame whose scene is taken from the channel.
60-func publishStep(t *testing.T, c *Core) func() {
63+// publishStep is one animation frame of only (nil: the fallback timer's,
64+// every screen) whose scene is taken from the channel.
65+func publishStep(t *testing.T, c *Core, only *screen) func() {
6166 ctx := context.Background()
6267 return func() {
63- if err := c.step(ctx, nil); err != nil {
68+ if err := c.step(ctx, only); err != nil {
6469 t.Fatal(err)
6570 }
6671 select {
@@ -71,66 +76,92 @@ func publishStep(t *testing.T, c *Core) func() {
7176 }
7277 }
7378
74-// TestPublishAllocations pins the cost of publishing an animation frame.
75-func TestPublishAllocations(t *testing.T) {
76- c := publishRig(t, 2)
77- t0 := time.Now()
78- for _, sc := range c.screens {
79- if sc.name() == "" {
80- continue
81- }
82- ws := sc.mon.Current()
83- ws.motion = c.spring(viewSpring(100, 0), t0)
84- ws.shift = 100
85- sc.rects = make(map[WindowID]rectMotion, 6)
86- sp := viewSpring(100, 0)
87- for _, p := range sc.settledLayout {
88- sc.rects[p.ID] = rectMotion{
89- x: c.spring(sp, t0), y: c.spring(sp, t0),
90- w: c.spring(sp, t0), h: c.spring(sp, t0),
91- dx: 100, dy: 100, dw: 100, dh: 100,
92- }
79+// startRectMotions gives every window of sc a rect motion on each
80+// component: position and size, fade, dim and content scale.
81+func startRectMotions(c *Core, sc *screen, t0 time.Time) {
82+ sc.rects = make(map[WindowID]rectMotion, 6)
83+ sp := viewSpring(100, 0)
84+ for _, p := range sc.settledLayout {
85+ sc.rects[p.ID] = rectMotion{
86+ x: c.spring(sp, t0), y: c.spring(sp, t0),
87+ w: c.spring(sp, t0), h: c.spring(sp, t0),
88+ dx: 100, dy: 100, dw: 100, dh: 100,
89+ fade: c.spring(viewSpring(0.5, 0), t0), df: 0.5,
90+ dim: c.spring(viewSpring(0.5, 0), t0), ddim: 0.5,
91+ scale: true,
9392 }
94- sc.rectsWS = ws
9593 }
96- step := publishStep(t, c)
97- step()
94+ sc.rectsWS = sc.mon.Current()
95+}
96+
97+// checkRunning fails unless every real screen still has six rect motions
98+// and its camera motion.
99+func checkRunning(t *testing.T, c *Core, when string) {
100+ t.Helper()
98101 for _, sc := range c.screens {
99102 if sc.name() == "" {
100103 continue
101104 }
102105 if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 {
103- t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown))
106+ t.Fatalf("%s: rects %d camera %v shown %d", when, len(sc.rects), sc.mon.Current().motion.on, len(sc.shown))
104107 }
105108 }
106- n := testing.AllocsPerRun(50, step)
107- t.Logf("allocs per step = %v", n)
109+}
110+
111+// TestPublishAllocations pins the cost of publishing an animation frame, on
112+// the fallback timer (every screen) and on a page flip of one screen while
113+// the other still animates (the flip path keeps the timer).
114+func TestPublishAllocations(t *testing.T) {
115+ c := publishRig(t, 2)
116+ t0 := time.Now()
108117 for _, sc := range c.screens {
109118 if sc.name() == "" {
110119 continue
111120 }
112- if len(sc.rects) != 6 || !sc.mon.Current().motion.on {
113- t.Fatalf("motions settled during the measure: rects %d", len(sc.rects))
114- }
121+ ws := sc.mon.Current()
122+ ws.motion = c.spring(viewSpring(100, 0), t0)
123+ ws.shift = 100
124+ startRectMotions(c, sc, t0)
115125 }
116- if n > publishAllocBudget {
117- t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget)
126+ for _, tc := range []struct {
127+ name string
128+ only *screen
129+ }{{"fallback", nil}, {"flip", c.screens[1]}} {
130+ step := publishStep(t, c, tc.only)
131+ step()
132+ checkRunning(t, c, "setup")
133+ if tc.only != nil && !c.animatingOther(tc.only) {
134+ t.Fatal("setup: the other screen does not animate")
135+ }
136+ n := testing.AllocsPerRun(50, step)
137+ t.Logf("%s allocs per step = %v", tc.name, n)
138+ checkRunning(t, c, "motions settled during the measure")
139+ if n > publishAllocBudget {
140+ t.Errorf("%s publish allocs per frame = %v, budget %d", tc.name, n, publishAllocBudget)
141+ }
118142 }
119143 }
120144
121-// TestPublishOverviewAllocations pins the cost of publishing with the
122-// overview open, which guards the reuse of Core.overviewReal.
145+// TestPublishOverviewAllocations pins the cost of publishing while the
146+// overview opens: its cards fly from the full rects (scale and fade motions
147+// on every card), which guards the reuse of Core.overviewReal.
123148 func TestPublishOverviewAllocations(t *testing.T) {
124149 c := publishRig(t, 1)
125150 sc := c.cur()
151+ now := time.Now()
152+ shots := c.snapshot(now)
126153 sc.mon.ToggleOverview()
127- step := publishStep(t, c)
154+ c.transition(shots, now)
155+ step := publishStep(t, c, nil)
128156 step()
129- if !sc.mon.ov.open || len(c.overviewReal) != 6 {
130- t.Fatalf("setup: overview %v, real layouts %d", sc.mon.ov.open, len(c.overviewReal))
157+ if !sc.mon.ov.open || len(c.overviewReal) != 6 || len(sc.rects) == 0 || !c.animating() {
158+ t.Fatalf("setup: overview %v, real layouts %d, rects %d, animating %v", sc.mon.ov.open, len(c.overviewReal), len(sc.rects), c.animating())
131159 }
132160 n := testing.AllocsPerRun(50, step)
133161 t.Logf("overview allocs per step = %v", n)
162+ if len(sc.rects) == 0 {
163+ t.Fatal("card motions settled during the measure")
164+ }
134165 if n > publishOverviewAllocBudget {
135166 t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
136167 }
@@ -138,7 +169,8 @@ func TestPublishOverviewAllocations(t *testing.T) {
138169
139170 // TestPublishLeavingAllocations pins the cost of a frame with leaving
140171 // entries on every screen (two of the six windows closed, fading out, the
141-// other four with rect motions): not above a frame without them.
172+// other four with rect motions): the measured 24, under a frame without
173+// them (the leaving entries take no configure target).
142174 func TestPublishLeavingAllocations(t *testing.T) {
143175 c := publishRig(t, 2)
144176 t0 := time.Now()
@@ -164,7 +196,7 @@ func TestPublishLeavingAllocations(t *testing.T) {
164196 }
165197 sc.rectsWS = sc.mon.Current()
166198 }
167- step := publishStep(t, c)
199+ step := publishStep(t, c, nil)
168200 step()
169201 for _, sc := range c.screens {
170202 if sc.name() == "" {
@@ -9,16 +9,20 @@ import (
9 )9 )
10 10
11 // Allocation budgets of one animation frame (step) on two screens of six11 // Allocation budgets of one animation frame (step) on two screens of six
12-// windows each, with camera and rect motions running, and on one screen with12+// windows each, with camera and rect motions (position, fade, dim, scale)
13-// the overview open. Every scene sent is freshly built (immutable once sent),13+// running, and on one screen with the overview opening (its cards' scale
14-// and the layouts are rebuilt for every publish, so the floor is not 0.14+// and fade motions running). Every scene sent is freshly built (immutable
15-// Measured: 43 before presizing the scene windows and separators, 36 after;15+// once sent), and the layouts are rebuilt for every publish, so the floor
16-// 70 with the overview open (its rows and card layouts are rebuilt too).16+// is not 0. Measured: 43 before presizing the scene windows and separators,
17-// Leaving entries (a closed window still fading) add none: the settled17+// 36 after, 32 once the fallback timer is made once and reset per frame
18-// layout with them goes into a reused buffer.18+// (the fallback path, step(ctx, nil), re-arms it; the flip path with
19+// another screen animating keeps it and costs the same). Leaving entries
20+// (a closed window still fading) add none: 24, less than a frame with six
21+// moving windows (fewer configure targets). The opening overview costs 70
22+// (its rows and card layouts are rebuilt too).
19 const (23 const (
20- publishAllocBudget = 3624+ publishAllocBudget = 32
21- publishLeavingAllocBudget = publishAllocBudget25+ publishLeavingAllocBudget = 24
22 publishOverviewAllocBudget = 7026 publishOverviewAllocBudget = 70
23 )27 )
24 28
@@ -56,11 +60,12 @@ func publishRig(t *testing.T, screens int) *Core {
56 return c60 return c
57 }61 }
58 62
59-// publishStep is one animation frame whose scene is taken from the channel.63+// publishStep is one animation frame of only (nil: the fallback timer's,
60-func publishStep(t *testing.T, c *Core) func() {64+// every screen) whose scene is taken from the channel.
65+func publishStep(t *testing.T, c *Core, only *screen) func() {
61 ctx := context.Background()66 ctx := context.Background()
62 return func() {67 return func() {
63- if err := c.step(ctx, nil); err != nil {68+ if err := c.step(ctx, only); err != nil {
64 t.Fatal(err)69 t.Fatal(err)
65 }70 }
66 select {71 select {
@@ -71,66 +76,92 @@ func publishStep(t *testing.T, c *Core) func() {
71 }76 }
72 }77 }
73 78
74-// TestPublishAllocations pins the cost of publishing an animation frame.79+// startRectMotions gives every window of sc a rect motion on each
75-func TestPublishAllocations(t *testing.T) {80+// component: position and size, fade, dim and content scale.
76- c := publishRig(t, 2)81+func startRectMotions(c *Core, sc *screen, t0 time.Time) {
77- t0 := time.Now()82+ sc.rects = make(map[WindowID]rectMotion, 6)
78- for _, sc := range c.screens {83+ sp := viewSpring(100, 0)
79- if sc.name() == "" {84+ for _, p := range sc.settledLayout {
80- continue85+ sc.rects[p.ID] = rectMotion{
81- }86+ x: c.spring(sp, t0), y: c.spring(sp, t0),
82- ws := sc.mon.Current()87+ w: c.spring(sp, t0), h: c.spring(sp, t0),
83- ws.motion = c.spring(viewSpring(100, 0), t0)88+ dx: 100, dy: 100, dw: 100, dh: 100,
84- ws.shift = 10089+ fade: c.spring(viewSpring(0.5, 0), t0), df: 0.5,
85- sc.rects = make(map[WindowID]rectMotion, 6)90+ dim: c.spring(viewSpring(0.5, 0), t0), ddim: 0.5,
86- sp := viewSpring(100, 0)91+ scale: true,
87- for _, p := range sc.settledLayout {
88- sc.rects[p.ID] = rectMotion{
89- x: c.spring(sp, t0), y: c.spring(sp, t0),
90- w: c.spring(sp, t0), h: c.spring(sp, t0),
91- dx: 100, dy: 100, dw: 100, dh: 100,
92- }
93 }92 }
94- sc.rectsWS = ws
95 }93 }
96- step := publishStep(t, c)94+ sc.rectsWS = sc.mon.Current()
97- step()95+}
96+
97+// checkRunning fails unless every real screen still has six rect motions
98+// and its camera motion.
99+func checkRunning(t *testing.T, c *Core, when string) {
100+ t.Helper()
98 for _, sc := range c.screens {101 for _, sc := range c.screens {
99 if sc.name() == "" {102 if sc.name() == "" {
100 continue103 continue
101 }104 }
102 if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 {105 if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 {
103- t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown))106+ t.Fatalf("%s: rects %d camera %v shown %d", when, len(sc.rects), sc.mon.Current().motion.on, len(sc.shown))
104 }107 }
105 }108 }
106- n := testing.AllocsPerRun(50, step)109+}
107- t.Logf("allocs per step = %v", n)110+
111+// TestPublishAllocations pins the cost of publishing an animation frame, on
112+// the fallback timer (every screen) and on a page flip of one screen while
113+// the other still animates (the flip path keeps the timer).
114+func TestPublishAllocations(t *testing.T) {
115+ c := publishRig(t, 2)
116+ t0 := time.Now()
108 for _, sc := range c.screens {117 for _, sc := range c.screens {
109 if sc.name() == "" {118 if sc.name() == "" {
110 continue119 continue
111 }120 }
112- if len(sc.rects) != 6 || !sc.mon.Current().motion.on {121+ ws := sc.mon.Current()
113- t.Fatalf("motions settled during the measure: rects %d", len(sc.rects))122+ ws.motion = c.spring(viewSpring(100, 0), t0)
114- }123+ ws.shift = 100
124+ startRectMotions(c, sc, t0)
115 }125 }
116- if n > publishAllocBudget {126+ for _, tc := range []struct {
117- t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget)127+ name string
128+ only *screen
129+ }{{"fallback", nil}, {"flip", c.screens[1]}} {
130+ step := publishStep(t, c, tc.only)
131+ step()
132+ checkRunning(t, c, "setup")
133+ if tc.only != nil && !c.animatingOther(tc.only) {
134+ t.Fatal("setup: the other screen does not animate")
135+ }
136+ n := testing.AllocsPerRun(50, step)
137+ t.Logf("%s allocs per step = %v", tc.name, n)
138+ checkRunning(t, c, "motions settled during the measure")
139+ if n > publishAllocBudget {
140+ t.Errorf("%s publish allocs per frame = %v, budget %d", tc.name, n, publishAllocBudget)
141+ }
118 }142 }
119 }143 }
120 144
121-// TestPublishOverviewAllocations pins the cost of publishing with the145+// TestPublishOverviewAllocations pins the cost of publishing while the
122-// overview open, which guards the reuse of Core.overviewReal.146+// overview opens: its cards fly from the full rects (scale and fade motions
147+// on every card), which guards the reuse of Core.overviewReal.
123 func TestPublishOverviewAllocations(t *testing.T) {148 func TestPublishOverviewAllocations(t *testing.T) {
124 c := publishRig(t, 1)149 c := publishRig(t, 1)
125 sc := c.cur()150 sc := c.cur()
151+ now := time.Now()
152+ shots := c.snapshot(now)
126 sc.mon.ToggleOverview()153 sc.mon.ToggleOverview()
127- step := publishStep(t, c)154+ c.transition(shots, now)
155+ step := publishStep(t, c, nil)
128 step()156 step()
129- if !sc.mon.ov.open || len(c.overviewReal) != 6 {157+ if !sc.mon.ov.open || len(c.overviewReal) != 6 || len(sc.rects) == 0 || !c.animating() {
130- t.Fatalf("setup: overview %v, real layouts %d", sc.mon.ov.open, len(c.overviewReal))158+ t.Fatalf("setup: overview %v, real layouts %d, rects %d, animating %v", sc.mon.ov.open, len(c.overviewReal), len(sc.rects), c.animating())
131 }159 }
132 n := testing.AllocsPerRun(50, step)160 n := testing.AllocsPerRun(50, step)
133 t.Logf("overview allocs per step = %v", n)161 t.Logf("overview allocs per step = %v", n)
162+ if len(sc.rects) == 0 {
163+ t.Fatal("card motions settled during the measure")
164+ }
134 if n > publishOverviewAllocBudget {165 if n > publishOverviewAllocBudget {
135 t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)166 t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
136 }167 }
@@ -138,7 +169,8 @@ func TestPublishOverviewAllocations(t *testing.T) {
138 169
139 // TestPublishLeavingAllocations pins the cost of a frame with leaving170 // TestPublishLeavingAllocations pins the cost of a frame with leaving
140 // entries on every screen (two of the six windows closed, fading out, the171 // entries on every screen (two of the six windows closed, fading out, the
141-// other four with rect motions): not above a frame without them.172+// other four with rect motions): the measured 24, under a frame without
173+// them (the leaving entries take no configure target).
142 func TestPublishLeavingAllocations(t *testing.T) {174 func TestPublishLeavingAllocations(t *testing.T) {
143 c := publishRig(t, 2)175 c := publishRig(t, 2)
144 t0 := time.Now()176 t0 := time.Now()
@@ -164,7 +196,7 @@ func TestPublishLeavingAllocations(t *testing.T) {
164 }196 }
165 sc.rectsWS = sc.mon.Current()197 sc.rectsWS = sc.mon.Current()
166 }198 }
167- step := publishStep(t, c)199+ step := publishStep(t, c, nil)
168 step()200 step()
169 for _, sc := range c.screens {201 for _, sc := range c.screens {
170 if sc.name() == "" {202 if sc.name() == "" {

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 23

Tags 10

↑↓ moveenter openesc close