1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/neferwl/internal/ports"
9 "golang.org/x/sys/unix"
10)
11
12// paceFixture is a server whose surface waits on an output whose next
13// frame is an hour away: a commit queues its callback and nothing fires.
14func paceFixture(t *testing.T, timer bool) (*Server, *surface) {
15 t.Helper()
16 out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
17 s := &Server{
18 outputs: []*output{out},
19 awaiting: map[string][]*wayland.Callback{},
20 frameDue: map[string]time.Time{"DP-2": time.Now().Add(time.Hour)},
21 fifoSurfaces: map[*surface]struct{}{},
22 lastFlip: map[string]time.Time{},
23 flips: map[string]ports.FlipInfo{},
24 reports: map[string]ports.OutputPresented{},
25 }
26 if timer {
27 fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC)
28 if err != nil {
29 t.Fatal(err)
30 }
31 t.Cleanup(func() { unix.Close(fd) })
32 s.paceFD, s.paceReady = fd, true
33 }
34 w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
35 surf := &surface{server: s, xdg: &xdgSurface{window: w}}
36 surf.next.callbacks = []*wayland.Callback{new(wayland.Callback)}
37 return s, surf
38}
39
40// A commit on the display goroutine ticks the pacer inline: it arms the
41// pace timer itself and posts no tick, which would only run later on the
42// display goroutine it is already on.
43func TestCommitTicksPacerInline(t *testing.T) {
44 s, surf := paceFixture(t, true)
45 surf.queueAndTick()
46 if len(s.awaiting["DP-2"]) != 1 {
47 t.Fatalf("callback not queued: %+v", s.awaiting)
48 }
49 if s.paceSignal.Load() {
50 t.Fatal("commit posted a tick")
51 }
52 waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline")
53 if s.paceDepth != 0 || s.paceDirty {
54 t.Fatalf("pace state leaked: depth %d dirty %v", s.paceDepth, s.paceDirty)
55 }
56}
57
58// Without a registered pace timer the wake falls back to a posted tick.
59func TestCommitWakesPacerWithoutTimer(t *testing.T) {
60 s, surf := paceFixture(t, false)
61 surf.queueAndTick()
62 if !s.paceSignal.Load() {
63 t.Fatal("tick not signalled")
64 }
65}
66
67// Wakes inside a tick coalesce into one more pass, not one posted tick each.
68func TestWakePacerDefersInsideTick(t *testing.T) {
69 s, _ := paceFixture(t, true)
70 s.paceDepth = 1
71 s.wakePacer()
72 s.wakePacer()
73 if s.paceSignal.Load() || !s.paceDirty {
74 t.Fatalf("wake inside a tick: signalled %v dirty %v", s.paceSignal.Load(), s.paceDirty)
75 }
76 s.paceDepth = 0
77 s.wakePacer()
78 if !s.paceSignal.Load() {
79 t.Fatal("wake outside a tick lost")
80 }
81}
82
83// A commit that waits (a fifo barrier) is timed by the inline tick.
84func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) {
85 s, surf := paceFixture(t, true)
86 surf.next.callbacks = nil
87 surf.barrier, surf.barrierAt = true, time.Now()
88 surf.next.wait = true
89 surf.queueAndTick()
90 if len(surf.queue) != 1 || s.paceSignal.Load() {
91 t.Fatalf("queued %d, tick signalled %v", len(surf.queue), s.paceSignal.Load())
92 }
93 waitArmed(t, s, "timer not armed for the barrier deadline")
94}
95
96// waitArmed asserts the timerfd was armed: it is pending, or it already
97// fired and holds its expiry.
98func waitArmed(t *testing.T, s *Server, msg string) {
99 t.Helper()
100 var cur unix.ItimerSpec
101 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
102 t.Fatal(err)
103 }
104 if cur.Value != (unix.Timespec{}) {
105 return
106 }
107 var buf [8]byte
108 if n, _ := unix.Read(s.paceFD, buf[:]); n != 8 {
109 t.Fatal(msg)
110 }
111}
112
113// A synchronized subsurface commit waits for its parent's commit: it does
114// not tick the pacer, so a burst of child commits costs no ticks.
115func TestSyncedCommitDoesNotTick(t *testing.T) {
116 s, surf := paceFixture(t, true)
117 surf.sub.parent = &surface{server: s}
118 surf.sub.synced = true
119 surf.queueAndTick()
120 if len(surf.queue) != 1 || !surf.queue[0].synced {
121 t.Fatalf("synced commit not queued: %d", len(surf.queue))
122 }
123 var cur unix.ItimerSpec
124 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
125 t.Fatal(err)
126 }
127 if s.paceSignal.Load() || cur.Value != (unix.Timespec{}) {
128 t.Fatal("synced commit ticked the pacer")
129 }
130}
131
132// An arm for the deadline already armed (within paceTolerance) keeps the
133// timer; another deadline re-arms it; an expiry consumed by onPaceTimer, or a
134// stop, leaves it disarmed, and a stop of a disarmed timer does nothing.
135func TestArmPaceSkipsUnchangedDeadline(t *testing.T) {
136 s, _ := paceFixture(t, true)
137 remaining := func() time.Duration {
138 var cur unix.ItimerSpec
139 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
140 t.Fatal(err)
141 }
142 return time.Duration(cur.Value.Nano())
143 }
144 s.stopPace()
145 if s.paceArmed {
146 t.Fatal("stop of a never armed timer armed it")
147 }
148 s.armPace(time.Hour)
149 first := s.paceDeadline
150 if !s.paceArmed || remaining() <= 59*time.Minute {
151 t.Fatalf("not armed: %v", remaining())
152 }
153 // A deadline a little before the armed one keeps the timer. A full
154 // tolerance of margin absorbs the clock advancing between the calls.
155 s.armPace(time.Until(first) - paceTolerance/2)
156 if !s.paceDeadline.Equal(first) {
157 t.Fatalf("an earlier deadline within tolerance re-armed the timer: %v -> %v", first, s.paceDeadline)
158 }
159 // A later deadline, however close, re-arms: the timer must never fire
160 // before the requested deadline.
161 s.armPace(time.Until(first) + time.Microsecond)
162 if !s.paceDeadline.After(first) {
163 t.Fatalf("a later deadline kept the earlier timer: %v -> %v", first, s.paceDeadline)
164 }
165 first = s.paceDeadline
166 s.armPace(time.Hour - time.Minute)
167 if s.paceDeadline.Equal(first) || remaining() > 59*time.Minute {
168 t.Fatalf("a new deadline kept the old timer: remaining %v", remaining())
169 }
170 s.stopPace()
171 if s.paceArmed || remaining() != 0 {
172 t.Fatalf("stop left the timer armed: %v", remaining())
173 }
174 // One-shot expiry: the fired timer is disarmed, so a stop is a no-op and
175 // the next arm sets the timer again even for the same deadline.
176 s.armPace(time.Nanosecond)
177 fds := []unix.PollFd{{Fd: int32(s.paceFD), Events: unix.POLLIN}}
178 if n, err := unix.Poll(fds, 5000); n != 1 {
179 t.Fatalf("the timer did not fire: %d, %v", n, err)
180 }
181 s.onPaceTimer()
182 if s.paceArmed {
183 t.Fatal("expiry did not disarm")
184 }
185 s.stopPace() // no syscall to observe: it must not re-arm anything
186 if remaining() != 0 {
187 t.Fatalf("stop after the expiry: %v", remaining())
188 }
189 s.armPace(time.Hour)
190 if !s.paceArmed || remaining() <= 59*time.Minute {
191 t.Fatalf("not re-armed after the expiry: %v", remaining())
192 }
193}