feat/data-dnd e05cb15

publish_alloc_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"reflect"
6	"slices"
7	"testing"
8	"time"
9
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13// Allocation budgets of one animation frame (step) on two screens of six
14// windows each, with camera and rect motions (position, fade, dim, scale)
15// running, and on one screen with the overview opening (its cards' scale
16// and fade motions running). Every scene sent is freshly built (immutable
17// once sent), so the floor is not 0. Measured: 43 before presizing the scene
18// windows and separators, 36 after, 32 once the fallback timer is made once
19// and reset per frame (the fallback path, step(ctx, nil), re-arms it; the
20// flip path with another screen animating keeps it and costs the same), then
21// 6 once the layouts are built in the owner's reused buffers (layoutInto:
22// the screens' layout, the workspaces' column, row and tile rects) instead of
23// fresh slices, and 5 once the output layout is built in the scratch too
24// (copied only when it changed). What is left is the scenes themselves
25// (windows, separators, the scene set). Leaving entries (a closed window
26// still fading) add none: 3, less than a frame with six moving windows
27// (fewer configure targets). The opening overview costs 4 alone and 6 with
28// neighbors (5 and 7 under -race); it was 11 and 13 when it rebuilt the row
29// map, the neighbor list, the workspace order (groups map, hidden clone) and
30// the outline slice (appended onto the dividers) every frame. The rows are
31// now a fixed array, the order and dividers monitor scratch, and the scene's
32// separators one slice. What is left is the scene, the stack items and the
33// preview rows.
34const (
35	publishAllocBudget         = 5
36	publishLeavingAllocBudget  = 3
37	publishOverviewAllocBudget = 7
38)
39
40// publishRig is a Core on the real clock (a mock clock would count its own
41// bookkeeping; the fallback timer is then the system one) with screens
42// screens of six windows each. The Slowdown cap keeps every motion running
43// for seconds, so none settles during a measure even under -race or load.
44func publishRig(t testing.TB, screens int) *Core {
45	t.Helper()
46	var cfg ports.Config
47	cfg.Keyboard.CmdKey = "super"
48	cfg.Layout.MaxColumns = 2
49	cfg.Border.Width = 2
50	cfg.Animations.On = true
51	cfg.Animations.Slowdown = 10
52	c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{})
53	if err != nil {
54		t.Fatal(err)
55	}
56	next := WindowID(1)
57	for i := range screens {
58		c.addScreen(ports.OutputInfo{Name: string(rune('A' + i)), Width: 300, Height: 200})
59	}
60	for _, sc := range c.screens {
61		if sc.name() == "" {
62			continue
63		}
64		for range 6 {
65			sc.mon.AddWindow(next)
66			next++
67		}
68		sc.arrange()
69	}
70	c.refreshShown()
71	return c
72}
73
74// publishStep is one animation frame of only (nil: the fallback timer's,
75// every screen) whose scene is taken from the channel.
76func publishStep(t *testing.T, c *Core, only *screen) func() {
77	ctx := context.Background()
78	return func() {
79		if err := c.step(ctx, only); err != nil {
80			t.Fatal(err)
81		}
82		select {
83		case <-c.ch.Scenes:
84		default:
85			t.Fatal("no scene published")
86		}
87	}
88}
89
90// startRectMotions gives every window of sc a rect motion on each
91// component: position and size, fade, dim and content scale.
92func startRectMotions(c *Core, sc *screen, t0 time.Time) {
93	sc.rects = make(map[WindowID]rectMotion, 6)
94	sp := viewSpring(100, 0)
95	for _, p := range sc.settledLayout {
96		sc.rects[p.ID] = rectMotion{
97			x: c.spring(sp, t0), y: c.spring(sp, t0),
98			w: c.spring(sp, t0), h: c.spring(sp, t0),
99			dx: 100, dy: 100, dw: 100, dh: 100,
100			fade: c.spring(levelSpring(0.5, 0), t0), df: 0.5,
101			dim: c.spring(levelSpring(0.5, 0), t0), ddim: 0.5,
102			scale: true,
103		}
104	}
105	sc.rectsWS = sc.mon.Current()
106}
107
108// checkRunning fails unless every real screen still has six rect motions
109// and its camera motion.
110func checkRunning(t *testing.T, c *Core, when string) {
111	t.Helper()
112	for _, sc := range c.screens {
113		if sc.name() == "" {
114			continue
115		}
116		if len(sc.rects) != 6 || !sc.mon.Current().view.motion.on || len(sc.shown) < 6 {
117			t.Fatalf("%s: rects %d camera %v shown %d", when, len(sc.rects), sc.mon.Current().view.motion.on, len(sc.shown))
118		}
119	}
120}
121
122// TestPublishAllocations pins the cost of publishing an animation frame, on
123// the fallback timer (every screen) and on a page flip of one screen while
124// the other still animates (the flip path keeps the timer).
125func TestPublishAllocations(t *testing.T) {
126	c := publishRig(t, 2)
127	t0 := time.Now()
128	for _, sc := range c.screens {
129		if sc.name() == "" {
130			continue
131		}
132		ws := sc.mon.Current()
133		ws.view.motion = c.spring(viewSpring(100, 0), t0)
134		ws.view.off = 100
135		startRectMotions(c, sc, t0)
136	}
137	for _, tc := range []struct {
138		name string
139		only *screen
140	}{{"fallback", nil}, {"flip", c.screens[1]}} {
141		step := publishStep(t, c, tc.only)
142		step()
143		checkRunning(t, c, "setup")
144		if tc.only != nil && !c.animatingOther(tc.only) {
145			t.Fatal("setup: the other screen does not animate")
146		}
147		n := testing.AllocsPerRun(50, step)
148		t.Logf("%s allocs per step = %v", tc.name, n)
149		checkRunning(t, c, "motions settled during the measure")
150		if n > publishAllocBudget {
151			t.Errorf("%s publish allocs per frame = %v, budget %d", tc.name, n, publishAllocBudget)
152		}
153	}
154}
155
156// TestPublishStashSlideAllocations pins the cost of publishing a frame while
157// the stash slides: windows laid out at a fractional offset, each with a
158// partial veil, with the same budget as any other animation frame.
159func TestPublishStashSlideAllocations(t *testing.T) {
160	c := publishRig(t, 1)
161	sc := c.screens[0]
162	ws := sc.mon.Current()
163	for _, id := range []WindowID{2, 3, 4, 5} {
164		ws.FocusID(id)
165		ws.ToggleWindowStash()
166	}
167	sc.arrange()
168	c.refreshShown()
169	ws.stashView.off = 0.5
170	ws.stashView.motion = c.spring(ws.stashSpring(0.5, 0), time.Now())
171	step := publishStep(t, c, nil)
172	step()
173	if !ws.stashView.motion.on || !ws.stashShown() {
174		t.Fatal("setup: no stash slide on screen")
175	}
176	var partial int
177	for _, p := range sc.shown {
178		if p.Veil > 0 && p.Veil < 1 {
179			partial++
180		}
181	}
182	if partial == 0 {
183		t.Fatal("setup: no window draws a partial veil")
184	}
185	n := testing.AllocsPerRun(50, step)
186	t.Logf("stash slide allocs per step = %v", n)
187	if !ws.stashView.motion.on {
188		t.Fatal("the slide settled during the measure")
189	}
190	if n > publishAllocBudget {
191		t.Errorf("stash slide publish allocs per frame = %v, budget %d", n, publishAllocBudget)
192	}
193}
194
195// TestPublishOverviewAllocations pins the cost of publishing while the
196// overview opens: its cards fly from the full rects (scale and fade motions
197// on every card), which guards the reuse of Core.overviewReal. The current
198// workspace is scroll or cascade, alone or between two workspaces with
199// windows (their slots and stacks are laid out every frame too).
200func TestPublishOverviewAllocations(t *testing.T) {
201	for _, overflow := range []Overflow{OverflowScroll, OverflowCascade} {
202		for _, neighbors := range []bool{false, true} {
203			name := string(overflow) + "/alone"
204			if neighbors {
205				name = string(overflow) + "/neighbors"
206			}
207			t.Run(name, func(t *testing.T) {
208				c := publishRig(t, 1)
209				sc := c.cur()
210				m := sc.mon
211				if neighbors {
212					// Windows 1..6 are on workspace 0: move to 1 with a
213					// workspace on each side.
214					m.Focus(1)
215					for id := WindowID(10); id < 16; id++ {
216						m.AddWindow(id)
217					}
218					m.Focus(2)
219					for id := WindowID(20); id < 26; id++ {
220						m.AddWindow(id)
221					}
222					m.Focus(1)
223				}
224				m.Current().Overflow = overflow
225				sc.arrange()
226				c.refreshShown()
227				now := time.Now()
228				shots := c.snapshot(now)
229				m.ToggleOverview()
230				c.transition(shots, now)
231				step := publishStep(t, c, nil)
232				step()
233				want, slots := 6, 1
234				if neighbors {
235					want, slots = 18, 3
236				}
237				if rows, _ := m.overviewRows(); rows.n != slots {
238					t.Fatalf("setup: %d overview slots, want %d", rows.n, slots)
239				}
240				if !m.ov.open || m.overviewAxis() != m.Current().policy().workspace || len(c.overviewReal) != want || len(sc.rects) == 0 || !c.animating() {
241					t.Fatalf("setup: overview %v, real layouts %d (want %d), rects %d, animating %v", m.ov.open, len(c.overviewReal), want, len(sc.rects), c.animating())
242				}
243				// The rows themselves cost nothing: array slots, scratch order.
244				if n := testing.AllocsPerRun(50, func() { m.overviewRows() }); n != 0 {
245					t.Errorf("overviewRows allocs = %v, want 0", n)
246				}
247				n := testing.AllocsPerRun(50, step)
248				t.Logf("%s overview allocs per step = %v", name, n)
249				if len(sc.rects) == 0 {
250					t.Fatal("card motions settled during the measure")
251				}
252				if n > publishOverviewAllocBudget {
253					t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
254				}
255			})
256		}
257	}
258	// A named row below its anchor adds the boundary divider: the scene
259	// copies it into its own separators, one slice like the outline alone.
260	t.Run("named", func(t *testing.T) {
261		c := publishRig(t, 1)
262		sc := c.cur()
263		m := sc.mon
264		m.SetNamed([]NamedWorkspace{{Name: "dev"}})
265		m.ToggleNamed("dev")
266		m.AddWindow(30)
267		m.ToggleNamed("dev")
268		sc.arrange()
269		c.refreshShown()
270		now := time.Now()
271		shots := c.snapshot(now)
272		m.ToggleOverview()
273		c.transition(shots, now)
274		step := publishStep(t, c, nil)
275		step()
276		if _, dividers := m.overviewRows(); len(dividers) != 1 || !m.ov.open || len(sc.rects) == 0 {
277			t.Fatalf("setup: %d dividers, overview %v, rects %d", len(dividers), m.ov.open, len(sc.rects))
278		}
279		n := testing.AllocsPerRun(50, step)
280		t.Logf("named overview allocs per step = %v", n)
281		if len(sc.rects) == 0 {
282			t.Fatal("card motions settled during the measure")
283		}
284		if n > publishOverviewAllocBudget {
285			t.Errorf("named overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
286		}
287	})
288	// The column switcher's cards are previews too: a held switch shows
289	// them, and a publish while their motions run costs a plain frame.
290	t.Run("switcher", func(t *testing.T) {
291		c := publishRig(t, 1)
292		c.cmdMod = ports.ModSuper
293		c.mods = ports.ModSuper
294		sc := c.cur()
295		m := sc.mon
296		m.SetMaxColumns(3)
297		sc.arrange()
298		c.refreshShown()
299		ctx := context.Background()
300		if err := c.runBind(ctx, ActionSwitchColumnNext); err != nil {
301			t.Fatal(err)
302		}
303		if !c.switching() || !c.switcherTick() || !m.switcherShown() {
304			t.Fatalf("setup: switching %v, shown %v", c.switching(), m.switcherShown())
305		}
306		// The rig slows every spring: the card motions run for the whole measure.
307		step := publishStep(t, c, nil)
308		step()
309		if len(sc.rects) == 0 || !c.animating() || len(c.overviewReal) == 0 {
310			t.Fatalf("setup: %d rect motions, animating %v, real layouts %d", len(sc.rects), c.animating(), len(c.overviewReal))
311		}
312		publish := func() {
313			if err := c.publish(ctx); err != nil {
314				t.Fatal(err)
315			}
316			select {
317			case <-c.ch.Scenes:
318			default:
319				t.Fatal("no scene published")
320			}
321		}
322		n := testing.AllocsPerRun(50, publish)
323		t.Logf("switcher allocs per publish = %v", n)
324		if len(sc.rects) == 0 {
325			t.Fatal("card motions settled during the measure")
326		}
327		if n > publishAllocBudget {
328			t.Errorf("switcher publish allocs per frame = %v, budget %d", n, publishAllocBudget)
329		}
330	})
331}
332
333// TestPublishLeavingAllocations pins the cost of a frame with leaving
334// entries on every screen (two of the six windows closed, fading out, the
335// other four with rect motions): the measured 4, under a frame without
336// them (the leaving entries take no configure target).
337func TestPublishLeavingAllocations(t *testing.T) {
338	c := publishRig(t, 2)
339	t0 := time.Now()
340	for _, sc := range c.screens {
341		if sc.name() == "" {
342			continue
343		}
344		c.refreshShown()
345		layout := sc.settledLayout
346		sc.rects = make(map[WindowID]rectMotion, 6)
347		sp := viewSpring(100, 0)
348		for i, p := range layout {
349			if i < 2 {
350				sc.mon.RemoveWindow(p.ID)
351				c.leaveFrom(sc, p, nil, t0)
352				continue
353			}
354			sc.rects[p.ID] = rectMotion{
355				x: c.spring(sp, t0), y: c.spring(sp, t0),
356				w: c.spring(sp, t0), h: c.spring(sp, t0),
357				dx: 100, dy: 100, dw: 100, dh: 100,
358			}
359		}
360		sc.rectsWS = sc.mon.Current()
361	}
362	step := publishStep(t, c, nil)
363	step()
364	for _, sc := range c.screens {
365		if sc.name() == "" {
366			continue
367		}
368		leaving := 0
369		for _, p := range sc.shown {
370			if p.Leaving {
371				leaving++
372			}
373		}
374		if len(sc.rects) != 6 || leaving != 2 || len(sc.shown) != 6 {
375			t.Fatalf("setup: rects %d leaving %d shown %d", len(sc.rects), leaving, len(sc.shown))
376		}
377	}
378	n := testing.AllocsPerRun(50, step)
379	t.Logf("leaving allocs per step = %v", n)
380	for _, sc := range c.screens {
381		if sc.name() != "" && len(sc.rects) != 6 {
382			t.Fatalf("motions settled during the measure: rects %d", len(sc.rects))
383		}
384	}
385	if n > publishLeavingAllocBudget {
386		t.Errorf("publish allocs per frame with leaving entries = %v, budget %d", n, publishLeavingAllocBudget)
387	}
388}
389
390// TestPublishSubscribersAllocations pins the cost of an animation frame
391// with the state and workspace subscribers attached: their snapshots are
392// built in the owner's scratch and, unchanged during a camera and rect
393// animation, add nothing to the frame. Measured: 32 when each publish built
394// fresh snapshots (state, workspaces and output layout), 5 since. Sent
395// values never share storage with the scratch they were built in.
396func TestPublishSubscribersAllocations(t *testing.T) {
397	c := publishRig(t, 2)
398	c.ch.State = make(chan ports.State, 1)
399	c.ch.Workspaces = make(chan ports.Workspaces, 1)
400	t0 := time.Now()
401	for _, sc := range c.screens {
402		if sc.name() == "" {
403			continue
404		}
405		ws := sc.mon.Current()
406		ws.view.motion = c.spring(viewSpring(100, 0), t0)
407		ws.view.off = 100
408		startRectMotions(c, sc, t0)
409	}
410	step := publishStep(t, c, nil)
411	step()
412	checkRunning(t, c, "setup")
413	if len(c.ch.State) != 1 || len(c.ch.Workspaces) != 1 {
414		t.Fatal("setup: no snapshot sent")
415	}
416	st := <-c.ch.State
417	ws := <-c.ch.Workspaces
418	n := testing.AllocsPerRun(50, step)
419	t.Logf("subscribers allocs per step = %v", n)
420	checkRunning(t, c, "motions settled during the measure")
421	if n > publishAllocBudget {
422		t.Errorf("publish allocs per frame with subscribers = %v, budget %d", n, publishAllocBudget)
423	}
424	if len(c.ch.State) != 0 || len(c.ch.Workspaces) != 0 {
425		t.Error("an unchanged snapshot was sent again")
426	}
427	// Sent values own their storage: the next publish rewrites the scratch
428	// in place.
429	sc := &c.scratch
430	if st.Window == nil || len(st.Windows) == 0 || len(ws.Outputs) == 0 || len(c.sent.outputs.Outputs) == 0 {
431		t.Fatalf("setup: empty snapshots %+v %+v", st, ws)
432	}
433	if &st.Windows[0] == &sc.state.Windows[:1][0] || &st.Outputs[0] == &sc.state.Outputs[:1][0] || inSlice(st.Window, sc.state.Windows) || inSlice(st.Window, st.Windows) {
434		t.Error("the sent state shares the scratch")
435	}
436	for _, sent := range []ports.Workspaces{ws, c.sent.workspaces} {
437		if &sent.Outputs[0] == &sc.workspaces.Outputs[:1][0] {
438			t.Error("the sent workspaces share the scratch")
439		}
440		for k, o := range sent.Outputs {
441			if &o.Workspaces[0] == &sc.workspaces.Outputs[k].Workspaces[:1][0] {
442				t.Errorf("the sent workspaces of %s share the scratch", o.Name)
443			}
444		}
445	}
446	if &c.sent.outputs.Outputs[0] == &sc.layout[:1][0] {
447		t.Error("the sent output layout shares the scratch")
448	}
449	// And a later change leaves them as they were.
450	want := ports.State{Output: st.Output, Outputs: slices.Clone(st.Outputs), Windows: slices.Clone(st.Windows)}
451	w := *st.Window
452	want.Window = &w
453	c.windows.setIdleInhibit(st.Windows[0].ID, true)
454	c.publishState()
455	// A sent value aliasing the scratch would always equal it: nothing sent.
456	select {
457	case got := <-c.ch.State:
458		if !got.Windows[0].IdleInhibit {
459			t.Fatal("change not published")
460		}
461	default:
462		t.Fatal("change not published")
463	}
464	if !reflect.DeepEqual(st, want) {
465		t.Fatal("a sent snapshot changed after a later publish")
466	}
467}
468
469func inSlice[T any](p *T, s []T) bool {
470	s = s[:cap(s)]
471	for i := range s {
472		if p == &s[i] {
473			return true
474		}
475	}
476	return false
477}
478
479// BenchmarkPublishSubscribers is one animation frame on two screens with
480// the state and workspace subscribers attached.
481func BenchmarkPublishSubscribers(b *testing.B) {
482	c := publishRig(b, 2)
483	c.ch.State = make(chan ports.State, 1)
484	c.ch.Workspaces = make(chan ports.Workspaces, 1)
485	t0 := time.Now()
486	for _, sc := range c.screens {
487		if sc.name() == "" {
488			continue
489		}
490		ws := sc.mon.Current()
491		ws.view.motion = c.spring(viewSpring(100, 0), t0)
492		ws.view.off = 100
493		startRectMotions(c, sc, t0)
494	}
495	ctx := context.Background()
496	b.ReportAllocs()
497	for b.Loop() {
498		if err := c.step(ctx, nil); err != nil {
499			b.Fatal(err)
500		}
501		<-c.ch.Scenes
502	}
503}

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 25

Tags 10

↑↓ moveenter openesc close