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

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