v0.3.0 2df0761

alloc_test.go RawHistoryFolder

1package neferclient_test
2
3import (
4	"encoding/binary"
5	"runtime"
6	"testing"
7
8	"github.com/bnema/go-wayland-bindings/client/wayland"
9	"github.com/stretchr/testify/require"
10	"golang.org/x/sys/unix"
11
12	"github.com/bnema/neferclient"
13)
14
15// countingHandler is the measured system's input, not a double of a port: it
16// only counts, so the test sees the cost of the library alone.
17type countingHandler struct {
18	neferclient.NopHandler
19	fdReady, errs, frames int
20	keys, pointers        int
21	lastText              string // set only by the allocation-free comparison below
22}
23
24func (h *countingHandler) FDReady(uint64)              { h.fdReady++ }
25func (h *countingHandler) Error(error)                 { h.errs++ }
26func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
27
28func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
29
30// Key records the text through a comparison that does not allocate: the
31// string is only built the first time a different text shows up.
32func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
33	h.keys++
34	if string(ev.Text) != h.lastText && ev.Pressed {
35		h.lastText = string(ev.Text)
36	}
37}
38
39// TestAllocDispatch measures the whole steady-state pipeline over a real
40// socketpair: wire frames in, reader decode, queue, Dispatch (output state
41// updates, an output commit and a watched-fd notification with epoll re-arm). testing's
42// AllocsPerRun counts every goroutine, so reader and epoll allocations are
43// included.
44func TestAllocDispatch(t *testing.T) {
45	skipUnderRace(t)
46	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
47	obj := srv.waitBound(7)
48
49	const scaleEvents = 200
50	var batch []byte
51	for i := range scaleEvents {
52		batch = append(batch, frame(obj.id, 3, appendU32(nil, uint32(1+i%2)))...)
53	}
54	batch = append(batch, frame(obj.id, 2, nil)...) // wl_output.done: commitOutput runs
55	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
56	require.NoError(t, err)
57	defer unix.Close(efd)
58	require.NoError(t, c.WatchFD(efd, 1))
59	one := [8]byte{1}
60	var drain [8]byte
61
62	h := &countingHandler{}
63	step := func() {
64		if _, err := srv.conn.Write(batch); err != nil {
65			t.Fatal(err)
66		}
67		_, _ = unix.Write(efd, one[:])
68		for c.QueueLen() < scaleEvents+2 {
69			runtime.Gosched()
70		}
71		_, _ = unix.Read(efd, drain[:])
72		if err := c.Dispatch(h); err != nil {
73			t.Fatal(err)
74		}
75	}
76	step() // warm-up beyond AllocsPerRun's own: grow pools and buffers
77	step()
78	allocs := testing.AllocsPerRun(50, step)
79	t.Logf("allocs per Dispatch of %d events: %v", scaleEvents+2, allocs)
80	require.Zero(t, allocs)
81	require.Positive(t, h.fdReady)
82	require.Zero(t, h.errs)
83}
84
85// TestAllocPresent measures the steady-state present loop over a real
86// socketpair: Present (all requests, including the frame callback), the peer
87// answering the callback, reader decode, queue and Dispatch applying Frame.
88// The peer parses and discards everything else.
89func TestAllocPresent(t *testing.T) {
90	skipUnderRace(t)
91	globals := []wireGlobal{{1, wayland.CompositorInterface, 6}, {2, "zwp_linux_dmabuf_v1", 4},
92		{3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}}
93	c, srv := connectWire(t, globals, nil)
94	s, err := c.NewToplevel("alloc", 64, 48)
95	require.NoError(t, err)
96	s.MarkPresentable(64, 48)
97
98	surfaceID := s.SurfaceObjectID()
99	srv.mu.Lock()
100	srv.hook = func(obj uint32, op uint16, body []byte) {
101		if obj == surfaceID && op == 3 && len(body) == 4 { // wl_surface.frame(new_id)
102			srv.write(frame(binary.LittleEndian.Uint32(body), 0, appendU32(nil, 1)))
103			srv.write(frame(1, 1, body)) // wl_display.delete_id: the id is recycled
104		}
105	}
106	srv.mu.Unlock()
107
108	memfd, err := unix.MemfdCreate("alloc", unix.MFD_CLOEXEC)
109	require.NoError(t, err)
110	defer unix.Close(memfd)
111	require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 0x34325258,
112		Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}, PlaneCount: 1}))
113	require.NoError(t, s.ImportTimeline(1, memfd))
114	require.NoError(t, s.ImportTimeline(2, memfd))
115
116	var point uint64
117	damage := []neferclient.Rect{{X: 0, Y: 0, Width: 16, Height: 16}}
118	present := func() {
119		point++
120		if err := s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2,
121			AcquirePoint: point, ReleasePoint: point, Damage: damage, Opaque: true}); err != nil {
122			t.Fatal(err)
123		}
124	}
125
126	// The request path: every request Present sends, over the real socket,
127	// without the frame callback.
128	s.SkipFrameRequests()
129	requests := func() {
130		s.ResetFrame()
131		present()
132	}
133	for range 4 { // warm-up beyond AllocsPerRun's own
134		requests()
135	}
136	allocs := testing.AllocsPerRun(100, requests)
137	t.Logf("allocs per Present (requests only): %v", allocs)
138	require.Zero(t, allocs)
139
140	// The complete cycle adds the frame callback, registered under a recycled
141	// ID in wlturbo's object tables.
142	s.UseWireRequests()
143	h := &countingHandler{}
144	cycle := func() {
145		present()
146		for c.QueueLen() < 1 {
147			runtime.Gosched()
148		}
149		if err := c.Dispatch(h); err != nil {
150			t.Fatal(err)
151		}
152	}
153	s.ResetFrame()
154	for range 4 {
155		cycle()
156	}
157	full := testing.AllocsPerRun(100, cycle)
158	t.Logf("allocs per Present+frame round trip: %v", full)
159	require.Zero(t, full)
160	require.Positive(t, h.frames)
161	require.Zero(t, h.errs)
162}

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 1

Tags 4

↑↓ moveenter openesc close