detached 0f2ac29

fix(wayland): never let a kept pace timer fire before the deadline

commit
0f2ac291f04daea187d472be5179699dcb0ad655
author
brice · · 2026-10-13 18:33 UTC
parent
059a23c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +19 -11
M internal/adapters/wayland/frames.go +8 -6
@@ -41,20 +41,22 @@ func (s *Server) paceNow() {
4141 s.paceTick(nil, nil)
4242 }
4343
44-// paceTolerance is how far a new deadline may differ from the armed one
44+// paceTolerance is how much earlier than the armed deadline a new one may be
4545 // and still reuse it: the ticks of one frame compute the same deadline from
4646 // clock reads a few microseconds apart.
4747 const paceTolerance = 50 * time.Microsecond
4848
4949 // armPace schedules the next flip-less tick d from now on the pace timerfd
50-// (nanosecond precision, one-shot). A deadline equal to the armed one (within paceTolerance) keeps the timer as
51-// it is: a frame's commit, flip and timer ticks mostly end on the same
52-// deadline. The tolerance is measured against the armed deadline, so it never
53-// accumulates. Display goroutine only.
50+// (nanosecond precision, one-shot). A deadline at most paceTolerance before
51+// the armed one keeps the timer as it is: a frame's commit, flip and timer
52+// ticks mostly end on the same deadline. The timer never fires before the
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
55+// against the armed deadline, so it never accumulates. Display goroutine only.
5456 func (s *Server) armPace(d time.Duration) {
5557 deadline := time.Now().Add(max(d, 0))
5658 if s.paceArmed {
57- if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= paceTolerance {
59+ if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 {
5860 return
5961 }
6062 }
@@ -41,20 +41,22 @@ func (s *Server) paceNow() {
41 s.paceTick(nil, nil)41 s.paceTick(nil, nil)
42 }42 }
43 43
44-// paceTolerance is how far a new deadline may differ from the armed one44+// paceTolerance is how much earlier than the armed deadline a new one may be
45 // and still reuse it: the ticks of one frame compute the same deadline from45 // and still reuse it: the ticks of one frame compute the same deadline from
46 // clock reads a few microseconds apart.46 // clock reads a few microseconds apart.
47 const paceTolerance = 50 * time.Microsecond47 const paceTolerance = 50 * time.Microsecond
48 48
49 // armPace schedules the next flip-less tick d from now on the pace timerfd49 // armPace schedules the next flip-less tick d from now on the pace timerfd
50-// (nanosecond precision, one-shot). A deadline equal to the armed one (within paceTolerance) keeps the timer as50+// (nanosecond precision, one-shot). A deadline at most paceTolerance before
51-// it is: a frame's commit, flip and timer ticks mostly end on the same51+// the armed one keeps the timer as it is: a frame's commit, flip and timer
52-// deadline. The tolerance is measured against the armed deadline, so it never52+// ticks mostly end on the same deadline. The timer never fires before the
53-// accumulates. Display goroutine only.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
55+// against the armed deadline, so it never accumulates. Display goroutine only.
54 func (s *Server) armPace(d time.Duration) {56 func (s *Server) armPace(d time.Duration) {
55 deadline := time.Now().Add(max(d, 0))57 deadline := time.Now().Add(max(d, 0))
56 if s.paceArmed {58 if s.paceArmed {
57- if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= paceTolerance {59+ if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 {
58 return60 return
59 }61 }
60 }62 }
M internal/adapters/wayland/pace_inline_test.go +11 -5
@@ -145,18 +145,24 @@ func TestArmPaceSkipsUnchangedDeadline(t *testing.T) {
145145 if s.paceArmed {
146146 t.Fatal("stop of a never armed timer armed it")
147147 }
148- start := time.Now()
149148 s.armPace(time.Hour)
150149 first := s.paceDeadline
151150 if !s.paceArmed || remaining() <= 59*time.Minute {
152151 t.Fatalf("not armed: %v", remaining())
153152 }
154- // Later, with a wait that ends at the same moment (within tolerance): the
155- // timer is kept.
156- s.armPace(time.Hour - time.Since(start))
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)
157156 if !s.paceDeadline.Equal(first) {
158- t.Fatalf("an unchanged deadline 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)
159158 }
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
160166 s.armPace(time.Hour - time.Minute)
161167 if s.paceDeadline.Equal(first) || remaining() > 59*time.Minute {
162168 t.Fatalf("a new deadline kept the old timer: remaining %v", remaining())
@@ -145,18 +145,24 @@ 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- start := time.Now()
149 s.armPace(time.Hour)148 s.armPace(time.Hour)
150 first := s.paceDeadline149 first := s.paceDeadline
151 if !s.paceArmed || remaining() <= 59*time.Minute {150 if !s.paceArmed || remaining() <= 59*time.Minute {
152 t.Fatalf("not armed: %v", remaining())151 t.Fatalf("not armed: %v", remaining())
153 }152 }
154- // Later, with a wait that ends at the same moment (within tolerance): the153+ // A deadline a little before the armed one keeps the timer. A full
155- // timer is kept.154+ // tolerance of margin absorbs the clock advancing between the calls.
156- s.armPace(time.Hour - time.Since(start))155+ s.armPace(time.Until(first) - paceTolerance/2)
157 if !s.paceDeadline.Equal(first) {156 if !s.paceDeadline.Equal(first) {
158- t.Fatalf("an unchanged deadline 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)
159 }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
160 s.armPace(time.Hour - time.Minute)166 s.armPace(time.Hour - time.Minute)
161 if s.paceDeadline.Equal(first) || remaining() > 59*time.Minute {167 if s.paceDeadline.Equal(first) || remaining() > 59*time.Minute {
162 t.Fatalf("a new deadline kept the old timer: remaining %v", remaining())168 t.Fatalf("a new deadline kept the old timer: remaining %v", remaining())

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