main f2d166b

fencegate_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"os"
7	"path/filepath"
8	"testing"
9	"time"
10
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/stretchr/testify/mock"
13	"golang.org/x/sys/unix"
14)
15
16// pipeFence returns a pipe standing in for a sync file: its read end is
17// readable (signalled) once the write end got a byte. Both ends close with
18// the test.
19func pipeFence(t *testing.T) (fence, signal *os.File) {
20	t.Helper()
21	r, w, err := os.Pipe()
22	if err != nil {
23		t.Fatal(err)
24	}
25	t.Cleanup(func() { r.Close(); w.Close() })
26	return r, w
27}
28
29func TestParseVersion(t *testing.T) {
30	for _, tc := range []struct {
31		in   string
32		want [3]int
33		ok   bool
34	}{
35		{"560.35.03\n", [3]int{560, 35, 3}, true},
36		{"615.78.08", [3]int{615, 78, 8}, true},
37		{"550.120", [3]int{550, 120, 0}, true},
38		{"570", [3]int{570, 0, 0}, true},
39		{"", [3]int{}, false},
40		{"abc", [3]int{}, false},
41		{"560.x.3", [3]int{}, false},
42		{"1.2.3.4", [3]int{}, false},
43		{"-1.2", [3]int{}, false},
44		{"+560.35.3", [3]int{}, false},
45		{"560.+35.3", [3]int{}, false},
46		{"560.-5.3", [3]int{}, false},
47		{"560..3", [3]int{}, false},
48		{"99999999999.1.1", [3]int{}, false},
49	} {
50		got, err := parseVersion(tc.in)
51		if (err == nil) != tc.ok || tc.ok && got != tc.want {
52			t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
53		}
54	}
55}
56
57// The gate opens for nvidia-drm before 560.35.3 or of unknown version, and
58// for no other driver.
59func TestBrokenInFence(t *testing.T) {
60	for _, tc := range []struct {
61		name, driver, version string
62		err                   error
63		want                  bool
64	}{
65		{"amd", "amdgpu", "", nil, false},
66		{"intel", "i915", "garbage", nil, false},
67		{"old nvidia", driverNVIDIA, "555.58.02", nil, true},
68		{"one patch before", driverNVIDIA, "560.35.2", nil, true},
69		{"minor before", driverNVIDIA, "560.34.99", nil, true},
70		{"fixed", driverNVIDIA, "560.35.03\n", nil, false},
71		{"patch after", driverNVIDIA, "560.35.5", nil, false},
72		{"minor after", driverNVIDIA, "560.36.0", nil, false},
73		{"major after", driverNVIDIA, "561.0.0", nil, false},
74		{"newer", driverNVIDIA, "615.78.08", nil, false},
75		{"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
76		{"unparsable", driverNVIDIA, "n/a", nil, true},
77	} {
78		if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
79			t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
80		}
81	}
82}
83
84// The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node
85// answers it too: skips without one.
86func TestDriverName(t *testing.T) {
87	nodes, _ := filepath.Glob("/dev/dri/renderD*")
88	if len(nodes) == 0 {
89		t.Skip("no /dev/dri/renderD* node")
90	}
91	for _, n := range nodes {
92		f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
93		if err != nil {
94			continue
95		}
96		defer f.Close()
97		name, err := driverName(int(f.Fd()))
98		if err != nil || name == "" {
99			t.Fatalf("driverName(%s) = %q, %v", n, name, err)
100		}
101		t.Logf("%s: %s", n, name)
102		return
103	}
104	t.Skip("no render node can be opened")
105}
106
107// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
108func TestFenceProp(t *testing.T) {
109	f, _ := pipeFence(t)
110	for _, cpu := range []bool{false, true} {
111		o, _, _ := testOutput(t)
112		o.cpuFences = cpu
113		req := &atomicReq{}
114		o.fenceProp(req, o.primary, f)
115		o.fenceProp(req, o.primary, nil)
116		v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
117		if ok == cpu || ok && v != uint64(f.Fd()) {
118			t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
119		}
120	}
121}
122
123// waitFences returns for a fence that signalled, for one that never will
124// once the output stops, and at once where fences go to the kernel.
125func TestWaitFences(t *testing.T) {
126	returns := func(t *testing.T, o *Output, fences ...*os.File) {
127		t.Helper()
128		done := make(chan struct{})
129		go func() { o.waitFences(fences...); close(done) }()
130		select {
131		case <-done:
132		case <-time.After(30 * time.Second): // failure guard only
133			t.Fatal("waitFences did not return")
134		}
135	}
136	t.Run("signalled", func(t *testing.T) {
137		o, _, _ := testOutput(t)
138		o.cpuFences = true
139		f, w := pipeFence(t)
140		_, _ = w.Write([]byte{1})
141		returns(t, o, nil, f)
142	})
143	t.Run("never signalled, output stopped", func(t *testing.T) {
144		o, _, _ := testOutput(t)
145		o.cpuFences = true
146		ctx, cancel := context.WithCancel(t.Context())
147		cancel()
148		o.runContext = ctx
149		f, _ := pipeFence(t)
150		returns(t, o, f)
151		if o.flipStats.fenceTimeouts != 0 {
152			t.Fatal("a stopped output counted a fence timeout")
153		}
154	})
155	t.Run("kernel waits", func(t *testing.T) {
156		o, _, _ := testOutput(t)
157		f, _ := pipeFence(t)
158		returns(t, o, f)
159	})
160}
161
162// A fence wait that hit its limit counts in the stats, and warns at most
163// once per fenceWarnEvery.
164func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) {
165	o, _, _ := testOutput(t)
166	o.cpuFences = true
167	o.fenceLimit = time.Millisecond
168	f, _ := pipeFence(t)
169	o.waitFences(f)
170	if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() {
171		t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt)
172	}
173	first := o.fenceWarnAt
174	o.waitFences(f)
175	if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) {
176		t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt)
177	}
178}
179
180// Without CPU fence waits every fence is ready; with them only a signalled
181// client fence is, and one that cannot be polled is not.
182func TestClientFenceReady(t *testing.T) {
183	f, w := pipeFence(t)
184	o, _, _ := testOutput(t)
185	if !o.clientFenceReady(f) || !o.clientFenceReady(nil) {
186		t.Fatal("fence not ready without CPU waits")
187	}
188	o.cpuFences = true
189	if !o.clientFenceReady(nil) {
190		t.Fatal("no fence must be ready")
191	}
192	if o.clientFenceReady(f) {
193		t.Fatal("pending client fence ready")
194	}
195	_, _ = w.Write([]byte{1})
196	if !o.clientFenceReady(f) {
197		t.Fatal("signalled client fence not ready")
198	}
199	closed, cw := pipeFence(t)
200	closed.Close()
201	cw.Close()
202	if o.clientFenceReady(closed) {
203		t.Fatal("closed fence ready")
204	}
205}
206
207// Polling a fence allocates nothing.
208func TestClientFenceReadyAllocatesNothing(t *testing.T) {
209	f, _ := pipeFence(t)
210	o, _, _ := testOutput(t)
211	o.cpuFences = true
212	o.clientFenceReady(f) // builds the poll closure
213	if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 {
214		t.Fatalf("%v allocations per poll", n)
215	}
216}
217
218// With CPU waits a client buffer whose fence is pending is composed, on the
219// primary plane and on the overlay alike: neither plane waits for it. Once
220// the fence signalled the same frame uses the plane.
221func TestPendingClientFenceComposes(t *testing.T) {
222	for _, tc := range []struct {
223		name   string
224		reason func(o *Output) string
225		// decide returns whether the plane took the buffer.
226		decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool
227	}{
228		{"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
229			o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
230			s, c := fullscreenScene()
231			content := c[1]
232			content.Acquire = fence
233			fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content})
234			return fb != 0
235		}},
236		{"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
237			o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
238			s, c := overlayScene()
239			content := c[2]
240			content.Acquire = fence
241			ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content})
242			ov.close()
243			return ov.fb != 0
244		}},
245	} {
246		t.Run(tc.name, func(t *testing.T) {
247			o, k, _ := overlayOutput(t)
248			k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
249			o.cpuFences = true
250			fence, w := pipeFence(t)
251			if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" {
252				t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o))
253			}
254			_, _ = w.Write([]byte{1})
255			if !tc.decide(t, o, k, fence) {
256				t.Fatalf("signalled fence: reason %q", tc.reason(o))
257			}
258		})
259	}
260}
261
262// A commit that is retried (here: VRR refused, then the same frame without
263// it) waits for its fence once, not once per attempt.
264func TestCommitWaitsForItsFenceOnce(t *testing.T) {
265	o, _, commits := testOutput(t, unix.EINVAL)
266	o.cpuFences = true
267	o.fenceLimit = time.Millisecond
268	fence, _ := pipeFence(t)
269	if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
270		t.Fatal(err)
271	}
272	if len(*commits) < 2 {
273		t.Fatalf("%d commits: no retry happened", len(*commits))
274	}
275	if o.flipStats.fenceTimeouts != 1 {
276		t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits))
277	}
278	for i, c := range *commits {
279		if _, ok := c.req.value(tPrimary, pFence); ok {
280			t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i)
281		}
282	}
283}

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 26

Tags 10

↑↓ moveenter openesc close