detached 5c1298d

#28 perf(wayland): wait for explicit-sync acquire points on the display loop

state
merged · f2d166b · merge commit
branch
perf/frame-fixed-cost → main
opened
bnema ·
updated

Explicit-sync (linux-drm-syncobj) acquire waits now run on the Wayland display loop instead of a helper goroutine.

Before, a commit whose acquire point was not yet signalled (Chromium/Electron) cost a pipe write to syncWaiter, its poll wakeup, a Display.Post back into the OS-locked display goroutine and a useless cancel write. The acquire eventfd is now a Display.AddFD source: the callback runs on the display goroutine and applies the commit in the same turn. The waiter goroutine stays for implicit dma-buf fences.

Measurements

Headless backend, 2560x1440, Electron with 30 animated GIFs, 5 s execution trace:

main branch
Goroutine wakeups 1408 930 (−34 %)
syncWaiter wakeups 451 0
Compositor CPU ~1.15 % ~1.05 %

Checks

  • make check, make race, make mocks-check, staticcheck ./... pass.
  • New tests use Mockery mocks of the acquireSource seam: fire, cancel (Remove before Close), add failure, spurious callback, shutdown release, and an end-to-end commit applied inline without a posted pace tick (fails if pacing goes back to wakePacer).
  • Not tested yet on a live DRM session.
CommitSubjectAuthorDateSignatureStatus
5c1298dtest(wayland): assert explicit acquire callbacks apply commits inlinebriceGPG Verified
d8d545eperf(wayland): wait for explicit-sync acquire points on the display loopbriceGPG Verified

Checks 1

