feat/data-dnd e05cb15

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// makeRoomLocked on a full ring: which report it merges or moves, and what it
178// reports.
179func TestPresentationRingMakeRoom(t *testing.T) {
180	tests := []struct {
181		name       string
182		ring       func() *presentSink
183		in         ports.OutputPresented
184		wantMerged bool
185		check      func(t *testing.T, p *presentSink, in *ports.OutputPresented)
186	}{
187		{
188			// A full ring merges the oldest flip of an output into its next flip.
189			name: "merges past the bound",
190			ring: func() *presentSink {
191				p := ringOf()
192				for i := range maxPresentRing {
193					p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
194				}
195				return p
196			},
197			in:         ports.OutputPresented{Output: "A", Flip: flipOf(maxPresentRing + 1)},
198			wantMerged: true,
199			check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
200				if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 {
201					t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged)
202				}
203			},
204		},
205		{
206			// A flip followed by a flip-less report of the same output is not
207			// lost: the later report carries it.
208			name: "keeps a flip before a flip-less report",
209			ring: func() *presentSink {
210				p := ringOf("A", "A")
211				p.ring[0].Flip = flipOf(7, 3)
212				p.fill()
213				p.ring = p.ring[:maxPresentRing]
214				return p
215			},
216			in: ports.OutputPresented{Output: "B", Flip: flipOf(9)},
217			check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
218				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 {
219					t.Fatalf("flip lost: ring %d, first %+v", len(p.ring), p.ring[0])
220				}
221			},
222		},
223		{
224			// With several outputs interleaved, the victim is the oldest report
225			// that has a successor of its own output; other outputs' reports are
226			// untouched.
227			name: "interleaved outputs",
228			ring: func() *presentSink {
229				p := ringOf("A", "B", "A", "B")
230				p.ring[0].Flip, p.ring[1].Flip = flipOf(1, 10), flipOf(2, 20)
231				p.ring[2].Flip, p.ring[3].Flip = flipOf(3, 11), nil
232				p.fill()
233				return p
234			},
235			in:         ports.OutputPresented{Output: "C"},
236			wantMerged: true,
237			check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
238				if p.ring[0].Output != "B" || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 0 {
239					t.Fatalf("B's flip touched: %+v", p.ring[0])
240				}
241				a := p.ring[1]
242				if a.Output != "A" || a.Flip.Seq != 3 || a.Flip.Merged != 1 || a.Flip.Shows[10] != 1 || a.Flip.Shows[11] != 3 {
243					t.Fatalf("A's flips not merged: %+v", a.Flip)
244				}
245				if p.ring[2].Output != "B" || p.ring[2].Flip != nil {
246					t.Fatalf("B's flip-less report moved: %+v", p.ring[2])
247				}
248			},
249		},
250		{
251			// The incoming report counts as the successor of a ring report.
252			name: "incoming takes over a flip",
253			ring: func() *presentSink {
254				p := ringOf("A")
255				p.ring[0].Flip = flipOf(4)
256				p.fill()
257				return p
258			},
259			in: ports.OutputPresented{Output: "A"},
260			check: func(t *testing.T, p *presentSink, in *ports.OutputPresented) {
261				if in.Flip == nil || in.Flip.Seq != 4 || len(p.ring) != maxPresentRing-1 {
262					t.Fatalf("incoming %+v, ring %d", in.Flip, len(p.ring))
263				}
264			},
265		},
266		{
267			// With no successor for any report (more outputs than ring slots)
268			// nothing is dropped: the ring grows by one and every flip is still
269			// delivered.
270			name: "without a successor keeps everything",
271			ring: func() *presentSink {
272				p := ringOf()
273				for i := range maxPresentRing {
274					p.ring = append(p.ring, ports.OutputPresented{Output: "o" + string(rune('A'+i/26)) + string(rune('a'+i%26)), Flip: flipOf(uint64(i + 1))})
275				}
276				return p
277			},
278			in: ports.OutputPresented{Output: "other", Flip: flipOf(99)},
279			check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
280				if len(p.ring) != maxPresentRing {
281					t.Fatalf("merged or dropped: ring %d", len(p.ring))
282				}
283			},
284		},
285	}
286	for _, tt := range tests {
287		t.Run(tt.name, func(t *testing.T) {
288			p := tt.ring()
289			in := tt.in
290			if merged := p.makeRoomLocked(&in); merged != tt.wantMerged {
291				t.Fatalf("merged %v, want %v", merged, tt.wantMerged)
292			}
293			tt.check(t, p, &in)
294		})
295	}
296}
297
298// Merges are counted, and warned about at most once per mergeWarnEvery.
299func TestPresentationRingWarnsRateLimited(t *testing.T) {
300	now := time.Unix(1000, 0)
301	clk := portsmocks.NewMockClock(t)
302	clk.EXPECT().Now().RunAndReturn(func() time.Time { return now })
303	p := ringOf()
304	p.s = &Server{clock: clk}
305	for i := range maxPresentRing {
306		p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
307	}
308	push := func(seq uint64) (bool, int) {
309		return p.enqueueLocked(ports.OutputPresented{Output: "A", Flip: flipOf(seq)})
310	}
311	if warn, n := push(100); !warn || n != 1 {
312		t.Fatalf("first merge: warn %v merged %d", warn, n)
313	}
314	for i := range 3 {
315		now = now.Add(time.Second)
316		if warn, _ := push(uint64(101 + i)); warn {
317			t.Fatalf("warned again after %v", time.Duration(i+1)*time.Second)
318		}
319	}
320	now = now.Add(mergeWarnEvery)
321	if warn, n := push(200); !warn || n != 4 {
322		t.Fatalf("after the interval: warn %v merged %d, want the 4 merges since", warn, n)
323	}
324}
325
326// A stalled display loop makes outputs merge flips; wayland says so once.
327func TestPresentationMergeWarnsInLog(t *testing.T) {
328	var out lockedBuffer
329	log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
330	s, _, _, _, _ := contentServerWith(t, Options{}, log)
331	release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
332	go func() {
333		s.display.Do(func() { close(started); <-release })
334		close(finished)
335	}()
336	<-started
337	for i := range 3 * maxPresentRing {
338		s.Presentation().Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}})
339	}
340	close(release)
341	<-finished
342	s.display.Do(func() {})
343	got := out.String()
344	if n := strings.Count(got, `"level":"warn"`); n != 1 {
345		t.Fatalf("%d warnings: %s", n, got)
346	}
347	if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) {
348		t.Fatalf("warning lacks fields: %s", got)
349	}
350}
351
352// Pushing a report and draining it on the display goroutine allocates nothing
353// in steady state. The count includes the display goroutine: AllocsPerRun
354// reads the global malloc counter.
355func TestPresentationPushAllocations(t *testing.T) {
356	if raceEnabled {
357		t.Skip("the race detector makes the purego wake callback allocate")
358	}
359	s, _, _, _, sink, _ := presentServer(t)
360	r := ports.OutputPresented{Output: "HEADLESS-1"}
361	// Posts run in order: once the marker ran, the drain before it did.
362	// The marker is bound once and the channel is reused, so waiting for it
363	// allocates nothing.
364	ran := make(chan struct{}, 1)
365	marker := func() { ran <- struct{}{} }
366	for range 4 {
367		sink.Push(r)
368		s.display.Post(marker)
369		<-ran
370	}
371	allocs := testing.AllocsPerRun(200, func() {
372		sink.Push(r)
373		s.display.Post(marker)
374		<-ran
375	})
376	if allocs != 0 {
377		t.Errorf("push + drain: %.1f allocs, want 0", allocs)
378	}
379}
380
381// A fifo barrier on an output that does not flip clears at barrierAt+period,
382// driven by the pace timerfd. On a one-second output (so nothing expires during
383// the test) the timerfd must be armed, no later than that deadline (other waits,
384// like held buffers, may be earlier); and its callback, run once the deadline
385// has passed, applies the queued commit.
386func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) {
387	s, events, commands, _, dir := contentServerWith(t, Options{Outputs: slowOutputs}, testLog())
388	c := protocolClient(t, s, dir)
389	w, surf, _ := surfaceMapper(t, c, events)()
390	placeWindow(t, s, commands, w, "HEADLESS-1")
391	fifoQueue(t, c, surf)
392	var remaining, want, period time.Duration
393	var queued, left int
394	var barrier bool
395	var err error
396	s.display.Do(func() {
397		root := s.windows[w.ID].xdg.surface
398		period, queued = s.outputPeriod(s.fifoOutput(root)), len(root.queue)
399		var spec unix.ItimerSpec
400		err = unix.TimerfdGettime(s.paceFD, &spec)
401		remaining = time.Duration(spec.Value.Nano())
402		want = time.Until(root.barrierAt.Add(period))
403		// The deadline passes: the timerfd callback clears the barrier.
404		root.barrierAt = time.Now().Add(-period)
405		s.onPaceTimer()
406		barrier, left = root.barrier, len(root.queue)
407	})
408	if err != nil {
409		t.Fatal(err)
410	}
411	if queued != 1 || period != time.Second {
412		t.Fatalf("queued %d commits, period %v: the second commit must wait for the barrier", queued, period)
413	}
414	if remaining <= 0 || remaining > want+50*time.Millisecond {
415		t.Fatalf("pace timerfd fires in %v, want armed at or before barrierAt+period (%v)", remaining, want)
416	}
417	if barrier || left != 0 {
418		t.Fatalf("timer callback after the deadline: barrier %v, %d queued", barrier, left)
419	}
420}
421
422func testLog() zerowrap.Logger {
423	return logging.For(context.Background(), "wayland")
424}
425
426// fifoQueue sets a barrier with one commit and queues a second commit behind
427// wait_barrier.
428func fifoQueue(t *testing.T, c *wlturbo.Display, surf uint32) {
429	t.Helper()
430	manager := bindProtocol(t, c, "wp_fifo_manager_v1")
431	f := c.AllocateID()
432	registerProtocol(t, c, f)
433	requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
434	buf := shmBuffer(t, c)
435	requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
436	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
437	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
438	requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
439	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
440	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
441	if err := c.Roundtrip(); err != nil {
442		t.Fatal(err)
443	}
444}

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 26

Tags 10

↑↓ moveenter openesc close