v0.3.0 cf370a0

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

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