8 files · +480 -24
1/1
M .mockery.yml +10 -0
@@ -87,6 +87,16 @@ packages:
8787 dir: internal/adapters/wayland
8888 pkgname: wayland
8989 filename: acquire_poller_mock_test.go
90+ acquireSource:
91+ config:
92+ dir: internal/adapters/wayland
93+ pkgname: wayland
94+ filename: acquire_source_mock_test.go
95+ acquireRemover:
96+ config:
97+ dir: internal/adapters/wayland
98+ pkgname: wayland
99+ filename: acquire_remover_mock_test.go
90100 syncobjDevice:
91101 config:
92102 dir: internal/adapters/wayland
@@ -87,6 +87,16 @@ packages:
87 dir: internal/adapters/wayland87 dir: internal/adapters/wayland
88 pkgname: wayland88 pkgname: wayland
89 filename: acquire_poller_mock_test.go89 filename: acquire_poller_mock_test.go
90+ acquireSource:
91+ config:
92+ dir: internal/adapters/wayland
93+ pkgname: wayland
94+ filename: acquire_source_mock_test.go
95+ acquireRemover:
96+ config:
97+ dir: internal/adapters/wayland
98+ pkgname: wayland
99+ filename: acquire_remover_mock_test.go
90 syncobjDevice:100 syncobjDevice:
91 config:101 config:
92 dir: internal/adapters/wayland102 dir: internal/adapters/wayland
M internal/adapters/wayland/acquire_failure_test.go +0 -4
@@ -197,10 +197,6 @@ func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) {
197197 if !sw.stopped() {
198198 t.Fatal("waiter not terminal")
199199 }
200- dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure
201- if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed {
202- t.Fatal("new explicit watch accepted")
203- }
204200 var waits [4]*syncWait
205201 if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil {
206202 t.Fatal("new implicit watch accepted")
@@ -197,10 +197,6 @@ func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) {
197 if !sw.stopped() {197 if !sw.stopped() {
198 t.Fatal("waiter not terminal")198 t.Fatal("waiter not terminal")
199 }199 }
200- dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure
201- if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed {
202- t.Fatal("new explicit watch accepted")
203- }
204 var waits [4]*syncWait200 var waits [4]*syncWait
205 if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil {201 if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil {
206 t.Fatal("new implicit watch accepted")202 t.Fatal("new implicit watch accepted")
A internal/adapters/wayland/acquire_remover_mock_test.go +78 -0
@@ -0,0 +1,78 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockacquireRemover creates a new instance of mockacquireRemover. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockacquireRemover(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockacquireRemover {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockacquireRemover{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockacquireRemover is an autogenerated mock type for the acquireRemover type
35+type mockacquireRemover struct {
36+ mock.Mock
37+}
38+
39+type mockacquireRemover_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockacquireRemover) EXPECT() *mockacquireRemover_Expecter {
44+ return &mockacquireRemover_Expecter{mock: &_m.Mock}
45+}
46+
47+// Remove provides a mock function for the type mockacquireRemover
48+func (_mock *mockacquireRemover) Remove() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockacquireRemover_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
54+type mockacquireRemover_Remove_Call struct {
55+ *mock.Call
56+}
57+
58+// Remove is a helper method to define mock.On call
59+func (_e *mockacquireRemover_Expecter) Remove() *mockacquireRemover_Remove_Call {
60+ return &mockacquireRemover_Remove_Call{Call: _e.mock.On("Remove")}
61+}
62+
63+func (_c *mockacquireRemover_Remove_Call) Run(run func()) *mockacquireRemover_Remove_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockacquireRemover_Remove_Call) Return() *mockacquireRemover_Remove_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockacquireRemover_Remove_Call) RunAndReturn(run func()) *mockacquireRemover_Remove_Call {
76+ _c.Run(run)
77+ return _c
78+}
@@ -0,0 +1,78 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockacquireRemover creates a new instance of mockacquireRemover. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockacquireRemover(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockacquireRemover {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockacquireRemover{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockacquireRemover is an autogenerated mock type for the acquireRemover type
35+type mockacquireRemover struct {
36+ mock.Mock
37+}
38+
39+type mockacquireRemover_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockacquireRemover) EXPECT() *mockacquireRemover_Expecter {
44+ return &mockacquireRemover_Expecter{mock: &_m.Mock}
45+}
46+
47+// Remove provides a mock function for the type mockacquireRemover
48+func (_mock *mockacquireRemover) Remove() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockacquireRemover_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
54+type mockacquireRemover_Remove_Call struct {
55+ *mock.Call
56+}
57+
58+// Remove is a helper method to define mock.On call
59+func (_e *mockacquireRemover_Expecter) Remove() *mockacquireRemover_Remove_Call {
60+ return &mockacquireRemover_Remove_Call{Call: _e.mock.On("Remove")}
61+}
62+
63+func (_c *mockacquireRemover_Remove_Call) Run(run func()) *mockacquireRemover_Remove_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockacquireRemover_Remove_Call) Return() *mockacquireRemover_Remove_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockacquireRemover_Remove_Call) RunAndReturn(run func()) *mockacquireRemover_Remove_Call {
76+ _c.Run(run)
77+ return _c
78+}
A internal/adapters/wayland/acquire_source_mock_test.go +113 -0
@@ -0,0 +1,113 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockacquireSource creates a new instance of mockacquireSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockacquireSource(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockacquireSource {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockacquireSource{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockacquireSource is an autogenerated mock type for the acquireSource type
35+type mockacquireSource struct {
36+ mock.Mock
37+}
38+
39+type mockacquireSource_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockacquireSource) EXPECT() *mockacquireSource_Expecter {
44+ return &mockacquireSource_Expecter{mock: &_m.Mock}
45+}
46+
47+// add provides a mock function for the type mockacquireSource
48+func (_mock *mockacquireSource) add(fd int, onReadable func()) (acquireRemover, error) {
49+ ret := _mock.Called(fd, onReadable)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for add")
53+ }
54+
55+ var r0 acquireRemover
56+ var r1 error
57+ if returnFunc, ok := ret.Get(0).(func(int, func()) (acquireRemover, error)); ok {
58+ return returnFunc(fd, onReadable)
59+ }
60+ if returnFunc, ok := ret.Get(0).(func(int, func()) acquireRemover); ok {
61+ r0 = returnFunc(fd, onReadable)
62+ } else {
63+ if ret.Get(0) != nil {
64+ r0 = ret.Get(0).(acquireRemover)
65+ }
66+ }
67+ if returnFunc, ok := ret.Get(1).(func(int, func()) error); ok {
68+ r1 = returnFunc(fd, onReadable)
69+ } else {
70+ r1 = ret.Error(1)
71+ }
72+ return r0, r1
73+}
74+
75+// mockacquireSource_add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'add'
76+type mockacquireSource_add_Call struct {
77+ *mock.Call
78+}
79+
80+// add is a helper method to define mock.On call
81+// - fd int
82+// - onReadable func()
83+func (_e *mockacquireSource_Expecter) add(fd any, onReadable any) *mockacquireSource_add_Call {
84+ return &mockacquireSource_add_Call{Call: _e.mock.On("add", fd, onReadable)}
85+}
86+
87+func (_c *mockacquireSource_add_Call) Run(run func(fd int, onReadable func())) *mockacquireSource_add_Call {
88+ _c.Call.Run(func(args mock.Arguments) {
89+ var arg0 int
90+ if args[0] != nil {
91+ arg0 = args[0].(int)
92+ }
93+ var arg1 func()
94+ if args[1] != nil {
95+ arg1 = args[1].(func())
96+ }
97+ run(
98+ arg0,
99+ arg1,
100+ )
101+ })
102+ return _c
103+}
104+
105+func (_c *mockacquireSource_add_Call) Return(acquireRemoverMoqParam acquireRemover, err error) *mockacquireSource_add_Call {
106+ _c.Call.Return(acquireRemoverMoqParam, err)
107+ return _c
108+}
109+
110+func (_c *mockacquireSource_add_Call) RunAndReturn(run func(fd int, onReadable func()) (acquireRemover, error)) *mockacquireSource_add_Call {
111+ _c.Call.Return(run)
112+ return _c
113+}
@@ -0,0 +1,113 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockacquireSource creates a new instance of mockacquireSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockacquireSource(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockacquireSource {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockacquireSource{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockacquireSource is an autogenerated mock type for the acquireSource type
35+type mockacquireSource struct {
36+ mock.Mock
37+}
38+
39+type mockacquireSource_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockacquireSource) EXPECT() *mockacquireSource_Expecter {
44+ return &mockacquireSource_Expecter{mock: &_m.Mock}
45+}
46+
47+// add provides a mock function for the type mockacquireSource
48+func (_mock *mockacquireSource) add(fd int, onReadable func()) (acquireRemover, error) {
49+ ret := _mock.Called(fd, onReadable)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for add")
53+ }
54+
55+ var r0 acquireRemover
56+ var r1 error
57+ if returnFunc, ok := ret.Get(0).(func(int, func()) (acquireRemover, error)); ok {
58+ return returnFunc(fd, onReadable)
59+ }
60+ if returnFunc, ok := ret.Get(0).(func(int, func()) acquireRemover); ok {
61+ r0 = returnFunc(fd, onReadable)
62+ } else {
63+ if ret.Get(0) != nil {
64+ r0 = ret.Get(0).(acquireRemover)
65+ }
66+ }
67+ if returnFunc, ok := ret.Get(1).(func(int, func()) error); ok {
68+ r1 = returnFunc(fd, onReadable)
69+ } else {
70+ r1 = ret.Error(1)
71+ }
72+ return r0, r1
73+}
74+
75+// mockacquireSource_add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'add'
76+type mockacquireSource_add_Call struct {
77+ *mock.Call
78+}
79+
80+// add is a helper method to define mock.On call
81+// - fd int
82+// - onReadable func()
83+func (_e *mockacquireSource_Expecter) add(fd any, onReadable any) *mockacquireSource_add_Call {
84+ return &mockacquireSource_add_Call{Call: _e.mock.On("add", fd, onReadable)}
85+}
86+
87+func (_c *mockacquireSource_add_Call) Run(run func(fd int, onReadable func())) *mockacquireSource_add_Call {
88+ _c.Call.Run(func(args mock.Arguments) {
89+ var arg0 int
90+ if args[0] != nil {
91+ arg0 = args[0].(int)
92+ }
93+ var arg1 func()
94+ if args[1] != nil {
95+ arg1 = args[1].(func())
96+ }
97+ run(
98+ arg0,
99+ arg1,
100+ )
101+ })
102+ return _c
103+}
104+
105+func (_c *mockacquireSource_add_Call) Return(acquireRemoverMoqParam acquireRemover, err error) *mockacquireSource_add_Call {
106+ _c.Call.Return(acquireRemoverMoqParam, err)
107+ return _c
108+}
109+
110+func (_c *mockacquireSource_add_Call) RunAndReturn(run func(fd int, onReadable func()) (acquireRemover, error)) *mockacquireSource_add_Call {
111+ _c.Call.Return(run)
112+ return _c
113+}
M internal/adapters/wayland/server.go +2 -0
@@ -459,6 +459,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
459459 s.tokens = map[string]activationToken{}
460460 s.foreignExports = map[string]*foreignExport{}
461461 s.syncWait.log = log
462+ s.syncWait.src, s.syncWait.pace = displayAcquireSource{d}, s.acquireSignalled
462463 s.cleanup = func() {
463464 if s.paceReady {
464465 unix.Close(s.paceFD)
@@ -585,6 +586,7 @@ func (s *Server) Run(ctx context.Context) error {
585586 }
586587 s.castInventoryChanged()
587588 err := s.display.Run(ctx)
589+ s.syncWait.releasePending()
588590 s.stopSecurityContexts()
589591 wg.Wait()
590592 return err
@@ -459,6 +459,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
459 s.tokens = map[string]activationToken{}459 s.tokens = map[string]activationToken{}
460 s.foreignExports = map[string]*foreignExport{}460 s.foreignExports = map[string]*foreignExport{}
461 s.syncWait.log = log461 s.syncWait.log = log
462+ s.syncWait.src, s.syncWait.pace = displayAcquireSource{d}, s.acquireSignalled
462 s.cleanup = func() {463 s.cleanup = func() {
463 if s.paceReady {464 if s.paceReady {
464 unix.Close(s.paceFD)465 unix.Close(s.paceFD)
@@ -585,6 +586,7 @@ func (s *Server) Run(ctx context.Context) error {
585 }586 }
586 s.castInventoryChanged()587 s.castInventoryChanged()
587 err := s.display.Run(ctx)588 err := s.display.Run(ctx)
589+ s.syncWait.releasePending()
588 s.stopSecurityContexts()590 s.stopSecurityContexts()
589 wg.Wait()591 wg.Wait()
590 return err592 return err
M internal/adapters/wayland/session_lock_surface_test.go +2 -2
@@ -470,8 +470,8 @@ func TestLockSurfaceDestroyRetiresFencedChildDependencies(t *testing.T) {
470470 if destroy == "surface" {
471471 remaining, refs = 0, 1 // parent destruction detaches the child
472472 }
473- if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.cancelled.Load() || tl.uses != 0 {
474- t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d cancelled=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.cancelled.Load())
473+ if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.done || wait.efd >= 0 || tl.uses != 0 {
474+ t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d done=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.done)
475475 }
476476 dep.applyGraph()
477477 parent.applyGraph()
@@ -470,8 +470,8 @@ func TestLockSurfaceDestroyRetiresFencedChildDependencies(t *testing.T) {
470 if destroy == "surface" {470 if destroy == "surface" {
471 remaining, refs = 0, 1 // parent destruction detaches the child471 remaining, refs = 0, 1 // parent destruction detaches the child
472 }472 }
473- if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.cancelled.Load() || tl.uses != 0 {473+ if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.done || wait.efd >= 0 || tl.uses != 0 {
474- t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d cancelled=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.cancelled.Load())474+ t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d done=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.done)
475 }475 }
476 dep.applyGraph()476 dep.applyGraph()
477 parent.applyGraph()477 parent.applyGraph()
M internal/adapters/wayland/syncobj.go +114 -18
@@ -191,9 +191,6 @@ func (s *surface) pendingNotDMABuf() bool {
191191 return !dma
192192 }
193193
194-// syncWaiter watches acquire points: each gets an eventfd the kernel
195-// writes once the point is signalled, and the pacer is woken to apply the
196-// commits that waited for it.
197194 type acquirePoller interface {
198195 poll([]unix.PollFd, int) (int, error)
199196 }
@@ -204,7 +201,38 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) {
204201 return p.waiter.Poll(fds, timeout)
205202 }
206203
204+// acquireSource registers an acquire eventfd with the display loop: onReadable
205+// runs on the display goroutine while fd is readable. Display goroutine only.
206+type acquireSource interface {
207+ add(fd int, onReadable func()) (acquireRemover, error)
208+}
209+
210+// acquireRemover unregisters a source added by acquireSource.
211+type acquireRemover interface {
212+ Remove()
213+}
214+
215+type displayAcquireSource struct{ d *server.Display }
216+
217+func (s displayAcquireSource) add(fd int, onReadable func()) (acquireRemover, error) {
218+ src, err := s.d.AddFD(fd, onReadable)
219+ if err != nil {
220+ return nil, err
221+ }
222+ return src, nil
223+}
224+
225+// syncWaiter watches acquire points: each gets an eventfd the kernel
226+// writes once the point is signalled, and the pacer is woken to apply the
227+// commits that waited for it. Explicit waits are display-loop sources; the
228+// goroutine (run) with its pipe polls implicit dma-buf fences only.
207229 type syncWaiter struct {
230+ // src and pace serve explicit waits, which run on the display loop:
231+ // src registers the eventfd there, pace applies what the fired wait
232+ // unblocked (display goroutine only).
233+ src acquireSource
234+ pace func()
235+ pending map[*syncWait]struct{} // explicit waits still registered (display goroutine, then Run's exit)
208236 poller acquirePoller
209237 waiter *fdwait.Poller
210238 terminal bool // polling ended in failure; protected by mu
@@ -218,9 +246,12 @@ type syncWaiter struct {
218246 pollError sync.Once
219247 }
220248
221-// syncWait is one acquire point a queued commit waits for.
249+// syncWait is one acquire point a queued commit waits for. An explicit wait
250+// lives on the display loop only; an implicit one is polled by run.
222251 type syncWait struct {
223252 efd int
253+ src acquireRemover // explicit: the display-loop registration, nil once removed
254+ explicit bool
224255 done bool // the eventfd fired or the wait was cancelled (display goroutine)
225256 failed bool // observation failed; done must not authorize publication
226257 cancelled atomic.Bool // run stops polling it and closes efd
@@ -240,30 +271,65 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
240271 return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil
241272 }
242273
243-// watch registers an eventfd for p; the wait is ready once it fires.
274+// watch registers an eventfd for p on the display loop; the wait is ready once
275+// it fires. Display goroutine only.
244276 func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
245- if sw.stopped() {
246- return &syncWait{efd: -1, done: true, failed: true}, unix.EIO
247- }
248277 ready, err := p.tl.dev.signalled(p.tl.handle, p.point)
249278 if err != nil || ready {
250- return &syncWait{efd: -1, done: true, failed: err != nil}, err
279+ return &syncWait{efd: -1, explicit: true, done: true, failed: err != nil}, err
251280 }
252281 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
253282 if err != nil {
254- return &syncWait{efd: -1, done: true, failed: true}, err
283+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
255284 }
256285 if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {
257286 unix.Close(efd)
258- return &syncWait{efd: -1, done: true, failed: true}, err
287+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
259288 }
260- w := sw.add(efd)
261- if w.failed {
262- return w, unix.EIO
289+ w := &syncWait{efd: efd, explicit: true}
290+ src, err := sw.src.add(efd, func() { sw.onAcquire(w) })
291+ if err != nil {
292+ unix.Close(efd)
293+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
294+ }
295+ w.src = src
296+ if sw.pending == nil {
297+ sw.pending = map[*syncWait]struct{}{}
263298 }
299+ sw.pending[w] = struct{}{}
264300 return w, nil
265301 }
266302
303+// onAcquire is the eventfd's callback, on the display goroutine.
304+func (sw *syncWaiter) onAcquire(w *syncWait) {
305+ if w.done {
306+ return
307+ }
308+ var b [8]byte
309+ if _, err := unix.Read(w.efd, b[:]); err != nil {
310+ if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
311+ return // not signalled yet: keep waiting
312+ }
313+ w.failed = true
314+ }
315+ sw.finish(w)
316+ sw.pace()
317+}
318+
319+// finish unregisters then closes an explicit wait's eventfd and marks it done.
320+func (sw *syncWaiter) finish(w *syncWait) {
321+ delete(sw.pending, w)
322+ if w.src != nil {
323+ w.src.Remove()
324+ w.src = nil
325+ }
326+ if w.efd >= 0 {
327+ unix.Close(w.efd)
328+ w.efd = -1
329+ }
330+ w.done = true
331+}
332+
267333 // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch.
268334 func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error {
269335 if sw.stopped() {
@@ -336,6 +402,9 @@ func (sw *syncWaiter) add(fd int) *syncWait {
336402
337403 // fired reports whether w's point is signalled, and forgets w once it has.
338404 func (sw *syncWaiter) fired(w *syncWait) bool {
405+ if w.explicit {
406+ return w.done // display goroutine only
407+ }
339408 sw.mu.Lock()
340409 defer sw.mu.Unlock()
341410 if ready, ok := sw.ready[w]; ok {
@@ -347,7 +416,14 @@ func (sw *syncWaiter) fired(w *syncWait) bool {
347416
348417 // cancel stops watching w (the commit was dropped).
349418 func (sw *syncWaiter) cancel(w *syncWait) {
350- if w == nil || w.efd < 0 {
419+ if w == nil {
420+ return
421+ }
422+ if w.explicit {
423+ sw.finish(w)
424+ return
425+ }
426+ if w.efd < 0 {
351427 return
352428 }
353429 w.cancelled.Store(true)
@@ -355,7 +431,7 @@ func (sw *syncWaiter) cancel(w *syncWait) {
355431 delete(sw.ready, w)
356432 sw.mu.Unlock()
357433 w.done = true
358- _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd
434+ _, _ = sw.pipeW.Write([]byte{0}) // run closes its fence fd
359435 }
360436
361437 func (sw *syncWaiter) stopped() bool {
@@ -380,12 +456,12 @@ func (sw *syncWaiter) fail(waits []*syncWait) {
380456 sw.wake()
381457 }
382458
383-// run polls every watched eventfd; it owns and closes them.
459+// run polls the implicit dma-buf fence descriptors; it owns and closes them.
384460 func (sw *syncWaiter) run(ctx context.Context) {
385461 defer sw.pipeR.Close()
386462 defer sw.waiter.Close()
387463 pipeFD := int(sw.pipeR.Fd())
388- // The pipe lives as long as the poller; the eventfds come and go. The
464+ // The pipe lives as long as the poller; the fence fds come and go. The
389465 // poll sleeps until a fence, a new or cancelled wait (the pipe), or the
390466 // end of ctx.
391467 var waits []*syncWait
@@ -593,3 +669,23 @@ func (s *surface) applySync(cs *commitSync) {
593669 s.server.dropTimeline(cs.acquire.tl)
594670 s.hold = syncHold{acquire: f, release: cs.release}
595671 }
672+
673+// acquireSignalled applies what a fired acquire point unblocked: like a
674+// commit, it ticks inline instead of posting a tick to the display loop.
675+func (s *Server) acquireSignalled() {
676+ s.paceDepth++
677+ s.wakePacer()
678+ s.paceDepth--
679+ if s.paceDepth == 0 && s.paceDirty {
680+ s.paceDirty = false
681+ s.paceNow()
682+ }
683+}
684+
685+// releasePending unregisters and closes the explicit waits still pending at
686+// shutdown. Called once the display loop has stopped.
687+func (sw *syncWaiter) releasePending() {
688+ for w := range sw.pending {
689+ sw.finish(w)
690+ }
691+}
@@ -191,9 +191,6 @@ func (s *surface) pendingNotDMABuf() bool {
191 return !dma191 return !dma
192 }192 }
193 193
194-// syncWaiter watches acquire points: each gets an eventfd the kernel
195-// writes once the point is signalled, and the pacer is woken to apply the
196-// commits that waited for it.
197 type acquirePoller interface {194 type acquirePoller interface {
198 poll([]unix.PollFd, int) (int, error)195 poll([]unix.PollFd, int) (int, error)
199 }196 }
@@ -204,7 +201,38 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) {
204 return p.waiter.Poll(fds, timeout)201 return p.waiter.Poll(fds, timeout)
205 }202 }
206 203
204+// acquireSource registers an acquire eventfd with the display loop: onReadable
205+// runs on the display goroutine while fd is readable. Display goroutine only.
206+type acquireSource interface {
207+ add(fd int, onReadable func()) (acquireRemover, error)
208+}
209+
210+// acquireRemover unregisters a source added by acquireSource.
211+type acquireRemover interface {
212+ Remove()
213+}
214+
215+type displayAcquireSource struct{ d *server.Display }
216+
217+func (s displayAcquireSource) add(fd int, onReadable func()) (acquireRemover, error) {
218+ src, err := s.d.AddFD(fd, onReadable)
219+ if err != nil {
220+ return nil, err
221+ }
222+ return src, nil
223+}
224+
225+// syncWaiter watches acquire points: each gets an eventfd the kernel
226+// writes once the point is signalled, and the pacer is woken to apply the
227+// commits that waited for it. Explicit waits are display-loop sources; the
228+// goroutine (run) with its pipe polls implicit dma-buf fences only.
207 type syncWaiter struct {229 type syncWaiter struct {
230+ // src and pace serve explicit waits, which run on the display loop:
231+ // src registers the eventfd there, pace applies what the fired wait
232+ // unblocked (display goroutine only).
233+ src acquireSource
234+ pace func()
235+ pending map[*syncWait]struct{} // explicit waits still registered (display goroutine, then Run's exit)
208 poller acquirePoller236 poller acquirePoller
209 waiter *fdwait.Poller237 waiter *fdwait.Poller
210 terminal bool // polling ended in failure; protected by mu238 terminal bool // polling ended in failure; protected by mu
@@ -218,9 +246,12 @@ type syncWaiter struct {
218 pollError sync.Once246 pollError sync.Once
219 }247 }
220 248
221-// syncWait is one acquire point a queued commit waits for.249+// syncWait is one acquire point a queued commit waits for. An explicit wait
250+// lives on the display loop only; an implicit one is polled by run.
222 type syncWait struct {251 type syncWait struct {
223 efd int252 efd int
253+ src acquireRemover // explicit: the display-loop registration, nil once removed
254+ explicit bool
224 done bool // the eventfd fired or the wait was cancelled (display goroutine)255 done bool // the eventfd fired or the wait was cancelled (display goroutine)
225 failed bool // observation failed; done must not authorize publication256 failed bool // observation failed; done must not authorize publication
226 cancelled atomic.Bool // run stops polling it and closes efd257 cancelled atomic.Bool // run stops polling it and closes efd
@@ -240,30 +271,65 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
240 return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil271 return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil
241 }272 }
242 273
243-// watch registers an eventfd for p; the wait is ready once it fires.274+// watch registers an eventfd for p on the display loop; the wait is ready once
275+// it fires. Display goroutine only.
244 func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {276 func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
245- if sw.stopped() {
246- return &syncWait{efd: -1, done: true, failed: true}, unix.EIO
247- }
248 ready, err := p.tl.dev.signalled(p.tl.handle, p.point)277 ready, err := p.tl.dev.signalled(p.tl.handle, p.point)
249 if err != nil || ready {278 if err != nil || ready {
250- return &syncWait{efd: -1, done: true, failed: err != nil}, err279+ return &syncWait{efd: -1, explicit: true, done: true, failed: err != nil}, err
251 }280 }
252 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)281 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
253 if err != nil {282 if err != nil {
254- return &syncWait{efd: -1, done: true, failed: true}, err283+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
255 }284 }
256 if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {285 if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {
257 unix.Close(efd)286 unix.Close(efd)
258- return &syncWait{efd: -1, done: true, failed: true}, err287+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
259 }288 }
260- w := sw.add(efd)289+ w := &syncWait{efd: efd, explicit: true}
261- if w.failed {290+ src, err := sw.src.add(efd, func() { sw.onAcquire(w) })
262- return w, unix.EIO291+ if err != nil {
292+ unix.Close(efd)
293+ return &syncWait{efd: -1, explicit: true, done: true, failed: true}, err
294+ }
295+ w.src = src
296+ if sw.pending == nil {
297+ sw.pending = map[*syncWait]struct{}{}
263 }298 }
299+ sw.pending[w] = struct{}{}
264 return w, nil300 return w, nil
265 }301 }
266 302
303+// onAcquire is the eventfd's callback, on the display goroutine.
304+func (sw *syncWaiter) onAcquire(w *syncWait) {
305+ if w.done {
306+ return
307+ }
308+ var b [8]byte
309+ if _, err := unix.Read(w.efd, b[:]); err != nil {
310+ if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
311+ return // not signalled yet: keep waiting
312+ }
313+ w.failed = true
314+ }
315+ sw.finish(w)
316+ sw.pace()
317+}
318+
319+// finish unregisters then closes an explicit wait's eventfd and marks it done.
320+func (sw *syncWaiter) finish(w *syncWait) {
321+ delete(sw.pending, w)
322+ if w.src != nil {
323+ w.src.Remove()
324+ w.src = nil
325+ }
326+ if w.efd >= 0 {
327+ unix.Close(w.efd)
328+ w.efd = -1
329+ }
330+ w.done = true
331+}
332+
267 // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch.333 // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch.
268 func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error {334 func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error {
269 if sw.stopped() {335 if sw.stopped() {
@@ -336,6 +402,9 @@ func (sw *syncWaiter) add(fd int) *syncWait {
336 402
337 // fired reports whether w's point is signalled, and forgets w once it has.403 // fired reports whether w's point is signalled, and forgets w once it has.
338 func (sw *syncWaiter) fired(w *syncWait) bool {404 func (sw *syncWaiter) fired(w *syncWait) bool {
405+ if w.explicit {
406+ return w.done // display goroutine only
407+ }
339 sw.mu.Lock()408 sw.mu.Lock()
340 defer sw.mu.Unlock()409 defer sw.mu.Unlock()
341 if ready, ok := sw.ready[w]; ok {410 if ready, ok := sw.ready[w]; ok {
@@ -347,7 +416,14 @@ func (sw *syncWaiter) fired(w *syncWait) bool {
347 416
348 // cancel stops watching w (the commit was dropped).417 // cancel stops watching w (the commit was dropped).
349 func (sw *syncWaiter) cancel(w *syncWait) {418 func (sw *syncWaiter) cancel(w *syncWait) {
350- if w == nil || w.efd < 0 {419+ if w == nil {
420+ return
421+ }
422+ if w.explicit {
423+ sw.finish(w)
424+ return
425+ }
426+ if w.efd < 0 {
351 return427 return
352 }428 }
353 w.cancelled.Store(true)429 w.cancelled.Store(true)
@@ -355,7 +431,7 @@ func (sw *syncWaiter) cancel(w *syncWait) {
355 delete(sw.ready, w)431 delete(sw.ready, w)
356 sw.mu.Unlock()432 sw.mu.Unlock()
357 w.done = true433 w.done = true
358- _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd434+ _, _ = sw.pipeW.Write([]byte{0}) // run closes its fence fd
359 }435 }
360 436
361 func (sw *syncWaiter) stopped() bool {437 func (sw *syncWaiter) stopped() bool {
@@ -380,12 +456,12 @@ func (sw *syncWaiter) fail(waits []*syncWait) {
380 sw.wake()456 sw.wake()
381 }457 }
382 458
383-// run polls every watched eventfd; it owns and closes them.459+// run polls the implicit dma-buf fence descriptors; it owns and closes them.
384 func (sw *syncWaiter) run(ctx context.Context) {460 func (sw *syncWaiter) run(ctx context.Context) {
385 defer sw.pipeR.Close()461 defer sw.pipeR.Close()
386 defer sw.waiter.Close()462 defer sw.waiter.Close()
387 pipeFD := int(sw.pipeR.Fd())463 pipeFD := int(sw.pipeR.Fd())
388- // The pipe lives as long as the poller; the eventfds come and go. The464+ // The pipe lives as long as the poller; the fence fds come and go. The
389 // poll sleeps until a fence, a new or cancelled wait (the pipe), or the465 // poll sleeps until a fence, a new or cancelled wait (the pipe), or the
390 // end of ctx.466 // end of ctx.
391 var waits []*syncWait467 var waits []*syncWait
@@ -593,3 +669,23 @@ func (s *surface) applySync(cs *commitSync) {
593 s.server.dropTimeline(cs.acquire.tl)669 s.server.dropTimeline(cs.acquire.tl)
594 s.hold = syncHold{acquire: f, release: cs.release}670 s.hold = syncHold{acquire: f, release: cs.release}
595 }671 }
672+
673+// acquireSignalled applies what a fired acquire point unblocked: like a
674+// commit, it ticks inline instead of posting a tick to the display loop.
675+func (s *Server) acquireSignalled() {
676+ s.paceDepth++
677+ s.wakePacer()
678+ s.paceDepth--
679+ if s.paceDepth == 0 && s.paceDirty {
680+ s.paceDirty = false
681+ s.paceNow()
682+ }
683+}
684+
685+// releasePending unregisters and closes the explicit waits still pending at
686+// shutdown. Called once the display loop has stopped.
687+func (sw *syncWaiter) releasePending() {
688+ for w := range sw.pending {
689+ sw.finish(w)
690+ }
691+}
A internal/adapters/wayland/syncwait_explicit_test.go +161 -0
@@ -0,0 +1,161 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/stretchr/testify/mock"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// explicitWaiter is a waiter whose explicit waits register with a mocked
12+// display-loop source; the callback and fd are captured.
13+type explicitWaiter struct {
14+ sw *syncWaiter
15+ src *mockacquireSource
16+ rm *mockacquireRemover
17+ paced int
18+ fd int
19+ fire func()
20+ point syncPoint
21+}
22+
23+func newExplicitWaiter(t *testing.T, addErr error) *explicitWaiter {
24+ t.Helper()
25+ e := &explicitWaiter{src: newMockacquireSource(t), rm: newMockacquireRemover(t)}
26+ dev := newMocksyncobjDevice(t)
27+ dev.EXPECT().signalled(uint32(7), uint64(4)).Return(false, nil).Once()
28+ dev.EXPECT().eventfd(uint32(7), uint64(4), mock.Anything).Return(nil).Once()
29+ e.point = syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4}
30+ sw, err := newSyncWaiter(func() { t.Error("explicit wait posted a tick") })
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ t.Cleanup(func() { sw.discard(); sw.close() })
35+ sw.src, sw.pace = e.src, func() { e.paced++ }
36+ e.sw = sw
37+ e.src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(fd int, f func()) (acquireRemover, error) {
38+ e.fd, e.fire = fd, f
39+ if addErr != nil {
40+ return nil, addErr
41+ }
42+ return e.rm, nil
43+ }).Once()
44+ return e
45+}
46+
47+func fdClosed(fd int) bool {
48+ _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
49+ return err == unix.EBADF
50+}
51+
52+// A fired eventfd removes the source, closes the fd and wakes the pacer
53+// inline, never through a posted tick.
54+func TestExplicitWaitFiresOnDisplayLoop(t *testing.T) {
55+ e := newExplicitWaiter(t, nil)
56+ w, err := e.sw.watch(e.point)
57+ if err != nil || e.sw.fired(w) {
58+ t.Fatalf("watch: %v fired=%v", err, e.sw.fired(w))
59+ }
60+ e.rm.EXPECT().Remove().Once()
61+ one := [8]byte{1}
62+ if _, err := unix.Write(e.fd, one[:]); err != nil {
63+ t.Fatal(err)
64+ }
65+ fd := e.fd
66+ e.fire()
67+ if !e.sw.fired(w) || w.failed || e.paced != 1 || !fdClosed(fd) {
68+ t.Fatalf("fired=%v failed=%v paced=%d closed=%v", e.sw.fired(w), w.failed, e.paced, fdClosed(fd))
69+ }
70+ e.sw.cancel(w) // applySync cancels after the fire: nothing more to release
71+}
72+
73+// Cancelling before the fire removes the source, then closes the fd.
74+func TestExplicitWaitCancelRemovesThenCloses(t *testing.T) {
75+ e := newExplicitWaiter(t, nil)
76+ w, err := e.sw.watch(e.point)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ e.rm.EXPECT().Remove().RunAndReturn(func() {
81+ if fdClosed(e.fd) {
82+ t.Error("fd closed before Remove")
83+ }
84+ }).Once()
85+ e.sw.cancel(w)
86+ if !e.sw.fired(w) || !fdClosed(e.fd) || e.paced != 0 {
87+ t.Fatal("cancel left the wait registered")
88+ }
89+ e.sw.cancel(w)
90+}
91+
92+func TestExplicitWaitAddFailureFails(t *testing.T) {
93+ e := newExplicitWaiter(t, unix.ENOMEM)
94+ w, err := e.sw.watch(e.point)
95+ if err == nil || !e.sw.fired(w) || !w.failed || !fdClosed(e.fd) {
96+ t.Fatalf("add failure: err=%v", err)
97+ }
98+}
99+
100+// A callback with nothing to read keeps the wait registered.
101+func TestExplicitWaitSpuriousCallbackKeepsWaiting(t *testing.T) {
102+ e := newExplicitWaiter(t, nil)
103+ w, err := e.sw.watch(e.point)
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ e.fire()
108+ if e.sw.fired(w) || e.paced != 0 || fdClosed(e.fd) {
109+ t.Fatal("spurious callback ended the wait")
110+ }
111+ e.rm.EXPECT().Remove().Once()
112+ e.sw.cancel(w)
113+}
114+
115+// Shutdown releases waits still registered, Remove before Close.
116+func TestExplicitWaitReleasePending(t *testing.T) {
117+ e := newExplicitWaiter(t, nil)
118+ if _, err := e.sw.watch(e.point); err != nil {
119+ t.Fatal(err)
120+ }
121+ e.rm.EXPECT().Remove().Once()
122+ e.sw.releasePending()
123+ if !fdClosed(e.fd) {
124+ t.Fatal("pending wait leaked its eventfd")
125+ }
126+}
127+
128+// End to end: a queued commit stays queued until the callback runs, then
129+// is applied by that same tick.
130+func TestSyncobjCommitAppliedByAcquireCallback(t *testing.T) {
131+ h := newSyncHarness(t)
132+ h.releases = map[uint32]*syncReleaseProxy{}
133+ src, rm := newMockacquireSource(t), newMockacquireRemover(t)
134+ var fire func()
135+ src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(_ int, f func()) (acquireRemover, error) {
136+ fire = f
137+ return rm, nil
138+ }).Once()
139+ rm.EXPECT().Remove().Maybe()
140+ h.s.display.Do(func() { h.s.syncWait.src = src })
141+ a := h.dmabuf()
142+ h.commit(a, 1, 2)
143+ if _, ok := h.content(50 * time.Millisecond); ok {
144+ t.Fatal("content before the acquire point fired")
145+ }
146+ h.fire(1) // the kernel signals: efd readable (display loop is mocked, so nothing reads it)
147+ var applied, posted bool
148+ h.s.display.Do(func() {
149+ fire()
150+ // Same tick: the commit left the queue without posting a pace tick.
151+ applied = len(h.s.windows[h.win].xdg.surface.queue) == 0
152+ posted = h.s.paceSignal.Load()
153+ })
154+ if !applied || posted {
155+ t.Fatalf("acquire callback: applied=%v posted tick=%v, want applied inline", applied, posted)
156+ }
157+ got, ok := h.content(2 * time.Second)
158+ if !ok || got.Acquire == nil {
159+ t.Fatalf("content %+v", got)
160+ }
161+}
@@ -0,0 +1,161 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/stretchr/testify/mock"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// explicitWaiter is a waiter whose explicit waits register with a mocked
12+// display-loop source; the callback and fd are captured.
13+type explicitWaiter struct {
14+ sw *syncWaiter
15+ src *mockacquireSource
16+ rm *mockacquireRemover
17+ paced int
18+ fd int
19+ fire func()
20+ point syncPoint
21+}
22+
23+func newExplicitWaiter(t *testing.T, addErr error) *explicitWaiter {
24+ t.Helper()
25+ e := &explicitWaiter{src: newMockacquireSource(t), rm: newMockacquireRemover(t)}
26+ dev := newMocksyncobjDevice(t)
27+ dev.EXPECT().signalled(uint32(7), uint64(4)).Return(false, nil).Once()
28+ dev.EXPECT().eventfd(uint32(7), uint64(4), mock.Anything).Return(nil).Once()
29+ e.point = syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4}
30+ sw, err := newSyncWaiter(func() { t.Error("explicit wait posted a tick") })
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ t.Cleanup(func() { sw.discard(); sw.close() })
35+ sw.src, sw.pace = e.src, func() { e.paced++ }
36+ e.sw = sw
37+ e.src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(fd int, f func()) (acquireRemover, error) {
38+ e.fd, e.fire = fd, f
39+ if addErr != nil {
40+ return nil, addErr
41+ }
42+ return e.rm, nil
43+ }).Once()
44+ return e
45+}
46+
47+func fdClosed(fd int) bool {
48+ _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
49+ return err == unix.EBADF
50+}
51+
52+// A fired eventfd removes the source, closes the fd and wakes the pacer
53+// inline, never through a posted tick.
54+func TestExplicitWaitFiresOnDisplayLoop(t *testing.T) {
55+ e := newExplicitWaiter(t, nil)
56+ w, err := e.sw.watch(e.point)
57+ if err != nil || e.sw.fired(w) {
58+ t.Fatalf("watch: %v fired=%v", err, e.sw.fired(w))
59+ }
60+ e.rm.EXPECT().Remove().Once()
61+ one := [8]byte{1}
62+ if _, err := unix.Write(e.fd, one[:]); err != nil {
63+ t.Fatal(err)
64+ }
65+ fd := e.fd
66+ e.fire()
67+ if !e.sw.fired(w) || w.failed || e.paced != 1 || !fdClosed(fd) {
68+ t.Fatalf("fired=%v failed=%v paced=%d closed=%v", e.sw.fired(w), w.failed, e.paced, fdClosed(fd))
69+ }
70+ e.sw.cancel(w) // applySync cancels after the fire: nothing more to release
71+}
72+
73+// Cancelling before the fire removes the source, then closes the fd.
74+func TestExplicitWaitCancelRemovesThenCloses(t *testing.T) {
75+ e := newExplicitWaiter(t, nil)
76+ w, err := e.sw.watch(e.point)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ e.rm.EXPECT().Remove().RunAndReturn(func() {
81+ if fdClosed(e.fd) {
82+ t.Error("fd closed before Remove")
83+ }
84+ }).Once()
85+ e.sw.cancel(w)
86+ if !e.sw.fired(w) || !fdClosed(e.fd) || e.paced != 0 {
87+ t.Fatal("cancel left the wait registered")
88+ }
89+ e.sw.cancel(w)
90+}
91+
92+func TestExplicitWaitAddFailureFails(t *testing.T) {
93+ e := newExplicitWaiter(t, unix.ENOMEM)
94+ w, err := e.sw.watch(e.point)
95+ if err == nil || !e.sw.fired(w) || !w.failed || !fdClosed(e.fd) {
96+ t.Fatalf("add failure: err=%v", err)
97+ }
98+}
99+
100+// A callback with nothing to read keeps the wait registered.
101+func TestExplicitWaitSpuriousCallbackKeepsWaiting(t *testing.T) {
102+ e := newExplicitWaiter(t, nil)
103+ w, err := e.sw.watch(e.point)
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ e.fire()
108+ if e.sw.fired(w) || e.paced != 0 || fdClosed(e.fd) {
109+ t.Fatal("spurious callback ended the wait")
110+ }
111+ e.rm.EXPECT().Remove().Once()
112+ e.sw.cancel(w)
113+}
114+
115+// Shutdown releases waits still registered, Remove before Close.
116+func TestExplicitWaitReleasePending(t *testing.T) {
117+ e := newExplicitWaiter(t, nil)
118+ if _, err := e.sw.watch(e.point); err != nil {
119+ t.Fatal(err)
120+ }
121+ e.rm.EXPECT().Remove().Once()
122+ e.sw.releasePending()
123+ if !fdClosed(e.fd) {
124+ t.Fatal("pending wait leaked its eventfd")
125+ }
126+}
127+
128+// End to end: a queued commit stays queued until the callback runs, then
129+// is applied by that same tick.
130+func TestSyncobjCommitAppliedByAcquireCallback(t *testing.T) {
131+ h := newSyncHarness(t)
132+ h.releases = map[uint32]*syncReleaseProxy{}
133+ src, rm := newMockacquireSource(t), newMockacquireRemover(t)
134+ var fire func()
135+ src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(_ int, f func()) (acquireRemover, error) {
136+ fire = f
137+ return rm, nil
138+ }).Once()
139+ rm.EXPECT().Remove().Maybe()
140+ h.s.display.Do(func() { h.s.syncWait.src = src })
141+ a := h.dmabuf()
142+ h.commit(a, 1, 2)
143+ if _, ok := h.content(50 * time.Millisecond); ok {
144+ t.Fatal("content before the acquire point fired")
145+ }
146+ h.fire(1) // the kernel signals: efd readable (display loop is mocked, so nothing reads it)
147+ var applied, posted bool
148+ h.s.display.Do(func() {
149+ fire()
150+ // Same tick: the commit left the queue without posting a pace tick.
151+ applied = len(h.s.windows[h.win].xdg.surface.queue) == 0
152+ posted = h.s.paceSignal.Load()
153+ })
154+ if !applied || posted {
155+ t.Fatalf("acquire callback: applied=%v posted tick=%v, want applied inline", applied, posted)
156+ }
157+ got, ok := h.content(2 * time.Second)
158+ if !ok || got.Acquire == nil {
159+ t.Fatalf("content %+v", got)
160+ }
161+}

Comments0 open / 0

no comments yet.

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
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close