fix(drm): separate the frame wait reason from the memory-refusal run
- commit
- 7703bb9e384ac91419bb87626e931a3226b6e64c
- author
- brice · · 2026-10-22 12:04 UTC
- parent
- d707400
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +56 -3
M internal/adapters/drm/frame_lifecycle.go +19 -3
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | // deadline ticks. Only Run uses the timer; the transitions take plain values. | |
| 14 | 14 | type frameLifecycle struct { | |
| 15 | 15 | pending bool | |
| 16 | + | waitKind frameWaitKind | |
| 16 | 17 | pendingFrame pendingFrame | |
| 17 | 18 | serials *atomic.Uint64 // shared across outputs on the card | |
| 18 | 19 | pendingSerial, nextSerial uint64 | |
| @@ -27,6 +28,16 @@ type frameLifecycle struct { | |||
| 27 | 28 | stuckTimer *time.Timer | |
| 28 | 29 | } | |
| 29 | 30 | ||
| 31 | + | // frameWaitKind is the current wait, independent of refusal-run summaries. | |
| 32 | + | type frameWaitKind uint8 | |
| 33 | + | ||
| 34 | + | const ( | |
| 35 | + | waitNone frameWaitKind = iota | |
| 36 | + | waitFlip | |
| 37 | + | waitBusy | |
| 38 | + | waitNoMem | |
| 39 | + | ) | |
| 40 | + | ||
| 30 | 41 | // pendingFrame is what the pending frame commit shows. | |
| 31 | 42 | type pendingFrame struct { | |
| 32 | 43 | security ports.SecurityState // admission epoch checked again at KMS boundary | |
| @@ -94,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 { | |||
| 94 | 105 | func (l *frameLifecycle) endPending() { | |
| 95 | 106 | l.pendingFrame.closeFences() | |
| 96 | 107 | l.pending, l.pendingFrame = false, pendingFrame{} | |
| 108 | + | l.waitKind = waitNone | |
| 97 | 109 | } | |
| 98 | 110 | ||
| 99 | 111 | // begin records a successful nonblocking commit awaiting its event. | |
| @@ -101,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) { | |||
| 101 | 113 | l.pendingSerial = l.nextSerial | |
| 102 | 114 | l.pendingFrame.closeFences() | |
| 103 | 115 | l.pending, l.pendingFrame, l.flipStart = true, f, now | |
| 116 | + | l.waitKind = waitFlip | |
| 104 | 117 | l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false | |
| 105 | 118 | l.busySince = time.Time{} | |
| 106 | 119 | } | |
| @@ -126,6 +139,7 @@ func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) { | |||
| 126 | 139 | l.lastNomem = now | |
| 127 | 140 | l.nomemAttempts++ | |
| 128 | 141 | l.pending, l.pendingSerial, l.flipStart = true, 0, now | |
| 142 | + | l.waitKind = waitNoMem | |
| 129 | 143 | l.stuckAt = now.Add(l.nomemBackoff) | |
| 130 | 144 | return l.nomemAttempts, newRun | |
| 131 | 145 | } | |
| @@ -158,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool { | |||
| 158 | 172 | l.endPending() | |
| 159 | 173 | l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry) | |
| 160 | 174 | l.pendingSerial = 0 | |
| 175 | + | l.waitKind = waitBusy | |
| 161 | 176 | newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry | |
| 162 | 177 | if newRun { | |
| 163 | 178 | l.busySince = now | |
| @@ -176,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool { | |||
| 176 | 191 | // EBUSY there is no known serial, so any CRTC event ends that wait. The | |
| 177 | 192 | // returned start lets Output measure flip age after pending fences close. | |
| 178 | 193 | func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) { | |
| 179 | - | if !l.pending || l.pendingSerial == 0 && !l.nomemSince.IsZero() || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial { | |
| 194 | + | if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial { | |
| 180 | 195 | return pendingFrame{}, time.Time{}, false | |
| 181 | 196 | } | |
| 182 | 197 | f, start := l.pendingFrame, l.flipStart | |
| @@ -211,11 +226,12 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult | |||
| 211 | 226 | } | |
| 212 | 227 | return timeoutEarly | |
| 213 | 228 | } | |
| 229 | + | kind := l.waitKind | |
| 214 | 230 | l.endPending() | |
| 215 | - | if l.pendingSerial != 0 { | |
| 231 | + | if kind == waitFlip { | |
| 216 | 232 | return timeoutMissing | |
| 217 | 233 | } | |
| 218 | - | if !l.nomemSince.IsZero() { | |
| 234 | + | if kind == waitNoMem { | |
| 219 | 235 | return timeoutRetry | |
| 220 | 236 | } | |
| 221 | 237 | if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() { | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | // deadline ticks. Only Run uses the timer; the transitions take plain values. | 13 | // deadline ticks. Only Run uses the timer; the transitions take plain values. | ||
| 14 | type frameLifecycle struct { | 14 | type frameLifecycle struct { | ||
| 15 | pending bool | 15 | pending bool | ||
| 16 | + | waitKind frameWaitKind | |||
| 16 | pendingFrame pendingFrame | 17 | pendingFrame pendingFrame | ||
| 17 | serials *atomic.Uint64 // shared across outputs on the card | 18 | serials *atomic.Uint64 // shared across outputs on the card | ||
| 18 | pendingSerial, nextSerial uint64 | 19 | pendingSerial, nextSerial uint64 | ||
| @@ -27,6 +28,16 @@ type frameLifecycle struct { | |||||
| 27 | stuckTimer *time.Timer | 28 | stuckTimer *time.Timer | ||
| 28 | } | 29 | } | ||
| 29 | 30 | ||||
| 31 | + | // frameWaitKind is the current wait, independent of refusal-run summaries. | |||
| 32 | + | type frameWaitKind uint8 | |||
| 33 | + | ||||
| 34 | + | const ( | |||
| 35 | + | waitNone frameWaitKind = iota | |||
| 36 | + | waitFlip | |||
| 37 | + | waitBusy | |||
| 38 | + | waitNoMem | |||
| 39 | + | ) | |||
| 40 | + | ||||
| 30 | // pendingFrame is what the pending frame commit shows. | 41 | // pendingFrame is what the pending frame commit shows. | ||
| 31 | type pendingFrame struct { | 42 | type pendingFrame struct { | ||
| 32 | security ports.SecurityState // admission epoch checked again at KMS boundary | 43 | security ports.SecurityState // admission epoch checked again at KMS boundary | ||
| @@ -94,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 { | |||||
| 94 | func (l *frameLifecycle) endPending() { | 105 | func (l *frameLifecycle) endPending() { | ||
| 95 | l.pendingFrame.closeFences() | 106 | l.pendingFrame.closeFences() | ||
| 96 | l.pending, l.pendingFrame = false, pendingFrame{} | 107 | l.pending, l.pendingFrame = false, pendingFrame{} | ||
| 108 | + | l.waitKind = waitNone | |||
| 97 | } | 109 | } | ||
| 98 | 110 | ||||
| 99 | // begin records a successful nonblocking commit awaiting its event. | 111 | // begin records a successful nonblocking commit awaiting its event. | ||
| @@ -101,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) { | |||||
| 101 | l.pendingSerial = l.nextSerial | 113 | l.pendingSerial = l.nextSerial | ||
| 102 | l.pendingFrame.closeFences() | 114 | l.pendingFrame.closeFences() | ||
| 103 | l.pending, l.pendingFrame, l.flipStart = true, f, now | 115 | l.pending, l.pendingFrame, l.flipStart = true, f, now | ||
| 116 | + | l.waitKind = waitFlip | |||
| 104 | l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false | 117 | l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false | ||
| 105 | l.busySince = time.Time{} | 118 | l.busySince = time.Time{} | ||
| 106 | } | 119 | } | ||
| @@ -126,6 +139,7 @@ func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) { | |||||
| 126 | l.lastNomem = now | 139 | l.lastNomem = now | ||
| 127 | l.nomemAttempts++ | 140 | l.nomemAttempts++ | ||
| 128 | l.pending, l.pendingSerial, l.flipStart = true, 0, now | 141 | l.pending, l.pendingSerial, l.flipStart = true, 0, now | ||
| 142 | + | l.waitKind = waitNoMem | |||
| 129 | l.stuckAt = now.Add(l.nomemBackoff) | 143 | l.stuckAt = now.Add(l.nomemBackoff) | ||
| 130 | return l.nomemAttempts, newRun | 144 | return l.nomemAttempts, newRun | ||
| 131 | } | 145 | } | ||
| @@ -158,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool { | |||||
| 158 | l.endPending() | 172 | l.endPending() | ||
| 159 | l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry) | 173 | l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry) | ||
| 160 | l.pendingSerial = 0 | 174 | l.pendingSerial = 0 | ||
| 175 | + | l.waitKind = waitBusy | |||
| 161 | newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry | 176 | newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry | ||
| 162 | if newRun { | 177 | if newRun { | ||
| 163 | l.busySince = now | 178 | l.busySince = now | ||
| @@ -176,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool { | |||||
| 176 | // EBUSY there is no known serial, so any CRTC event ends that wait. The | 191 | // EBUSY there is no known serial, so any CRTC event ends that wait. The | ||
| 177 | // returned start lets Output measure flip age after pending fences close. | 192 | // returned start lets Output measure flip age after pending fences close. | ||
| 178 | func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) { | 193 | func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) { | ||
| 179 | - | if !l.pending || l.pendingSerial == 0 && !l.nomemSince.IsZero() || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial { | 194 | + | if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial { |
| 180 | return pendingFrame{}, time.Time{}, false | 195 | return pendingFrame{}, time.Time{}, false | ||
| 181 | } | 196 | } | ||
| 182 | f, start := l.pendingFrame, l.flipStart | 197 | f, start := l.pendingFrame, l.flipStart | ||
| @@ -211,11 +226,12 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult | |||||
| 211 | } | 226 | } | ||
| 212 | return timeoutEarly | 227 | return timeoutEarly | ||
| 213 | } | 228 | } | ||
| 229 | + | kind := l.waitKind | |||
| 214 | l.endPending() | 230 | l.endPending() | ||
| 215 | - | if l.pendingSerial != 0 { | 231 | + | if kind == waitFlip { |
| 216 | return timeoutMissing | 232 | return timeoutMissing | ||
| 217 | } | 233 | } | ||
| 218 | - | if !l.nomemSince.IsZero() { | 234 | + | if kind == waitNoMem { |
| 219 | return timeoutRetry | 235 | return timeoutRetry | ||
| 220 | } | 236 | } | ||
| 221 | if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() { | 237 | if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() { | ||
M internal/adapters/drm/frame_lifecycle_test.go +37 -0
| @@ -124,3 +124,40 @@ func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) { | |||
| 124 | 124 | t.Fatal("modeset did not clear run") | |
| 125 | 125 | } | |
| 126 | 126 | } | |
| 127 | + | ||
| 128 | + | func 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 | + | ||
| 144 | + | func 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 | + | } | |
| @@ -124,3 +124,40 @@ func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) { | |||||
| 124 | t.Fatal("modeset did not clear run") | 124 | t.Fatal("modeset did not clear run") | ||
| 125 | } | 125 | } | ||
| 126 | } | 126 | } | ||
| 127 | + | ||||
| 128 | + | func 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 | + | ||||
| 144 | + | func 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 | + | } | |||