v0.8.0 446d1e3

stuck_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"os"
6	"sync"
7	"sync/atomic"
8	"testing"
9	"testing/synctest"
10	"time"
11
12	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/stretchr/testify/mock"
15	"golang.org/x/sys/unix"
16)
17
18// runStuck runs an output whose commits return errs (after the start
19// modeset test and modeset) and whose CRTC never sends an event. It
20// returns the recorded commits once n were made, failing after 2s.
21func runStuck(t *testing.T, n int, errs ...error) []commitRec {
22	t.Helper()
23	h := startStuck(t, nil, errs...)
24	h.scenes <- ports.Scene{Seq: 1}
25	waitFor(t, func() bool { return h.count() >= n })
26	return h.stop()
27}
28
29// stuckRun is an output run by startStuck.
30type stuckRun struct {
31	t         *testing.T
32	scenes    chan ports.Scene
33	contents  chan ports.SurfaceContent
34	active    chan bool
35	presented chan ports.OutputPresented
36	count     func() int
37	stop      func() []commitRec
38}
39
40// startStuck runs an output whose commits return errs (after the start
41// modeset test and modeset) and whose CRTC never sends an event. render
42// makes each frame's fence once the output started (nil: none).
43func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
44	t.Helper()
45	o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)
46	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
47	o.frame.stuckAfter = 200 * time.Millisecond
48	o.flipped = make(chan flipEvent) // no event ever
49	r := portsmocks.NewMockRenderer(t)
50	buf := func() ports.DMABuf {
51		f, w, _ := os.Pipe()
52		w.Close()
53		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
54	}
55	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
56	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
57	r.EXPECT().UseTarget(mock.Anything).Return()
58	var started atomic.Bool
59	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
60		if render == nil || !started.Load() {
61			return nil, nil
62		}
63		return render(), nil
64	})
65	r.EXPECT().Close().Return().Once()
66	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
67	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
68	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
69	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
70	h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)}
71	ctx, cancel := context.WithCancel(context.Background())
72	done := make(chan error, 1)
73	go func() {
74		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented, nil, nil)
75	}()
76	h.count = func() int {
77		commitMu.Lock()
78		defer commitMu.Unlock()
79		return len(*commits)
80	}
81	h.stop = func() []commitRec {
82		cancel()
83		if err := <-done; err != nil {
84			t.Fatal(err)
85		}
86		commitMu.Lock()
87		defer commitMu.Unlock()
88		return append([]commitRec(nil), (*commits)...)
89	}
90	waitFor(t, func() bool { return h.count() == 2 })
91	started.Store(true)
92	return h
93}
94
95// A commit whose event never comes must not stop the output: it is
96// abandoned with a modeset, then the scene is committed again.
97func TestMissingEventRecoversWithModeset(t *testing.T) {
98	c := runStuck(t, 5)
99	if c[2].user&3 != userFrame {
100		t.Fatalf("commit 2 is not a frame: %+v", c[2])
101	}
102	if c[3].flags&atomicAllowModes == 0 {
103		t.Fatalf("no recovery modeset after the missing event: flags %#x", c[3].flags)
104	}
105	if c[4].user&3 != userFrame || c[4].user>>userKindBits <= c[2].user>>userKindBits {
106		t.Fatalf("scene not committed again after the modeset: %+v", c[4])
107	}
108}
109
110// EBUSY with no commit of ours in flight gets no event: the commit is
111// retried without one.
112func TestBusyWithoutEventRetries(t *testing.T) {
113	c := runStuck(t, 4, unix.EBUSY)
114	if c[2].user&3 != userFrame || c[3].user&3 != userFrame || c[3].flags&atomicAllowModes != 0 {
115		t.Fatalf("busy frame not retried: %+v", c[2:])
116	}
117}
118
119// EBUSY that lasts past the limit ends in a modeset, and a modeset
120// refused as busy is retried too.
121func TestLastingBusyModesets(t *testing.T) {
122	errs := make([]error, 8)
123	for i := range errs {
124		errs[i] = unix.EBUSY
125	}
126	c := runStuck(t, 2+len(errs)+1, errs...)
127	found := false
128	for _, r := range c[2:] {
129		if r.flags&atomicAllowModes != 0 {
130			found = true
131		}
132	}
133	if !found {
134		t.Fatal("no modeset while EBUSY lasted")
135	}
136}
137
138// fencePipe is a fence that signals when its write end is written.
139func fencePipe(t *testing.T) (fence, signal *os.File) {
140	t.Helper()
141	r, w, err := os.Pipe()
142	if err != nil {
143		t.Fatal(err)
144	}
145	t.Cleanup(func() { r.Close(); w.Close() })
146	return r, w
147}
148
149// A frame whose GPU fence has not signalled is slow, not lost: it keeps
150// waiting past the limit; once the fence signalled and still no event
151// came, the output modesets.
152func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
153	synctest.Test(t, unsignalledFenceDelaysRecovery)
154}
155
156func unsignalledFenceDelaysRecovery(t *testing.T) {
157	var mu sync.Mutex
158	var signals []*os.File
159	h := startStuck(t, func() *os.File {
160		f, w := fencePipe(t)
161		mu.Lock()
162		signals = append(signals, w)
163		mu.Unlock()
164		return f
165	})
166	h.scenes <- ports.Scene{Seq: 1}
167	waitFor(t, func() bool { return h.count() == 3 })
168	time.Sleep(3 * 200 * time.Millisecond)
169	synctest.Wait()
170	if n := h.count(); n != 3 {
171		t.Fatalf("%d commits while the frame's fence is unsignalled", n)
172	}
173	mu.Lock()
174	_, _ = signals[0].Write([]byte{1})
175	mu.Unlock()
176	waitFor(t, func() bool { return h.count() >= 4 })
177	if c := h.stop(); c[3].flags&atomicAllowModes == 0 {
178		t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags)
179	}
180}
181
182// A frame refused with EBUSY may still be read by the GPU: what was seen
183// is reported only once its fence signalled.
184func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
185	var mu sync.Mutex
186	var signals []*os.File
187	h := startStuck(t, func() *os.File {
188		f, w := fencePipe(t)
189		mu.Lock()
190		signals = append(signals, w)
191		mu.Unlock()
192		return f
193	}, unix.EBUSY, unix.EACCES)
194	h.scenes <- ports.Scene{Seq: 1}
195	// EBUSY, then the retry loses DRM master: nothing is pending.
196	waitFor(t, func() bool { return h.count() == 4 })
197	h.contents <- ports.SurfaceContent{ID: 1, Seq: 7}
198	select {
199	case p := <-h.presented:
200		t.Fatalf("seen reported while refused frames may be read: %+v", p)
201	case <-time.After(50 * time.Millisecond):
202	}
203	mu.Lock()
204	for _, w := range signals {
205		_, _ = w.Write([]byte{1})
206	}
207	mu.Unlock()
208	select {
209	case p := <-h.presented:
210		if p.Seen[1] != 7 {
211			t.Fatalf("report %+v", p)
212		}
213	case <-time.After(2 * time.Second):
214		t.Fatal("seen not reported once the fences signalled")
215	}
216	h.stop()
217}
218
219// An EBUSY run ends when commits stop being refused: a later EBUSY is
220// retried, not taken as the same run lasting past the limit.
221func TestLaterBusyStartsNewRun(t *testing.T) { synctest.Test(t, laterBusyStartsNewRun) }
222
223func laterBusyStartsNewRun(t *testing.T) {
224	o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)
225	o.frame.stuckAfter = 100 * time.Millisecond
226	enabled := true
227	o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
228	time.Sleep(busyRetry)
229	if o.expire() {
230		t.Fatal("first EBUSY retry modesets")
231	}
232	time.Sleep(2 * o.frame.stuckAfter)
233	o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
234	time.Sleep(busyRetry)
235	if o.expire() {
236		t.Fatal("a new EBUSY taken as the old run: modeset")
237	}
238}
239
240// A modeset that fails keeps what is on screen: buffers still shown are
241// not released under the plane.
242func TestFailedModesetKeepsShown(t *testing.T) {
243	o, k, _ := testOutput(t, unix.EACCES)
244	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
245	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
246	o.shown, o.queued = 5, 6
247	if err := o.modeset(); err == nil {
248		t.Fatal("modeset succeeded")
249	}
250	if o.shown != 5 || o.queued != 6 {
251		t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued)
252	}
253}

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