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}