detached 1a6c6c9

performance_test.go RawHistoryFolder

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}

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 1

↑↓ moveenter openesc close