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}