test(wayland): remove wall-clock margins from pace timer tests
The unchanged-deadline test now arms from one clock reading (armPaceFrom) instead of relying on 25 us between two reads. The end-to-end 20 Hz barrier timing test is removed: under -race its upper bound depended on scheduling, and the deterministic armed-deadline test covers the contract. The merge warning test checks level and fields instead of the message text.
- commit
- 40240cd18aee456b8dceeffcb7a1671cbe3c2326
- author
- brice · · 2026-10-14 03:07 UTC
- committer
- brice · · 2026-10-14 04:34 UTC
- parent
- c7a0951
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +11 -33
M internal/adapters/wayland/frames.go +5 -2
| @@ -53,8 +53,11 @@ const paceTolerance = 50 * time.Microsecond | |||
| 53 | 53 | // requested deadline, since the tick's checks are strict and an early tick | |
| 54 | 54 | // would push due work back by their minimum re-arm. The tolerance is measured | |
| 55 | 55 | // against the armed deadline, so it never accumulates. Display goroutine only. | |
| 56 | - | func (s *Server) armPace(d time.Duration) { | |
| 57 | - | deadline := time.Now().Add(max(d, 0)) | |
| 56 | + | func (s *Server) armPace(d time.Duration) { s.armPaceFrom(time.Now(), d) } | |
| 57 | + | ||
| 58 | + | // armPaceFrom is armPace with the clock read at now. | |
| 59 | + | func (s *Server) armPaceFrom(now time.Time, d time.Duration) { | |
| 60 | + | deadline := now.Add(max(d, 0)) | |
| 58 | 61 | if s.paceArmed { | |
| 59 | 62 | if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 { | |
| 60 | 63 | return | |
| @@ -53,8 +53,11 @@ const paceTolerance = 50 * time.Microsecond | |||||
| 53 | // requested deadline, since the tick's checks are strict and an early tick | 53 | // requested deadline, since the tick's checks are strict and an early tick | ||
| 54 | // would push due work back by their minimum re-arm. The tolerance is measured | 54 | // would push due work back by their minimum re-arm. The tolerance is measured | ||
| 55 | // against the armed deadline, so it never accumulates. Display goroutine only. | 55 | // against the armed deadline, so it never accumulates. Display goroutine only. | ||
| 56 | - | func (s *Server) armPace(d time.Duration) { | 56 | + | func (s *Server) armPace(d time.Duration) { s.armPaceFrom(time.Now(), d) } |
| 57 | - | deadline := time.Now().Add(max(d, 0)) | 57 | + | |
| 58 | + | // armPaceFrom is armPace with the clock read at now. | |||
| 59 | + | func (s *Server) armPaceFrom(now time.Time, d time.Duration) { | |||
| 60 | + | deadline := now.Add(max(d, 0)) | |||
| 58 | if s.paceArmed { | 61 | if s.paceArmed { | ||
| 59 | if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 { | 62 | if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 { | ||
| 60 | return | 63 | return | ||
M internal/adapters/wayland/pace_inline_test.go +5 -5
| @@ -145,20 +145,20 @@ func TestArmPaceSkipsUnchangedDeadline(t *testing.T) { | |||
| 145 | 145 | if s.paceArmed { | |
| 146 | 146 | t.Fatal("stop of a never armed timer armed it") | |
| 147 | 147 | } | |
| 148 | - | s.armPace(time.Hour) | |
| 148 | + | now := time.Now() | |
| 149 | + | s.armPaceFrom(now, time.Hour) | |
| 149 | 150 | first := s.paceDeadline | |
| 150 | 151 | if !s.paceArmed || remaining() <= 59*time.Minute { | |
| 151 | 152 | t.Fatalf("not armed: %v", remaining()) | |
| 152 | 153 | } | |
| 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) | |
| 154 | + | // A deadline a little before the armed one keeps the timer. | |
| 155 | + | s.armPaceFrom(now, time.Hour-paceTolerance/2) | |
| 156 | 156 | if !s.paceDeadline.Equal(first) { | |
| 157 | 157 | t.Fatalf("an earlier deadline within tolerance re-armed the timer: %v -> %v", first, s.paceDeadline) | |
| 158 | 158 | } | |
| 159 | 159 | // A later deadline, however close, re-arms: the timer must never fire | |
| 160 | 160 | // before the requested deadline. | |
| 161 | - | s.armPace(time.Until(first) + time.Microsecond) | |
| 161 | + | s.armPaceFrom(now, time.Hour+time.Nanosecond) | |
| 162 | 162 | if !s.paceDeadline.After(first) { | |
| 163 | 163 | t.Fatalf("a later deadline kept the earlier timer: %v -> %v", first, s.paceDeadline) | |
| 164 | 164 | } | |
| @@ -145,20 +145,20 @@ func TestArmPaceSkipsUnchangedDeadline(t *testing.T) { | |||||
| 145 | if s.paceArmed { | 145 | if s.paceArmed { | ||
| 146 | t.Fatal("stop of a never armed timer armed it") | 146 | t.Fatal("stop of a never armed timer armed it") | ||
| 147 | } | 147 | } | ||
| 148 | - | s.armPace(time.Hour) | 148 | + | now := time.Now() |
| 149 | + | s.armPaceFrom(now, time.Hour) | |||
| 149 | first := s.paceDeadline | 150 | first := s.paceDeadline | ||
| 150 | if !s.paceArmed || remaining() <= 59*time.Minute { | 151 | if !s.paceArmed || remaining() <= 59*time.Minute { | ||
| 151 | t.Fatalf("not armed: %v", remaining()) | 152 | t.Fatalf("not armed: %v", remaining()) | ||
| 152 | } | 153 | } | ||
| 153 | - | // A deadline a little before the armed one keeps the timer. A full | 154 | + | // A deadline a little before the armed one keeps the timer. |
| 154 | - | // tolerance of margin absorbs the clock advancing between the calls. | 155 | + | s.armPaceFrom(now, time.Hour-paceTolerance/2) |
| 155 | - | s.armPace(time.Until(first) - paceTolerance/2) | |||
| 156 | if !s.paceDeadline.Equal(first) { | 156 | if !s.paceDeadline.Equal(first) { | ||
| 157 | t.Fatalf("an earlier deadline within tolerance re-armed the timer: %v -> %v", first, s.paceDeadline) | 157 | t.Fatalf("an earlier deadline within tolerance re-armed the timer: %v -> %v", first, s.paceDeadline) | ||
| 158 | } | 158 | } | ||
| 159 | // A later deadline, however close, re-arms: the timer must never fire | 159 | // A later deadline, however close, re-arms: the timer must never fire | ||
| 160 | // before the requested deadline. | 160 | // before the requested deadline. | ||
| 161 | - | s.armPace(time.Until(first) + time.Microsecond) | 161 | + | s.armPaceFrom(now, time.Hour+time.Nanosecond) |
| 162 | if !s.paceDeadline.After(first) { | 162 | if !s.paceDeadline.After(first) { | ||
| 163 | t.Fatalf("a later deadline kept the earlier timer: %v -> %v", first, s.paceDeadline) | 163 | t.Fatalf("a later deadline kept the earlier timer: %v -> %v", first, s.paceDeadline) | ||
| 164 | } | 164 | } | ||
M internal/adapters/wayland/presented_test.go +1 -26
| @@ -301,7 +301,7 @@ func TestPresentationMergeWarnsInLog(t *testing.T) { | |||
| 301 | 301 | <-finished | |
| 302 | 302 | s.display.Do(func() {}) | |
| 303 | 303 | got := out.String() | |
| 304 | - | if n := strings.Count(got, "outputs report faster than the display loop drains; flips merged"); n != 1 { | |
| 304 | + | if n := strings.Count(got, `"level":"warn"`); n != 1 { | |
| 305 | 305 | t.Fatalf("%d warnings: %s", n, got) | |
| 306 | 306 | } | |
| 307 | 307 | if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) { | |
| @@ -379,31 +379,6 @@ func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) { | |||
| 379 | 379 | } | |
| 380 | 380 | } | |
| 381 | 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. | |
| 385 | - | func 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 | - | ||
| 407 | 382 | func testLog() zerowrap.Logger { | |
| 408 | 383 | return logging.For(context.Background(), "wayland") | |
| 409 | 384 | } | |
| @@ -301,7 +301,7 @@ func TestPresentationMergeWarnsInLog(t *testing.T) { | |||||
| 301 | <-finished | 301 | <-finished | ||
| 302 | s.display.Do(func() {}) | 302 | s.display.Do(func() {}) | ||
| 303 | got := out.String() | 303 | got := out.String() | ||
| 304 | - | if n := strings.Count(got, "outputs report faster than the display loop drains; flips merged"); n != 1 { | 304 | + | if n := strings.Count(got, `"level":"warn"`); n != 1 { |
| 305 | t.Fatalf("%d warnings: %s", n, got) | 305 | t.Fatalf("%d warnings: %s", n, got) | ||
| 306 | } | 306 | } | ||
| 307 | if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) { | 307 | if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) { | ||
| @@ -379,31 +379,6 @@ func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) { | |||||
| 379 | } | 379 | } | ||
| 380 | } | 380 | } | ||
| 381 | 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. | |||
| 385 | - | func 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 | - | ||||
| 407 | func testLog() zerowrap.Logger { | 382 | func testLog() zerowrap.Logger { | ||
| 408 | return logging.For(context.Background(), "wayland") | 383 | return logging.For(context.Background(), "wayland") | ||
| 409 | } | 384 | } | ||