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}