1//go:build linux
2
3package client_test
4
5import (
6 "encoding/binary"
7 "net"
8 "os"
9 "testing"
10 "time"
11
12 "github.com/bnema/go-wayland-bindings/client/cursorshape"
13 "github.com/bnema/go-wayland-bindings/client/fractionalscale"
14 "github.com/bnema/go-wayland-bindings/client/viewporter"
15 "github.com/bnema/go-wayland-bindings/client/wayland"
16 "github.com/bnema/wlturbo"
17 "golang.org/x/sys/unix"
18)
19
20// Allocation profile of generated bindings, measured against the current
21// transport (see the regression tests below for what is pinned):
22//
23// - Event dispatch of numeric arguments (uint/int/fixed) is allocation-free.
24// - Zero-argument requests are allocation-free.
25// - Generated requests pass typed wl.Arg values to Context.RequestArgs, so
26// numeric requests are allocation-free for every value. (The legacy
27// Context.Request boxed each argument into an interface: one heap
28// allocation per 32-bit value >= 256, e.g. set_source 4, set_destination
29// 2, set_shape 1 before this path existed.)
30// - Allocations that are inherent to the operation are kept separate below:
31// new_id child creation (proxy object + map entries), received strings
32// (Event.String copies), and received file descriptors (OwnedFD).
33
34// loopConn is an in-memory net.Conn. Reads replay a fixed byte pattern made of
35// whole frames (never splitting one), and writes are discarded, so a benchmark
36// measures generated-binding and transport cost without socket scheduling.
37type loopConn struct{ pattern []byte }
38
39func (c *loopConn) Read(p []byte) (int, error) {
40 n := len(p) / len(c.pattern) * len(c.pattern)
41 if n == 0 {
42 return 0, net.ErrClosed
43 }
44 for off := 0; off < n; off += len(c.pattern) {
45 copy(p[off:], c.pattern)
46 }
47 return n, nil
48}
49func (c *loopConn) Write(p []byte) (int, error) { return len(p), nil }
50func (c *loopConn) Close() error { return nil }
51func (c *loopConn) LocalAddr() net.Addr { return loopAddr{} }
52func (c *loopConn) RemoteAddr() net.Addr { return loopAddr{} }
53func (c *loopConn) SetDeadline(time.Time) error { return nil }
54func (c *loopConn) SetReadDeadline(time.Time) error { return nil }
55func (c *loopConn) SetWriteDeadline(time.Time) error { return nil }
56
57type loopAddr struct{}
58
59func (loopAddr) Network() string { return "loop" }
60func (loopAddr) String() string { return "loop" }
61
62// loopEnv is a display over loopConn with one registered object of each kind
63// used by the tests. Tests replay an event by assigning its frame to c.pattern.
64type loopEnv struct {
65 d *wlturbo.Display
66 c *loopConn
67 surface *wayland.Surface
68 ptr *wayland.Pointer
69 output *wayland.Output
70 vp *viewporter.WpViewport
71 shape *cursorshape.WpCursorShapeDevice
72 scale *fractionalscale.WpFractionalScale
73 vpr *viewporter.WpViewporter
74}
75
76func newLoopEnv(tb testing.TB) *loopEnv {
77 tb.Helper()
78 c := &loopConn{pattern: make([]byte, 12)}
79 d, err := wlturbo.ConnectFromConn(c)
80 if err != nil {
81 tb.Fatal(err)
82 }
83 tb.Cleanup(func() { d.Close() })
84 e := &loopEnv{d: d, c: c}
85 reg := func(p wlturbo.Proxy) {
86 p.SetID(d.AllocateID())
87 d.Context().Register(p)
88 }
89 e.surface = wayland.NewSurface(d.Context())
90 e.ptr = wayland.NewPointer(d.Context())
91 e.output = wayland.NewOutput(d.Context())
92 e.vp = viewporter.NewWpViewport(d.Context())
93 e.shape = cursorshape.NewWpCursorShapeDevice(d.Context())
94 e.scale = fractionalscale.NewWpFractionalScale(d.Context())
95 e.vpr = viewporter.NewWpViewporter(d.Context())
96 for _, p := range []wlturbo.Proxy{e.surface, e.ptr, e.output, e.vp, e.shape, e.scale, e.vpr} {
97 reg(p)
98 }
99 return e
100}
101
102// perfFrame encodes one event with 32-bit words as its whole body.
103func perfFrame(id uint32, op uint16, words ...uint32) []byte {
104 b := make([]byte, 8+4*len(words))
105 binary.LittleEndian.PutUint32(b, id)
106 binary.LittleEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
107 for i, v := range words {
108 binary.LittleEndian.PutUint32(b[8+i*4:], v)
109 }
110 return b
111}
112
113// perfStringFrame encodes an event whose only argument is a string.
114func perfStringFrame(id uint32, op uint16, s string) []byte {
115 n := len(s) + 1
116 padded := (n + 3) &^ 3
117 b := make([]byte, 8+4+padded)
118 binary.LittleEndian.PutUint32(b, id)
119 binary.LittleEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
120 binary.LittleEndian.PutUint32(b[8:], uint32(n))
121 copy(b[12:], s)
122 return b
123}
124
125// numericRequests are numeric requests with values that do NOT hit the
126// runtime's small-integer boxing table: this is the realistic cost.
127func (e *loopEnv) numericRequests() []struct {
128 name string
129 args int // number of numeric arguments
130 run func() error
131} {
132 x, w := wlturbo.NewFixed(10.5), wlturbo.NewFixed(1920)
133 return []struct {
134 name string
135 args int
136 run func() error
137 }{
138 {"wp_viewport.set_source", 4, func() error { return e.vp.SetSource(x, x, w, w) }},
139 {"wp_viewport.set_destination", 2, func() error { return e.vp.SetDestination(1920, 1080) }},
140 {"wp_cursor_shape_device_v1.set_shape", 2, func() error { return e.shape.SetShape(123456, cursorshape.SHAPE_POINTER) }},
141 {"wl_surface.damage_buffer", 4, func() error { return e.surface.DamageBuffer(0, 0, 1920, 1080) }},
142 {"wl_surface.set_buffer_scale", 1, func() error { return e.surface.SetBufferScale(1000) }},
143 }
144}
145
146// TestRequestAllocations pins the request-side allocation contract.
147func TestRequestAllocations(t *testing.T) {
148 e := newLoopEnv(t)
149 run := func(name string, f func() error) float64 {
150 var err error
151 n := testing.AllocsPerRun(500, func() { err = f() })
152 if err != nil {
153 t.Fatalf("%s: %v", name, err)
154 }
155 return n
156 }
157 // Requests without arguments, and numeric requests whose values are
158 // below 256 (the runtime's static boxing table), are allocation-free.
159 // Both must stay so, whatever the generator does with arguments.
160 for _, tc := range []struct {
161 name string
162 f func() error
163 }{
164 {"wl_surface.commit", e.surface.Commit},
165 {"wl_surface.set_buffer_scale(2)", func() error { return e.surface.SetBufferScale(2) }},
166 {"wp_viewport.set_destination(64,32)", func() error { return e.vp.SetDestination(64, 32) }},
167 {"wp_cursor_shape_device_v1.set_shape(small)", func() error { return e.shape.SetShape(7, cursorshape.SHAPE_DEFAULT) }},
168 } {
169 if n := run(tc.name, tc.f); n != 0 {
170 t.Errorf("%s: %v allocs/op, want 0", tc.name, n)
171 }
172 }
173 // Realistic values (outside the runtime's small-integer boxing table)
174 // travel as typed wl.Arg values and must not allocate either.
175 for _, tc := range e.numericRequests() {
176 if n := run(tc.name, tc.run); n != 0 {
177 t.Errorf("%s: %v allocs/op, want 0 (%d numeric args)", tc.name, n, tc.args)
178 }
179 }
180}
181
182// TestNumericEventDispatchAllocationFree pins the event side: framing,
183// signature validation, pooled Event and typed handler dispatch for numeric
184// arguments must not allocate.
185func TestNumericEventDispatchAllocationFree(t *testing.T) {
186 // One environment per case: loopConn refills a whole receive chunk, so
187 // frames of a previous pattern would still be buffered.
188 for _, tc := range []struct {
189 name string
190 setup func(e *loopEnv) (frame []byte, observed func() bool)
191 }{
192 {"wp_fractional_scale_v1.preferred_scale", func(e *loopEnv) ([]byte, func() bool) {
193 var scale uint32
194 e.scale.OnPreferredScale(func(s uint32) { scale = s })
195 return perfFrame(e.scale.ID(), 0, 180), func() bool { return scale == 180 }
196 }},
197 {"wl_pointer.motion", func(e *loopEnv) ([]byte, func() bool) {
198 var tm uint32
199 var fx, fy wlturbo.Fixed
200 e.ptr.OnMotion(func(t uint32, x, y wlturbo.Fixed) { tm, fx, fy = t, x, y })
201 return perfFrame(e.ptr.ID(), 2, 1234567, 300*256, 0xffffff00), func() bool { return tm == 1234567 && fx == 300*256 && fy == -256 }
202 }},
203 {"wl_surface.preferred_buffer_scale", func(e *loopEnv) ([]byte, func() bool) {
204 var factor int32
205 e.surface.OnPreferredBufferScale(func(f int32) { factor = f })
206 return perfFrame(e.surface.ID(), 2, 3), func() bool { return factor == 3 }
207 }},
208 } {
209 t.Run(tc.name, func(t *testing.T) {
210 e := newLoopEnv(t)
211 f, observed := tc.setup(e)
212 e.c.pattern = f
213 var derr error
214 n := testing.AllocsPerRun(1000, func() {
215 if err := e.d.Dispatch(); err != nil {
216 derr = err
217 }
218 })
219 if derr != nil {
220 t.Fatal(derr)
221 }
222 if !observed() {
223 t.Fatal("handler did not observe the event")
224 }
225 if n != 0 {
226 t.Errorf("%v allocs/op, want 0", n)
227 }
228 })
229 }
230}
231
232// TestNecessaryAllocations documents allocations that are inherent to the
233// operation. They are bounded so they cannot silently grow, but they are not
234// expected to reach zero.
235func TestNecessaryAllocations(t *testing.T) {
236 e := newLoopEnv(t)
237
238 // A received string is copied out of the receive buffer: one allocation.
239 var name string
240 e.output.OnName(func(s string) { name = s })
241 e.c.pattern = perfStringFrame(e.output.ID(), 4, "HDMI-A-1")
242 n := testing.AllocsPerRun(1000, func() { _ = e.d.Dispatch() })
243 if name != "HDMI-A-1" {
244 t.Fatalf("string event not delivered: %q", name)
245 }
246 if n > 1 {
247 t.Errorf("wl_output.name: %v allocs/op, want <= 1 (the string copy)", n)
248 }
249
250 // A new_id request creates a proxy and registers it in two maps; that is
251 // the object-creation cost, reported rather than asserted to be zero.
252 // The IDs are never released in this loopback, so keep the run short.
253 n = testing.AllocsPerRun(50, func() {
254 if _, err := e.vpr.GetViewport(e.surface); err != nil {
255 t.Fatal(err)
256 }
257 })
258 t.Logf("wp_viewporter.get_viewport (object creation): %v allocs/op", n)
259 if n < 1 {
260 t.Errorf("get_viewport allocated %v; a child proxy must be created", n)
261 }
262}
263
264// BenchmarkRequests measures generated numeric requests with small and
265// realistic values; both are allocation-free.
266func BenchmarkRequests(b *testing.B) {
267 e := newLoopEnv(b)
268 b.Run("no_args/wl_surface.commit", func(b *testing.B) {
269 b.ReportAllocs()
270 for i := 0; i < b.N; i++ {
271 if err := e.surface.Commit(); err != nil {
272 b.Fatal(err)
273 }
274 }
275 })
276 b.Run("small_values/wl_surface.set_buffer_scale", func(b *testing.B) {
277 b.ReportAllocs()
278 for i := 0; i < b.N; i++ {
279 if err := e.surface.SetBufferScale(2); err != nil {
280 b.Fatal(err)
281 }
282 }
283 })
284 for _, tc := range e.numericRequests() {
285 b.Run("realistic_values/"+tc.name, func(b *testing.B) {
286 b.ReportAllocs()
287 for i := 0; i < b.N; i++ {
288 if err := tc.run(); err != nil {
289 b.Fatal(err)
290 }
291 }
292 })
293 }
294}
295
296// BenchmarkEventDispatch measures typed numeric event dispatch through the
297// generated Dispatch methods, without socket scheduling.
298func BenchmarkEventDispatch(b *testing.B) {
299 e := newLoopEnv(b)
300 var sink uint32
301 e.scale.OnPreferredScale(func(s uint32) { sink += s })
302 e.ptr.OnMotion(func(tm uint32, x, y wlturbo.Fixed) { sink += tm + uint32(x) + uint32(y) })
303 for _, tc := range []struct {
304 name string
305 frame []byte
306 }{
307 {"wp_fractional_scale_v1.preferred_scale", perfFrame(e.scale.ID(), 0, 180)},
308 {"wl_pointer.motion", perfFrame(e.ptr.ID(), 2, 1234567, 300*256, 400*256)},
309 } {
310 b.Run("numeric/"+tc.name, func(b *testing.B) {
311 e.c.pattern = tc.frame
312 b.ReportAllocs()
313 for i := 0; i < b.N; i++ {
314 if err := e.d.Dispatch(); err != nil {
315 b.Fatal(err)
316 }
317 }
318 })
319 }
320 _ = sink
321}
322
323// BenchmarkNecessaryAllocations reports operations whose allocations cannot be
324// removed by the generator: string copies, object creation and received FDs.
325func BenchmarkNecessaryAllocations(b *testing.B) {
326 b.Run("string_event/wl_output.name", func(b *testing.B) {
327 e := newLoopEnv(b)
328 var name string
329 e.output.OnName(func(s string) { name = s })
330 e.c.pattern = perfStringFrame(e.output.ID(), 4, "HDMI-A-1")
331 b.ReportAllocs()
332 for i := 0; i < b.N; i++ {
333 if err := e.d.Dispatch(); err != nil {
334 b.Fatal(err)
335 }
336 }
337 _ = name
338 })
339 b.Run("new_id/wp_viewporter.get_viewport", func(b *testing.B) {
340 e := newLoopEnv(b)
341 b.ReportAllocs()
342 for i := 0; i < b.N; i++ {
343 if _, err := e.vpr.GetViewport(e.surface); err != nil {
344 b.Fatal(err)
345 }
346 }
347 })
348 b.Run("received_fd/wl_keyboard.keymap", func(b *testing.B) {
349 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
350 if err != nil {
351 b.Fatal(err)
352 }
353 fa, fb := os.NewFile(uintptr(fds[0]), "a"), os.NewFile(uintptr(fds[1]), "b")
354 ca, err := net.FileConn(fa)
355 fa.Close()
356 if err != nil {
357 b.Fatal(err)
358 }
359 cb, err := net.FileConn(fb)
360 fb.Close()
361 if err != nil {
362 b.Fatal(err)
363 }
364 defer cb.Close()
365 peer := cb.(*net.UnixConn)
366 d, err := wlturbo.ConnectFromConn(ca)
367 if err != nil {
368 b.Fatal(err)
369 }
370 defer d.Close()
371 kb := wayland.NewKeyboard(d.Context())
372 kb.SetID(d.AllocateID())
373 d.Context().Register(kb)
374 kb.OnKeymap(func(format uint32, fd *wlturbo.OwnedFD, size uint32) {})
375 // The descriptor sent each iteration; the receiver closes its copy.
376 null, err := os.Open(os.DevNull)
377 if err != nil {
378 b.Fatal(err)
379 }
380 defer null.Close()
381 msg := perfFrame(kb.ID(), 0, 1, 4096)
382 rights := unix.UnixRights(int(null.Fd()))
383 b.ReportAllocs()
384 for i := 0; i < b.N; i++ {
385 if _, _, err := peer.WriteMsgUnix(msg, rights, nil); err != nil {
386 b.Fatal(err)
387 }
388 if err := d.Dispatch(); err != nil {
389 b.Fatal(err)
390 }
391 }
392 })
393}