1package core
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// Allocation budgets of one animation frame (step) on two screens of six
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), so the floor is not 0. Measured: 43 before presizing the scene
16// windows and separators, 36 after, 32 once the fallback timer is made once
17// and reset per frame (the fallback path, step(ctx, nil), re-arms it; the
18// flip path with another screen animating keeps it and costs the same), then
19// 6 once the layouts are built in the owner's reused buffers (layoutInto:
20// the screens' layout, the workspaces' column, row and tile rects) instead of
21// fresh slices. What is left is the scenes themselves (windows, separators,
22// the scene set) and the screen placements. Leaving entries (a closed window
23// still fading) add none: 4, less than a frame with six moving windows
24// (fewer configure targets). The opening overview costs 18 (the stack
25// items, the row map and the neighbor list it rebuilds every frame; its
26// layouts reuse the buffers too).
27const (
28 publishAllocBudget = 6
29 publishLeavingAllocBudget = 4
30 publishOverviewAllocBudget = 18
31)
32
33// publishRig is a Core on the real clock (a mock clock would count its own
34// bookkeeping; the fallback timer is then the system one) with screens
35// screens of six windows each. The Slowdown cap keeps every motion running
36// for seconds, so none settles during a measure even under -race or load.
37func publishRig(t *testing.T, screens int) *Core {
38 t.Helper()
39 var cfg ports.Config
40 cfg.Keyboard.CmdKey = "super"
41 cfg.Layout.MaxColumns = 2
42 cfg.Border.Width = 2
43 cfg.Animations.On = true
44 cfg.Animations.Slowdown = 10
45 c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{})
46 if err != nil {
47 t.Fatal(err)
48 }
49 next := WindowID(1)
50 for i := range screens {
51 c.addScreen(ports.OutputInfo{Name: string(rune('A' + i)), Width: 300, Height: 200})
52 }
53 for _, sc := range c.screens {
54 if sc.name() == "" {
55 continue
56 }
57 for range 6 {
58 sc.mon.AddWindow(next)
59 next++
60 }
61 sc.arrange()
62 }
63 c.refreshShown()
64 return c
65}
66
67// publishStep is one animation frame of only (nil: the fallback timer's,
68// every screen) whose scene is taken from the channel.
69func publishStep(t *testing.T, c *Core, only *screen) func() {
70 ctx := context.Background()
71 return func() {
72 if err := c.step(ctx, only); err != nil {
73 t.Fatal(err)
74 }
75 select {
76 case <-c.ch.Scenes:
77 default:
78 t.Fatal("no scene published")
79 }
80 }
81}
82
83// startRectMotions gives every window of sc a rect motion on each
84// component: position and size, fade, dim and content scale.
85func startRectMotions(c *Core, sc *screen, t0 time.Time) {
86 sc.rects = make(map[WindowID]rectMotion, 6)
87 sp := viewSpring(100, 0)
88 for _, p := range sc.settledLayout {
89 sc.rects[p.ID] = rectMotion{
90 x: c.spring(sp, t0), y: c.spring(sp, t0),
91 w: c.spring(sp, t0), h: c.spring(sp, t0),
92 dx: 100, dy: 100, dw: 100, dh: 100,
93 fade: c.spring(levelSpring(0.5, 0), t0), df: 0.5,
94 dim: c.spring(levelSpring(0.5, 0), t0), ddim: 0.5,
95 scale: true,
96 }
97 }
98 sc.rectsWS = sc.mon.Current()
99}
100
101// checkRunning fails unless every real screen still has six rect motions
102// and its camera motion.
103func checkRunning(t *testing.T, c *Core, when string) {
104 t.Helper()
105 for _, sc := range c.screens {
106 if sc.name() == "" {
107 continue
108 }
109 if len(sc.rects) != 6 || !sc.mon.Current().view.motion.on || len(sc.shown) < 6 {
110 t.Fatalf("%s: rects %d camera %v shown %d", when, len(sc.rects), sc.mon.Current().view.motion.on, len(sc.shown))
111 }
112 }
113}
114
115// TestPublishAllocations pins the cost of publishing an animation frame, on
116// the fallback timer (every screen) and on a page flip of one screen while
117// the other still animates (the flip path keeps the timer).
118func TestPublishAllocations(t *testing.T) {
119 c := publishRig(t, 2)
120 t0 := time.Now()
121 for _, sc := range c.screens {
122 if sc.name() == "" {
123 continue
124 }
125 ws := sc.mon.Current()
126 ws.view.motion = c.spring(viewSpring(100, 0), t0)
127 ws.view.off = 100
128 startRectMotions(c, sc, t0)
129 }
130 for _, tc := range []struct {
131 name string
132 only *screen
133 }{{"fallback", nil}, {"flip", c.screens[1]}} {
134 step := publishStep(t, c, tc.only)
135 step()
136 checkRunning(t, c, "setup")
137 if tc.only != nil && !c.animatingOther(tc.only) {
138 t.Fatal("setup: the other screen does not animate")
139 }
140 n := testing.AllocsPerRun(50, step)
141 t.Logf("%s allocs per step = %v", tc.name, n)
142 checkRunning(t, c, "motions settled during the measure")
143 if n > publishAllocBudget {
144 t.Errorf("%s publish allocs per frame = %v, budget %d", tc.name, n, publishAllocBudget)
145 }
146 }
147}
148
149// TestPublishStashSlideAllocations pins the cost of publishing a frame while
150// the stash slides: windows laid out at a fractional offset, each with a
151// partial veil, with the same budget as any other animation frame.
152func TestPublishStashSlideAllocations(t *testing.T) {
153 c := publishRig(t, 1)
154 sc := c.screens[0]
155 ws := sc.mon.Current()
156 for _, id := range []WindowID{2, 3, 4, 5} {
157 ws.FocusID(id)
158 ws.ToggleWindowStash()
159 }
160 sc.arrange()
161 c.refreshShown()
162 ws.stashView.off = 0.5
163 ws.stashView.motion = c.spring(ws.stashSpring(0.5, 0), time.Now())
164 step := publishStep(t, c, nil)
165 step()
166 if !ws.stashView.motion.on || !ws.stashShown() {
167 t.Fatal("setup: no stash slide on screen")
168 }
169 var partial int
170 for _, p := range sc.shown {
171 if p.Veil > 0 && p.Veil < 1 {
172 partial++
173 }
174 }
175 if partial == 0 {
176 t.Fatal("setup: no window draws a partial veil")
177 }
178 n := testing.AllocsPerRun(50, step)
179 t.Logf("stash slide allocs per step = %v", n)
180 if !ws.stashView.motion.on {
181 t.Fatal("the slide settled during the measure")
182 }
183 if n > publishAllocBudget {
184 t.Errorf("stash slide publish allocs per frame = %v, budget %d", n, publishAllocBudget)
185 }
186}
187
188// TestPublishOverviewAllocations pins the cost of publishing while the
189// overview opens: its cards fly from the full rects (scale and fade motions
190// on every card), which guards the reuse of Core.overviewReal.
191func TestPublishOverviewAllocations(t *testing.T) {
192 c := publishRig(t, 1)
193 sc := c.cur()
194 now := time.Now()
195 shots := c.snapshot(now)
196 sc.mon.ToggleOverview()
197 c.transition(shots, now)
198 step := publishStep(t, c, nil)
199 step()
200 if !sc.mon.ov.open || len(c.overviewReal) != 6 || len(sc.rects) == 0 || !c.animating() {
201 t.Fatalf("setup: overview %v, real layouts %d, rects %d, animating %v", sc.mon.ov.open, len(c.overviewReal), len(sc.rects), c.animating())
202 }
203 n := testing.AllocsPerRun(50, step)
204 t.Logf("overview allocs per step = %v", n)
205 if len(sc.rects) == 0 {
206 t.Fatal("card motions settled during the measure")
207 }
208 if n > publishOverviewAllocBudget {
209 t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
210 }
211}
212
213// TestPublishLeavingAllocations pins the cost of a frame with leaving
214// entries on every screen (two of the six windows closed, fading out, the
215// other four with rect motions): the measured 4, under a frame without
216// them (the leaving entries take no configure target).
217func TestPublishLeavingAllocations(t *testing.T) {
218 c := publishRig(t, 2)
219 t0 := time.Now()
220 for _, sc := range c.screens {
221 if sc.name() == "" {
222 continue
223 }
224 c.refreshShown()
225 layout := sc.settledLayout
226 sc.rects = make(map[WindowID]rectMotion, 6)
227 sp := viewSpring(100, 0)
228 for i, p := range layout {
229 if i < 2 {
230 sc.mon.RemoveWindow(p.ID)
231 c.leaveFrom(sc, p, nil, t0)
232 continue
233 }
234 sc.rects[p.ID] = rectMotion{
235 x: c.spring(sp, t0), y: c.spring(sp, t0),
236 w: c.spring(sp, t0), h: c.spring(sp, t0),
237 dx: 100, dy: 100, dw: 100, dh: 100,
238 }
239 }
240 sc.rectsWS = sc.mon.Current()
241 }
242 step := publishStep(t, c, nil)
243 step()
244 for _, sc := range c.screens {
245 if sc.name() == "" {
246 continue
247 }
248 leaving := 0
249 for _, p := range sc.shown {
250 if p.Leaving {
251 leaving++
252 }
253 }
254 if len(sc.rects) != 6 || leaving != 2 || len(sc.shown) != 6 {
255 t.Fatalf("setup: rects %d leaving %d shown %d", len(sc.rects), leaving, len(sc.shown))
256 }
257 }
258 n := testing.AllocsPerRun(50, step)
259 t.Logf("leaving allocs per step = %v", n)
260 for _, sc := range c.screens {
261 if sc.name() != "" && len(sc.rects) != 6 {
262 t.Fatalf("motions settled during the measure: rects %d", len(sc.rects))
263 }
264 }
265 if n > publishLeavingAllocBudget {
266 t.Errorf("publish allocs per frame with leaving entries = %v, budget %d", n, publishLeavingAllocBudget)
267 }
268}