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)})
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().motion.on || len(sc.shown) < 6 {
110 t.Fatalf("%s: rects %d camera %v shown %d", when, len(sc.rects), sc.mon.Current().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.motion = c.spring(viewSpring(100, 0), t0)
127 ws.shift = 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// TestPublishOverviewAllocations pins the cost of publishing while the
150// overview opens: its cards fly from the full rects (scale and fade motions
151// on every card), which guards the reuse of Core.overviewReal.
152func TestPublishOverviewAllocations(t *testing.T) {
153 c := publishRig(t, 1)
154 sc := c.cur()
155 now := time.Now()
156 shots := c.snapshot(now)
157 sc.mon.ToggleOverview()
158 c.transition(shots, now)
159 step := publishStep(t, c, nil)
160 step()
161 if !sc.mon.ov.open || len(c.overviewReal) != 6 || len(sc.rects) == 0 || !c.animating() {
162 t.Fatalf("setup: overview %v, real layouts %d, rects %d, animating %v", sc.mon.ov.open, len(c.overviewReal), len(sc.rects), c.animating())
163 }
164 n := testing.AllocsPerRun(50, step)
165 t.Logf("overview allocs per step = %v", n)
166 if len(sc.rects) == 0 {
167 t.Fatal("card motions settled during the measure")
168 }
169 if n > publishOverviewAllocBudget {
170 t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
171 }
172}
173
174// TestPublishLeavingAllocations pins the cost of a frame with leaving
175// entries on every screen (two of the six windows closed, fading out, the
176// other four with rect motions): the measured 4, under a frame without
177// them (the leaving entries take no configure target).
178func TestPublishLeavingAllocations(t *testing.T) {
179 c := publishRig(t, 2)
180 t0 := time.Now()
181 for _, sc := range c.screens {
182 if sc.name() == "" {
183 continue
184 }
185 c.refreshShown()
186 layout := sc.settledLayout
187 sc.rects = make(map[WindowID]rectMotion, 6)
188 sp := viewSpring(100, 0)
189 for i, p := range layout {
190 if i < 2 {
191 sc.mon.RemoveWindow(p.ID)
192 c.leaveFrom(sc, p, nil, t0)
193 continue
194 }
195 sc.rects[p.ID] = rectMotion{
196 x: c.spring(sp, t0), y: c.spring(sp, t0),
197 w: c.spring(sp, t0), h: c.spring(sp, t0),
198 dx: 100, dy: 100, dw: 100, dh: 100,
199 }
200 }
201 sc.rectsWS = sc.mon.Current()
202 }
203 step := publishStep(t, c, nil)
204 step()
205 for _, sc := range c.screens {
206 if sc.name() == "" {
207 continue
208 }
209 leaving := 0
210 for _, p := range sc.shown {
211 if p.Leaving {
212 leaving++
213 }
214 }
215 if len(sc.rects) != 6 || leaving != 2 || len(sc.shown) != 6 {
216 t.Fatalf("setup: rects %d leaving %d shown %d", len(sc.rects), leaving, len(sc.shown))
217 }
218 }
219 n := testing.AllocsPerRun(50, step)
220 t.Logf("leaving allocs per step = %v", n)
221 for _, sc := range c.screens {
222 if sc.name() != "" && len(sc.rects) != 6 {
223 t.Fatalf("motions settled during the measure: rects %d", len(sc.rects))
224 }
225 }
226 if n > publishLeavingAllocBudget {
227 t.Errorf("publish allocs per frame with leaving entries = %v, budget %d", n, publishLeavingAllocBudget)
228 }
229}