main 3a820d6

frame_lifecycle_test.go RawHistoryFolder

1package drm
2
3import (
4	"sync/atomic"
5	"testing"
6	"time"
7)
8
9// All transitions below use values only: no event reader, timer, clock or
10// KMS mock is needed to prove the serial and deadline rules.
11func TestFrameLifecycleStaleFlipAndRecovery(t *testing.T) {
12	var serials atomic.Uint64
13	l := frameLifecycle{serials: &serials}
14	start := time.Unix(100, 0)
15	old := l.userData(userFrame)
16	l.begin(pendingFrame{frame: true, queued: 7}, start)
17	l.endPending() // successful recovery modeset invalidates the old event
18	current := l.userData(userFrame)
19	l.begin(pendingFrame{frame: true, queued: 8}, start.Add(time.Millisecond))
20	if _, _, ok := l.flip(old); ok || !l.pending {
21		t.Fatal("old event completed new commit")
22	}
23	f, _, ok := l.flip(current)
24	if !ok || l.pending || !f.frame || f.queued != 8 {
25		t.Fatalf("new event: ok=%v pending=%v frame=%+v", ok, l.pending, f)
26	}
27}
28
29func TestFrameLifecycleBusyRetryAndLimit(t *testing.T) {
30	var serials atomic.Uint64
31	l := frameLifecycle{serials: &serials, stuckAfter: 200 * time.Millisecond}
32	start := time.Unix(100, 0)
33	if !l.busy(start) || l.stuckAt != start.Add(busyRetry) {
34		t.Fatal("initial EBUSY did not arm retry")
35	}
36	if got := l.timeout(start.Add(busyRetry-time.Nanosecond), true); got != timeoutEarly {
37		t.Fatalf("premature deadline: %d", got)
38	}
39	if got := l.timeout(start.Add(busyRetry), true); got != timeoutRetry || l.pending {
40		t.Fatalf("first retry: %d pending %v", got, l.pending)
41	}
42	l.busy(start.Add(busyRetry))
43	if _, _, ok := l.flip(999 << userKindBits); !ok || l.pending {
44		t.Fatal("CRTC event did not unblock EBUSY")
45	}
46	for i := 3; i < 6; i++ {
47		l.busy(start.Add(time.Duration(i) * busyRetry))
48		if got := l.timeout(start.Add(time.Duration(i+1)*busyRetry), true); got != timeoutRetry {
49			t.Fatalf("busy retry %d: %d", i, got)
50		}
51	}
52	l.busy(start.Add(6 * busyRetry))
53	if got := l.timeout(start.Add(7*busyRetry), true); got != timeoutBusy {
54		t.Fatalf("long EBUSY did not request modeset: %d", got)
55	}
56	if !l.busy(start.Add(time.Second)) {
57		t.Fatal("later refusal did not start a new busy run")
58	}
59	if got := l.timeout(start.Add(time.Second+busyRetry), true); got != timeoutRetry {
60		t.Fatalf("new busy run: %d", got)
61	}
62}
63
64func TestFrameLifecycleFenceAndMissingEvent(t *testing.T) {
65	var serials atomic.Uint64
66	l := frameLifecycle{serials: &serials, stuckAfter: 100 * time.Millisecond}
67	start := time.Unix(100, 0)
68	user := l.userData(userFrame)
69	l.begin(pendingFrame{frame: true}, start)
70	if got := l.timeout(start.Add(100*time.Millisecond), false); got != timeoutFence || !l.pending || l.stuckAt != start.Add(200*time.Millisecond) {
71		t.Fatalf("unsignalled fence: %d pending %v deadline %v", got, l.pending, l.stuckAt)
72	}
73	if got := l.timeout(start.Add(200*time.Millisecond), false); got != timeoutEarly || !l.pending {
74		t.Fatalf("repeated fence wait: %d", got)
75	}
76	if got := l.timeout(start.Add(300*time.Millisecond), true); got != timeoutMissing || l.pending {
77		t.Fatalf("missing event after fence: %d pending %v", got, l.pending)
78	}
79	if _, _, ok := l.flip(user); ok {
80		t.Fatal("late event completed abandoned commit")
81	}
82}
83
84func TestFrameLifecycleTransitionsAllocations(t *testing.T) {
85	var serials atomic.Uint64
86	l := frameLifecycle{serials: &serials}
87	start := time.Unix(100, 0)
88	if allocs := testing.AllocsPerRun(100, func() {
89		user := l.userData(userFrame)
90		l.begin(pendingFrame{frame: true}, start)
91		l.flip(user)
92		l.busy(start)
93		l.timeout(start.Add(busyRetry), true)
94	}); allocs != 0 {
95		t.Fatalf("frame lifecycle: %.1f allocs, want 0", allocs)
96	}
97}
98
99func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) {
100	l := frameLifecycle{stuckAfter: time.Millisecond}
101	start := time.Unix(100, 0)
102	now := start
103	for i := range 20 {
104		attempt, fresh := l.nomem(now)
105		want := min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7)))
106		if attempt != i+1 || fresh != (i == 0) || l.nomemBackoff != want || l.stuckAt != now.Add(want) {
107			t.Fatalf("attempt %d fresh=%v backoff=%v deadline=%v", attempt, fresh, l.nomemBackoff, l.stuckAt)
108		}
109		if _, _, ok := l.flip(123 << userKindBits); ok {
110			t.Fatal("stale event bypassed memory backoff")
111		}
112		if got := l.timeout(l.stuckAt.Add(10*l.stuckLimit()), true); got != timeoutRetry {
113			t.Fatalf("ENOMEM escalated: %d", got)
114		}
115		now = l.stuckAt
116	}
117	attempts, outage := l.nomemRecovered(now)
118	if attempts != 20 || outage != now.Sub(start) || !l.nomemSince.IsZero() || !l.lastNomem.IsZero() || l.nomemBackoff != 0 || l.nomemAttempts != 0 {
119		t.Fatalf("recovery: %d %v %+v", attempts, outage, l)
120	}
121	l.nomem(now)
122	l.resetAfterModeset()
123	if !l.nomemSince.IsZero() || l.nomemAttempts != 0 || l.pending {
124		t.Fatal("modeset did not clear run")
125	}
126}
127
128func TestFrameLifecycleNoMemThenBusyEvent(t *testing.T) {
129	l := frameLifecycle{}
130	start := time.Unix(100, 0)
131	l.nomem(start)
132	if got := l.timeout(l.stuckAt, true); got != timeoutRetry {
133		t.Fatalf("memory wait: %v", got)
134	}
135	l.busy(start.Add(nomemRetryMin))
136	if _, _, ok := l.flip(123 << userKindBits); !ok || l.pending || l.waitKind != waitNone {
137		t.Fatal("memory history prevented a CRTC event from ending EBUSY")
138	}
139	if l.nomemAttempts != 1 || l.nomemSince != start {
140		t.Fatal("busy event lost memory recovery summary")
141	}
142}
143
144func TestFrameLifecycleNoMemThenBusyLimit(t *testing.T) {
145	l := frameLifecycle{stuckAfter: 200 * time.Millisecond}
146	start := time.Unix(100, 0)
147	l.nomem(start)
148	l.timeout(l.stuckAt, true)
149	busyStart := start.Add(nomemRetryMin)
150	for i := range 4 {
151		l.busy(busyStart.Add(time.Duration(i) * busyRetry))
152		want := timeoutRetry
153		if i == 3 {
154			want = timeoutBusy
155		}
156		if got := l.timeout(l.stuckAt, true); got != want {
157			t.Fatalf("busy attempt %d: got %v want %v", i+1, got, want)
158		}
159	}
160	if l.nomemAttempts != 1 || l.nomemSince != start {
161		t.Fatal("busy timeout lost memory recovery summary")
162	}
163}

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