feat/data-dnd e05cb15

commit_alloc_test.go RawHistoryFolder

1package drm
2
3import (
4	"runtime"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/stretchr/testify/mock"
9	"golang.org/x/sys/unix"
10)
11
12// mallocsPerRun is testing.AllocsPerRun without the integer truncation.
13func mallocsPerRun(runs int, f func()) float64 {
14	f()
15	var before, after runtime.MemStats
16	runtime.GC()
17	runtime.ReadMemStats(&before)
18	for range runs {
19		f()
20	}
21	runtime.ReadMemStats(&after)
22	return float64(after.Mallocs-before.Mallocs) / float64(runs)
23}
24
25func scanoutAllocsOutput(t *testing.T) (*Output, ports.SurfaceContent) {
26	o, _, _ := testOutput(t)
27	o.cursor.image = true
28	o.cursor.Move(12, 34)
29	o.clientFBs[9] = &clientFB{fbID: 80}
30	return o, ports.SurfaceContent{ID: 1, Width: 200, Height: 100, DMABuf: &ports.DMABuf{ID: 9}}
31}
32
33// A steady-state direct-scanout commit builds its request, and the flat
34// ioctl arrays, in buffers it reuses.
35//
36// The real kmsDevice on an invalid fd runs the whole request path up to the
37// failing ioctl: exactly zero allocations. testify's own bookkeeping per
38// mocked call allocates, so the success path (lifecycle, cursor, VRR) is
39// measured on the generated mockkms against the same mocked call made
40// directly: the commit may add nothing to it.
41func TestCommitFrameAllocations(t *testing.T) {
42	t.Run("request and ioctl arrays", func(t *testing.T) {
43		o, c := scanoutAllocsOutput(t)
44		o.k = kmsDevice{fd: -1}
45		rect := fullPlaneRect(200, 100)
46		commit := func() { _, _ = o.commitScanoutRect(80, c, pendingFrame{}, rect, colorBypass) }
47		commit() // grow the buffers
48		if len(o.frameReq.objs) != 3 || len(o.frameReq.counts) != 3 || len(o.frameReq.flatProps) == 0 {
49			t.Fatalf("frame request %+v", o.frameReq)
50		}
51		if n := testing.AllocsPerRun(100, commit); n != 0 {
52			t.Fatalf("direct scanout commit: %.1f allocs, want 0", n)
53		}
54	})
55	t.Run("success path", func(t *testing.T) {
56		if raceEnabled {
57			// The race runtime moves testify's count by a few
58			// allocations per call, as much as the margin below.
59			// perf-check runs it without; the subtest above stays exact.
60			t.Skip("testify allocates unevenly under the race detector")
61		}
62		o, c := scanoutAllocsOutput(t)
63		k := newMockkms(t)
64		k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).Return(nil)
65		o.k = k // no recorder: it copies every request
66		rect := fullPlaneRect(200, 100)
67		commit := func() {
68			if _, err := o.commitScanoutRect(80, c, pendingFrame{}, rect, colorBypass); err != nil {
69				t.Fatal(err)
70			}
71		}
72		commit()
73		// The mock boxes flags and userData (a fresh serial per commit):
74		// the reference call passes them as variables too.
75		serial, flags := uint64(1<<20), uint32(atomicNonblock|flipEventFlag)
76		ref := mallocsPerRun(1000, func() {
77			serial += 1 << userKindBits
78			mockCallOuter(k, &o.frameReq, flags, serial|userFrame)
79		})
80		got := mallocsPerRun(1000, commit)
81		t.Logf("commit %.2f allocs, the mocked call alone %.2f", got, ref)
82		// A heap profile puts the whole difference inside testify (its
83		// call-site lookup allocates per stack frame, and the stacks
84		// differ by a frame or so); the first subtest proves the commit
85		// itself allocates zero. A leaked request would add one per
86		// slice and grow far past this margin (before: 9 more).
87		if got-ref > 3 {
88			t.Fatalf("direct scanout commit: %.2f allocs, %.2f of them the mock's", got, ref)
89		}
90	})
91}
92
93// mockCallOuter and mockCallInner stand where commitScanoutRect and
94// commitWithRect stand: testify's call-site lookup allocates per stack
95// frame, so the reference call must be as deep as the real one.
96//
97//go:noinline
98func mockCallOuter(k *mockkms, req *atomicReq, flags uint32, user uint64) {
99	mockCallInner(k, req, flags, user)
100}
101
102//go:noinline
103func mockCallInner(k *mockkms, req *atomicReq, flags uint32, user uint64) {
104	_ = k.commit(req, flags, user)
105}
106
107// A retry resets the frame request and builds it again: the second commit
108// carries the retried frame, not the first one's leftovers, and the caller
109// never reads the request after the recursive call.
110func TestCommitFrameRetryRebuildsRequest(t *testing.T) {
111	o, _, commits := testOutput(t, unix.EINVAL)
112	o.shown, o.vrrOn = 1, true
113	o.clientFBs[9] = &clientFB{fbID: 80}
114	c := ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}, Async: true}
115	// The async attempt is refused, then retried synchronously with the
116	// full plane state in the same request object.
117	ok, err := o.commitScanout(80, c, pendingFrame{})
118	if !ok || err != nil || len(*commits) != 2 {
119		t.Fatalf("ok=%v err=%v commits=%d", ok, err, len(*commits))
120	}
121	first, second := (*commits)[0], (*commits)[1]
122	if len(first.req.objs) != 1 || first.req.objs[0] != tPrimary || len(first.req.props[0]) != 1 {
123		t.Fatalf("async request %+v", first.req)
124	}
125	if v, ok := second.req.value(tPrimary, pFB); !ok || v != 80 {
126		t.Fatalf("retry primary fb %d %v", v, ok)
127	}
128	if _, ok := second.req.value(tPrimary, 17); !ok {
129		t.Fatal("retry lost the plane geometry")
130	}
131	if _, ok := second.req.value(tCrtc, pVRR); !ok {
132		t.Fatal("retry lost VRR")
133	}
134	// The retry's request shows no stale entry twice.
135	for i, ps := range second.req.props {
136		seen := map[uint32]bool{}
137		for _, p := range ps {
138			if seen[p.prop] {
139				t.Fatalf("object %d: property %d twice", second.req.objs[i], p.prop)
140			}
141			seen[p.prop] = true
142		}
143	}
144}
145
146// The cursor-refused retry rebuilds the request with the cursor off while
147// the TEST_ONLY probe uses its own request.
148func TestCommitFrameCursorRetryKeepsProbeApart(t *testing.T) {
149	var frameSeen []*atomicReq
150	o, _, _ := testOutput(t)
151	o.cursor.image = true
152	o.cursor.Move(1, 2)
153	// First real commit refused with EINVAL, the cursor probe refused too,
154	// then the retry without the cursor passes.
155	k2 := newMockkms(t)
156	n := 0
157	k2.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, _ uint64) error {
158		n++
159		frameSeen = append(frameSeen, r)
160		if n <= 2 {
161			return unix.EINVAL
162		}
163		return nil
164	})
165	o.k = k2
166	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
167		t.Fatal(err)
168	}
169	if n != 3 || !o.cursor.off {
170		t.Fatalf("commits %d cursorOff %v", n, o.cursor.off)
171	}
172	if frameSeen[0] != &o.frameReq || frameSeen[1] == &o.frameReq || frameSeen[2] != &o.frameReq {
173		t.Fatal("probe must not reuse the frame request, retry must")
174	}
175	if _, ok := o.frameReq.value(tCursor, pFB); !ok {
176		t.Fatal("cursor plane not turned off in the retry")
177	}
178	if v, _ := o.frameReq.value(tCursor, pFB); v != 0 {
179		t.Fatalf("retry still shows the cursor: fb %d", v)
180	}
181}
182
183// kmsDevice.commit fills the request's own scratch: once grown it allocates
184// nothing. The bad fd makes the ioctl fail before reaching a kernel.
185func TestKMSDeviceCommitAllocations(t *testing.T) {
186	k := kmsDevice{fd: -1}
187	req := &atomicReq{}
188	build := func() {
189		req.reset()
190		for i := range 3 {
191			obj := uint32(10 + i)
192			for p := range 8 {
193				req.set(obj, uint32(p+1), uint64(p))
194			}
195		}
196	}
197	build()
198	if err := k.commit(req, atomicTestOnly, 0); err == nil {
199		t.Fatal("ioctl on fd -1 succeeded")
200	}
201	if len(req.counts) != 3 || len(req.flatProps) != 24 || len(req.flatVals) != 24 || req.counts[1] != 8 {
202		t.Fatalf("scratch counts %v props %d vals %d", req.counts, len(req.flatProps), len(req.flatVals))
203	}
204	if n := testing.AllocsPerRun(100, func() {
205		build()
206		_ = k.commit(req, atomicTestOnly, 0)
207	}); n != 0 {
208		t.Fatalf("build and commit: %.1f allocs", n)
209	}
210}
211
212func TestAtomicReqResetKeepsCapacityAndDropsContent(t *testing.T) {
213	r := &atomicReq{}
214	r.set(1, 1, 5)
215	r.set(1, 2, 6)
216	r.set(2, 1, 7)
217	r.reset()
218	if len(r.objs) != 0 || len(r.props) != 0 {
219		t.Fatalf("not empty: %+v", r)
220	}
221	r.set(2, 9, 1)
222	if len(r.objs) != 1 || len(r.props) != 1 || len(r.props[0]) != 1 || r.props[0][0] != (propValue{9, 1}) {
223		t.Fatalf("stale content after reset: %+v", r)
224	}
225	if _, ok := r.value(1, 1); ok {
226		t.Fatal("stale object")
227	}
228}
229
230// With a plane colour pipeline active, the steady-state frame decision and
231// commit allocate nothing: the colour state is compared, not rebuilt, and a
232// cached verdict spares the TEST_ONLY.
233func TestColorPipelineAllocations(t *testing.T) {
234	t.Run("scanout decision", func(t *testing.T) {
235		o, _, _ := colorOutput(t, false, nil)
236		o.cursor.image = true
237		o.cursor.Move(12, 34)
238		o.clientFBs[9] = &clientFB{fbID: 80}
239		s, surfaces := hdrScanoutScene()
240		d := o.decideFrame(s, surfaces, false) // first use: TEST_ONLY, then cached
241		if d.fb != 80 || d.color != colorSDRToPQ {
242			t.Fatalf("decision %+v reason %q", d, o.reason)
243		}
244		if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
245			t.Fatalf("decision with the pipeline: %.1f allocs, want 0", n)
246		}
247	})
248	t.Run("overlay decision", func(t *testing.T) {
249		o, k, _ := colorOutput(t, true, nil)
250		o.primary.pipeline = nil
251		k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(88), nil).Maybe()
252		s, surfaces := overlayScene()
253		if d := o.decideFrame(s, surfaces, false); d.overlay.fb != 88 || d.overlay.color.mode != colorSDRToPQ {
254			t.Fatalf("decision %+v reason %q", d, o.overlayReason)
255		}
256		if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
257			t.Fatalf("overlay decision with the pipeline: %.1f allocs, want 0", n)
258		}
259	})
260	// The real kmsDevice on an invalid fd runs the request path up to the
261	// failing ioctl (see TestCommitFrameAllocations).
262	for _, tc := range []struct {
263		name  string
264		known bool // the plane applies the pipeline already
265	}{{"commit writing the pipeline", false}, {"steady-state commit", true}} {
266		t.Run(tc.name, func(t *testing.T) {
267			o, _, _ := colorOutput(t, false, nil)
268			c := sdrContent(o)
269			o.cursor.image = true
270			o.cursor.Move(12, 34)
271			o.k = kmsDevice{fd: -1}
272			rect := fullPlaneRect(200, 100)
273			commit := func() {
274				o.primary.colorKnown, o.primary.colorApplied = tc.known, colorSDRToPQ
275				_, _ = o.commitScanoutRect(80, c, pendingFrame{}, rect, colorSDRToPQ)
276			}
277			commit() // grow the buffers
278			if _, ok := o.frameReq.value(tPrimary, pColorPipe); ok == tc.known {
279				t.Fatalf("COLOR_PIPELINE in the request: %v, known %v", ok, tc.known)
280			}
281			if n := testing.AllocsPerRun(100, commit); n != 0 {
282				t.Fatalf("%s: %.1f allocs, want 0", tc.name, n)
283			}
284		})
285	}
286}

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 25

Tags 10

↑↓ moveenter openesc close