presented_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"encoding/binary"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/go-wayland-bindings/server/fifo"
11	"github.com/bnema/go-wayland-bindings/server/wayland"
12	"github.com/bnema/neferwl/internal/adapters/logging"
13	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
14	"github.com/bnema/neferwl/internal/ports"
15	"github.com/bnema/wlturbo"
16	"github.com/bnema/zerowrap"
17	"golang.org/x/sys/unix"
18)
19
20// slowOutputs has two outputs refreshing once a second: their deadlines never
21// fire inside a test, so only flips pace their frame callbacks.
22var slowOutputs = ports.Layout{
23	{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080, RefreshMilli: 1000}, Width: 1920, Height: 1080, Scale: 1},
24	{Info: ports.OutputInfo{Name: "HEADLESS-2", Width: 1920, Height: 1080, RefreshMilli: 1000}, X: 1920, Width: 1920, Height: 1080, Scale: 1},
25}
26
27// frameDoneProxy records the time of a wl_callback.done.
28type frameDoneProxy struct {
29	wlturbo.BaseProxy
30	done chan uint32
31}
32
33func (p *frameDoneProxy) Dispatch(e *wlturbo.Event) {
34	if e.Opcode == uint16(wayland.CallbackEventDone) {
35		p.done <- binary.NativeEndian.Uint32(e.Data())
36	}
37}
38
39// requestFrame commits the surface with a new frame callback.
40func requestFrame(t *testing.T, c *wlturbo.Display, surf uint32) *frameDoneProxy {
41	t.Helper()
42	cb := c.AllocateID()
43	p := &frameDoneProxy{done: make(chan uint32, 2)}
44	p.SetID(cb)
45	registerWireProxy(c, p)
46	requestProtocol(t, c, surf, wayland.SurfaceRequestFrame, cb)
47	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
48	if err := c.Roundtrip(); err != nil {
49		t.Fatal(err)
50	}
51	return p
52}
53
54// awaitFrame waits for the callback to be answered and returns its time.
55func awaitFrame(t *testing.T, c *wlturbo.Display, p *frameDoneProxy) uint32 {
56	t.Helper()
57	for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
58		if err := c.Roundtrip(); err != nil {
59			t.Fatal(err)
60		}
61		select {
62		case ms := <-p.done:
63			return ms
64		case <-time.After(2 * time.Millisecond):
65		}
66	}
67	t.Fatal("frame callback not answered")
68	return 0
69}
70
71// placeWindow puts the window on an output and waits until wayland applied it.
72func placeWindow(t *testing.T, s *Server, commands chan<- ports.ClientCommand, w ports.WindowMapped, output string) {
73	t.Helper()
74	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: output, Visible: true}
75	for deadline := time.Now().Add(2 * time.Second); ; {
76		var got string
77		s.display.Do(func() { got = s.frameOutput(s.windows[w.ID].xdg.surface) })
78		if got == output {
79			return
80		}
81		if time.Now().After(deadline) {
82			t.Fatalf("window not placed on %s", output)
83		}
84		time.Sleep(time.Millisecond)
85	}
86}
87
88// Flips pushed while the display goroutine is busy are paced by one tick per
89// output, not one per stale flip. The frame callback of each output is
90// answered once, with the time of that output's last flip: a tick per flip
91// would have answered it with the first flip's time.
92func TestPresentationPushCoalescesWhileDisplayBusy(t *testing.T) {
93	s, events, commands, _, sink, dir := presentServerOn(t, slowOutputs)
94	c := protocolClient(t, s, dir)
95	mk := surfaceMapper(t, c, events)
96	outputs := []string{"HEADLESS-1", "HEADLESS-2"}
97	surfs := make([]uint32, len(outputs))
98	for i, name := range outputs {
99		w, surf, _ := mk()
100		placeWindow(t, s, commands, w, name)
101		surfs[i] = surf
102		// The first callback of an idle output is answered at once and
103		// starts its one-second period: the next one waits for a flip.
104		awaitFrame(t, c, requestFrame(t, c, surf))
105	}
106	waiting := make([]*frameDoneProxy, len(outputs))
107	for i := range outputs {
108		waiting[i] = requestFrame(t, c, surfs[i])
109		select {
110		case ms := <-waiting[i].done:
111			t.Fatalf("%s answered before any flip (at %d ms)", outputs[i], ms)
112		default:
113		}
114	}
115	var base time.Duration
116	release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
117	go func() {
118		s.display.Do(func() { base = monotonic(s.started); close(started); <-release })
119		close(finished)
120	}()
121	<-started
122	const flips = 5
123	const step = 100 * time.Millisecond
124	for i := range flips {
125		for _, name := range outputs {
126			sink.Push(ports.OutputPresented{Output: name, Flip: &ports.FlipInfo{When: base + time.Duration(i+1)*step, Seq: uint64(i + 1), HardwareClock: true}})
127		}
128	}
129	close(release)
130	<-finished
131	last := uint32((flips * step).Milliseconds())
132	for i, name := range outputs {
133		ms := awaitFrame(t, c, waiting[i])
134		// base is read on the display goroutine a little before the server
135		// answers: a few ms of slack, far below one step.
136		if ms < last-5 || ms > last+5 {
137			t.Fatalf("%s answered at %d ms, want the last flip's %d ms (the first flip is at %d)", name, ms, last, step.Milliseconds())
138		}
139	}
140	var seq uint64
141	var reported bool
142	s.display.Do(func() {
143		seq = s.flips["HEADLESS-1"].Seq
144		_, reported = s.reports["HEADLESS-1"]
145	})
146	if !reported || seq != flips {
147		t.Fatalf("last flip %d, reported %v: every flip must still be answered in order", seq, reported)
148	}
149}
150
151func ringOf(outputs ...string) *presentSink {
152	p := &presentSink{ring: make([]ports.OutputPresented, 0, maxPresentRing+1)}
153	for _, o := range outputs {
154		p.ring = append(p.ring, ports.OutputPresented{Output: o})
155	}
156	return p
157}
158
159// fill pads the ring with reports of distinct outputs up to the bound.
160func (p *presentSink) fill() {
161	for i := 0; len(p.ring) < maxPresentRing; i++ {
162		p.ring = append(p.ring, ports.OutputPresented{Output: "pad" + string(rune('A'+i/26)) + string(rune('a'+i%26))})
163	}
164}
165
166func flipOf(seq uint64, shows ...ports.WindowID) *ports.FlipInfo {
167	f := &ports.FlipInfo{Seq: seq}
168	if len(shows) > 0 {
169		f.Shows = map[ports.WindowID]uint64{}
170		for _, id := range shows {
171			f.Shows[id] = seq
172		}
173	}
174	return f
175}
176
177// A full ring merges the oldest flip of an output into its next flip.
178func TestPresentationRingMergesPastBound(t *testing.T) {
179	p := ringOf()
180	for i := range maxPresentRing {
181		p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
182	}
183	in := ports.OutputPresented{Output: "A", Flip: flipOf(maxPresentRing + 1)}
184	if !p.makeRoomLocked(&in) {
185		t.Fatal("two flips merged but not reported")
186	}
187	if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 {
188		t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged)
189	}
190}
191
192// A flip followed by a flip-less report of the same output is not lost: the
193// later report carries it.
194func TestPresentationRingKeepsFlipBeforeFliplessReport(t *testing.T) {
195	p := ringOf("A", "A")
196	p.ring[0].Flip = flipOf(7, 3)
197	p.fill()
198	p.ring = p.ring[:maxPresentRing]
199	in := ports.OutputPresented{Output: "B", Flip: flipOf(9)}
200	if p.makeRoomLocked(&in) {
201		t.Fatal("no flips merged, yet reported")
202	}
203	if len(p.ring) != maxPresentRing-1 || p.ring[0].Output != "A" || p.ring[0].Flip == nil || p.ring[0].Flip.Seq != 7 || p.ring[0].Flip.Merged != 0 {
204		t.Fatalf("flip lost: ring %d, first %+v", len(p.ring), p.ring[0])
205	}
206}
207
208// With several outputs interleaved, the victim is the oldest report that has
209// a successor of its own output; other outputs' reports are untouched.
210func TestPresentationRingInterleavedOutputs(t *testing.T) {
211	p := ringOf("A", "B", "A", "B")
212	p.ring[0].Flip, p.ring[1].Flip = flipOf(1, 10), flipOf(2, 20)
213	p.ring[2].Flip, p.ring[3].Flip = flipOf(3, 11), nil
214	p.fill()
215	in := ports.OutputPresented{Output: "C"}
216	if !p.makeRoomLocked(&in) {
217		t.Fatal("merge not reported")
218	}
219	if p.ring[0].Output != "B" || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 0 {
220		t.Fatalf("B's flip touched: %+v", p.ring[0])
221	}
222	a := p.ring[1]
223	if a.Output != "A" || a.Flip.Seq != 3 || a.Flip.Merged != 1 || a.Flip.Shows[10] != 1 || a.Flip.Shows[11] != 3 {
224		t.Fatalf("A's flips not merged: %+v", a.Flip)
225	}
226	if p.ring[2].Output != "B" || p.ring[2].Flip != nil {
227		t.Fatalf("B's flip-less report moved: %+v", p.ring[2])
228	}
229}
230
231// The incoming report counts as the successor of a ring report.
232func TestPresentationRingIncomingTakesOverFlip(t *testing.T) {
233	p := ringOf("A")
234	p.ring[0].Flip = flipOf(4)
235	p.fill()
236	in := ports.OutputPresented{Output: "A"}
237	if p.makeRoomLocked(&in) {
238		t.Fatal("no merge expected")
239	}
240	if in.Flip == nil || in.Flip.Seq != 4 || len(p.ring) != maxPresentRing-1 {
241		t.Fatalf("incoming %+v, ring %d", in.Flip, len(p.ring))
242	}
243}
244
245// With no successor for any report (more outputs than ring slots) nothing is
246// dropped: the ring grows by one and every flip is still delivered.
247func TestPresentationRingWithoutSuccessorKeepsEverything(t *testing.T) {
248	p := ringOf()
249	for i := range maxPresentRing {
250		p.ring = append(p.ring, ports.OutputPresented{Output: "o" + string(rune('A'+i/26)) + string(rune('a'+i%26)), Flip: flipOf(uint64(i + 1))})
251	}
252	in := ports.OutputPresented{Output: "other", Flip: flipOf(99)}
253	if p.makeRoomLocked(&in) || len(p.ring) != maxPresentRing {
254		t.Fatalf("merged or dropped: ring %d", len(p.ring))
255	}
256}
257
258// Merges are counted, and warned about at most once per mergeWarnEvery.
259func TestPresentationRingWarnsRateLimited(t *testing.T) {
260	now := time.Unix(1000, 0)
261	clk := portsmocks.NewMockClock(t)
262	clk.EXPECT().Now().RunAndReturn(func() time.Time { return now })
263	p := ringOf()
264	p.s = &Server{clock: clk}
265	for i := range maxPresentRing {
266		p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
267	}
268	push := func(seq uint64) (bool, int) {
269		return p.enqueueLocked(ports.OutputPresented{Output: "A", Flip: flipOf(seq)})
270	}
271	if warn, n := push(100); !warn || n != 1 {
272		t.Fatalf("first merge: warn %v merged %d", warn, n)
273	}
274	for i := range 3 {
275		now = now.Add(time.Second)
276		if warn, _ := push(uint64(101 + i)); warn {
277			t.Fatalf("warned again after %v", time.Duration(i+1)*time.Second)
278		}
279	}
280	now = now.Add(mergeWarnEvery)
281	if warn, n := push(200); !warn || n != 4 {
282		t.Fatalf("after the interval: warn %v merged %d, want the 4 merges since", warn, n)
283	}
284}
285
286// A stalled display loop makes outputs merge flips; wayland says so once.
287func TestPresentationMergeWarnsInLog(t *testing.T) {
288	var out lockedBuffer
289	log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
290	s, _, _, _, _ := contentServerWith(t, Options{}, log)
291	release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
292	go func() {
293		s.display.Do(func() { close(started); <-release })
294		close(finished)
295	}()
296	<-started
297	for i := range 3 * maxPresentRing {
298		s.Presentation().Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}})
299	}
300	close(release)
301	<-finished
302	s.display.Do(func() {})
303	got := out.String()
304	if n := strings.Count(got, "outputs report faster than the display loop drains; flips merged"); n != 1 {
305		t.Fatalf("%d warnings: %s", n, got)
306	}
307	if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) {
308		t.Fatalf("warning lacks fields: %s", got)
309	}
310}
311
312// Pushing a report and draining it on the display goroutine allocates nothing
313// in steady state. The count includes the display goroutine: AllocsPerRun
314// reads the global malloc counter.
315func TestPresentationPushAllocations(t *testing.T) {
316	if raceEnabled {
317		t.Skip("the race detector makes the purego wake callback allocate")
318	}
319	s, _, _, _, sink, _ := presentServer(t)
320	r := ports.OutputPresented{Output: "HEADLESS-1"}
321	// Posts run in order: once the marker ran, the drain before it did.
322	// The marker is bound once and the channel is reused, so waiting for it
323	// allocates nothing.
324	ran := make(chan struct{}, 1)
325	marker := func() { ran <- struct{}{} }
326	for range 4 {
327		sink.Push(r)
328		s.display.Post(marker)
329		<-ran
330	}
331	allocs := testing.AllocsPerRun(200, func() {
332		sink.Push(r)
333		s.display.Post(marker)
334		<-ran
335	})
336	if allocs != 0 {
337		t.Errorf("push + drain: %.1f allocs, want 0", allocs)
338	}
339}
340
341// A fifo barrier on an output that does not flip clears at barrierAt+period,
342// driven by the pace timerfd. On a one-second output (so nothing expires during
343// the test) the timerfd must be armed, no later than that deadline (other waits,
344// like held buffers, may be earlier); and its callback, run once the deadline
345// has passed, applies the queued commit.
346func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) {
347	s, events, commands, _, dir := contentServerWith(t, Options{Outputs: slowOutputs}, testLog())
348	c := protocolClient(t, s, dir)
349	w, surf, _ := surfaceMapper(t, c, events)()
350	placeWindow(t, s, commands, w, "HEADLESS-1")
351	fifoQueue(t, c, surf)
352	var remaining, want, period time.Duration
353	var queued, left int
354	var barrier bool
355	var err error
356	s.display.Do(func() {
357		root := s.windows[w.ID].xdg.surface
358		period, queued = s.outputPeriod(s.fifoOutput(root)), len(root.queue)
359		var spec unix.ItimerSpec
360		err = unix.TimerfdGettime(s.paceFD, &spec)
361		remaining = time.Duration(spec.Value.Nano())
362		want = time.Until(root.barrierAt.Add(period))
363		// The deadline passes: the timerfd callback clears the barrier.
364		root.barrierAt = time.Now().Add(-period)
365		s.onPaceTimer()
366		barrier, left = root.barrier, len(root.queue)
367	})
368	if err != nil {
369		t.Fatal(err)
370	}
371	if queued != 1 || period != time.Second {
372		t.Fatalf("queued %d commits, period %v: the second commit must wait for the barrier", queued, period)
373	}
374	if remaining <= 0 || remaining > want+50*time.Millisecond {
375		t.Fatalf("pace timerfd fires in %v, want armed at or before barrierAt+period (%v)", remaining, want)
376	}
377	if barrier || left != 0 {
378		t.Fatalf("timer callback after the deadline: barrier %v, %d queued", barrier, left)
379	}
380}
381
382// End to end on a 20 Hz output: the queued commit is applied at barrierAt+50 ms
383// (the timerfd), not at the next 100 ms held-buffer tick; the bound is generous
384// (half a period) against scheduler noise.
385func TestPaceTimerfdClearsBarrierOnDeadline(t *testing.T) {
386	out := ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080, RefreshMilli: 20000}, Width: 1920, Height: 1080, Scale: 1}}
387	s, events, commands, contents, dir := contentServerWith(t, Options{Outputs: out}, testLog())
388	c := protocolClient(t, s, dir)
389	w, surf, _ := surfaceMapper(t, c, events)()
390	placeWindow(t, s, commands, w, "HEADLESS-1")
391	nextContent(t, contents, w.ID) // the mapping commit
392	fifoQueue(t, c, surf)
393	var barrierAt time.Time
394	var period time.Duration
395	s.display.Do(func() {
396		root := s.windows[w.ID].xdg.surface
397		barrierAt, period = root.barrierAt, s.outputPeriod(s.fifoOutput(root))
398	})
399	nextContent(t, contents, w.ID) // the commit that set the barrier
400	late := nextContent(t, contents, w.ID).Sub(barrierAt.Add(period))
401	// Never early; late by far less than the period (not by a polling tick).
402	if late < -time.Millisecond || late > period/2 {
403		t.Fatalf("queued commit applied %v after barrierAt+%v", late, period)
404	}
405}
406
407func testLog() zerowrap.Logger {
408	return logging.For(context.Background(), "wayland")
409}
410
411// fifoQueue sets a barrier with one commit and queues a second commit behind
412// wait_barrier.
413func fifoQueue(t *testing.T, c *wlturbo.Display, surf uint32) {
414	t.Helper()
415	manager := bindProtocol(t, c, "wp_fifo_manager_v1")
416	f := c.AllocateID()
417	registerProtocol(t, c, f)
418	requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
419	buf := shmBuffer(t, c)
420	requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
421	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
422	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
423	requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
424	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
425	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
426	if err := c.Roundtrip(); err != nil {
427		t.Fatal(err)
428	}
429}

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