v0.4.0 5f1ad00

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	configures, popupDone int
22	lastText              string // set only by the allocation-free comparison below
23}
24
25func (h *countingHandler) FDReady(uint64)              { h.fdReady++ }
26func (h *countingHandler) Error(error)                 { h.errs++ }
27func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
28
29func (h *countingHandler) Configure(neferclient.SurfaceID, int32, int32) { h.configures++ }
30func (h *countingHandler) PopupDone(neferclient.SurfaceID)               { h.popupDone++ }
31
32func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
33
34// Key records the text through a comparison that does not allocate: the
35// string is only built the first time a different text shows up.
36func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
37	h.keys++
38	if string(ev.Text) != h.lastText && ev.Pressed {
39		h.lastText = string(ev.Text)
40	}
41}
42
43// TestAllocDispatch measures the whole steady-state pipeline over a real
44// socketpair: wire frames in, reader decode, queue, Dispatch (output state
45// updates, an output commit and a watched-fd notification with epoll re-arm). testing's
46// AllocsPerRun counts every goroutine, so reader and epoll allocations are
47// included.
48func TestAllocDispatch(t *testing.T) {
49	skipUnderRace(t)
50	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
51	obj := srv.waitBound(7)
52
53	const scaleEvents = 200
54	var batch []byte
55	for i := range scaleEvents {
56		batch = append(batch, frame(obj.id, 3, appendU32(nil, uint32(1+i%2)))...)
57	}
58	batch = append(batch, frame(obj.id, 2, nil)...) // wl_output.done: commitOutput runs
59	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
60	require.NoError(t, err)
61	defer unix.Close(efd)
62	require.NoError(t, c.WatchFD(efd, 1))
63	one := [8]byte{1}
64	var drain [8]byte
65
66	h := &countingHandler{}
67	step := func() {
68		if _, err := srv.conn.Write(batch); err != nil {
69			t.Fatal(err)
70		}
71		_, _ = unix.Write(efd, one[:])
72		for c.QueueLen() < scaleEvents+2 {
73			runtime.Gosched()
74		}
75		_, _ = unix.Read(efd, drain[:])
76		if err := c.Dispatch(h); err != nil {
77			t.Fatal(err)
78		}
79	}
80	step() // warm-up beyond AllocsPerRun's own: grow pools and buffers
81	step()
82	allocs := testing.AllocsPerRun(50, step)
83	t.Logf("allocs per Dispatch of %d events: %v", scaleEvents+2, allocs)
84	require.Zero(t, allocs)
85	require.Positive(t, h.fdReady)
86	require.Zero(t, h.errs)
87}
88
89// TestAllocPresent measures the steady-state present loop over a real
90// socketpair: Present (all requests, including the frame callback), the peer
91// answering the callback, reader decode, queue and Dispatch applying Frame.
92// The peer parses and discards everything else.
93func TestAllocPresent(t *testing.T) {
94	skipUnderRace(t)
95	globals := []wireGlobal{{1, wayland.CompositorInterface, 6}, {2, "zwp_linux_dmabuf_v1", 4},
96		{3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}}
97	c, srv := connectWire(t, globals, nil)
98	s, err := c.NewToplevel("alloc", 64, 48)
99	require.NoError(t, err)
100	s.MarkPresentable(64, 48)
101
102	surfaceID := s.SurfaceObjectID()
103	srv.mu.Lock()
104	srv.hook = func(obj uint32, op uint16, body []byte) {
105		if obj == surfaceID && op == 3 && len(body) == 4 { // wl_surface.frame(new_id)
106			srv.write(frame(binary.LittleEndian.Uint32(body), 0, appendU32(nil, 1)))
107			srv.write(frame(1, 1, body)) // wl_display.delete_id: the id is recycled
108		}
109	}
110	srv.mu.Unlock()
111
112	memfd, err := unix.MemfdCreate("alloc", unix.MFD_CLOEXEC)
113	require.NoError(t, err)
114	defer unix.Close(memfd)
115	require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 0x34325258,
116		Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}, PlaneCount: 1}))
117	require.NoError(t, s.ImportTimeline(1, memfd))
118	require.NoError(t, s.ImportTimeline(2, memfd))
119
120	var point uint64
121	damage := []neferclient.Rect{{X: 0, Y: 0, Width: 16, Height: 16}}
122	present := func() {
123		point++
124		if err := s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2,
125			AcquirePoint: point, ReleasePoint: point, Damage: damage, Opaque: true}); err != nil {
126			t.Fatal(err)
127		}
128	}
129
130	// The request path: every request Present sends, over the real socket,
131	// without the frame callback.
132	s.SkipFrameRequests()
133	requests := func() {
134		s.ResetFrame()
135		present()
136	}
137	for range 4 { // warm-up beyond AllocsPerRun's own
138		requests()
139	}
140	allocs := testing.AllocsPerRun(100, requests)
141	t.Logf("allocs per Present (requests only): %v", allocs)
142	require.Zero(t, allocs)
143
144	// The complete cycle adds the frame callback, registered under a recycled
145	// ID in wlturbo's object tables.
146	s.UseWireRequests()
147	h := &countingHandler{}
148	cycle := func() {
149		present()
150		for c.QueueLen() < 1 {
151			runtime.Gosched()
152		}
153		if err := c.Dispatch(h); err != nil {
154			t.Fatal(err)
155		}
156	}
157	s.ResetFrame()
158	for range 4 {
159		cycle()
160	}
161	full := testing.AllocsPerRun(100, cycle)
162	t.Logf("allocs per Present+frame round trip: %v", full)
163	require.Zero(t, full)
164	require.Positive(t, h.frames)
165	require.Zero(t, h.errs)
166}
167
168// TestAllocPopupEvents measures the popup events in steady state: the
169// xdg_popup configure (with its xdg_surface configure and the ack request it
170// triggers) and popup_done.
171func TestAllocPopupEvents(t *testing.T) {
172	skipUnderRace(t)
173	e := newPopupEnv(t, popupGlobals)
174	p, err := e.c.NewPopup(neferclient.PopupConfig{Parent: e.layer, Place: place, Grab: true, Serial: 3})
175	require.NoError(t, err)
176	require.NoError(t, e.c.Roundtrip())
177	xdg := e.xdgSurfaceOf(t, p.SurfaceObjectID())
178	_, gp := e.getPopup(t, xdg)
179	popup := gp.u32(0)
180	e.srv.mu.Lock()
181	e.srv.hook = func(uint32, uint16, []byte) {} // parse and discard, as the real peer's requests would be
182	e.srv.mu.Unlock()
183
184	const rounds = 40
185	var batch []byte
186	for i := range rounds {
187		batch = append(batch, frame(popup, 0, appendU32(appendU32(appendU32(appendU32(nil, 0), 0), 60), 40))...)
188		batch = append(batch, frame(xdg, 0, appendU32(nil, uint32(i+1)))...)
189		batch = append(batch, frame(popup, 1, nil)...) // popup_done
190	}
191	h := &countingHandler{}
192	step := func() {
193		if _, err := e.srv.conn.Write(batch); err != nil {
194			t.Fatal(err)
195		}
196		for e.c.QueueLen() < 3*rounds {
197			runtime.Gosched()
198		}
199		if err := e.c.Dispatch(h); err != nil {
200			t.Fatal(err)
201		}
202	}
203	step()
204	step()
205	before := h.popupDone
206	allocs := testing.AllocsPerRun(20, step)
207	t.Logf("allocs per Dispatch of %d popup events: %v", 3*rounds, allocs)
208	require.Zero(t, allocs)
209	require.Greater(t, h.popupDone, before)
210	require.Positive(t, h.configures)
211	require.Zero(t, h.errs)
212}

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