detached c0f82f3

perf(core): reuse the outbox progress signal and skip doomed direct sends

The sender signals a capacity-one progress channel instead of closing and
remaking it per batch, and push queues at once when the commands channel is
full, so a secured command no longer boxes a throwaway envelope. A queued
delivery allocation guard joins perf-check.

TestBlockedStartDoesNotDelayRouting now releases and joins the blocked
start on failure too.
commit
c0f82f31ba2a826a73129369a57f07c1e973c2d8
author
brice · · 2026-10-18 05:50 UTC
parent
011648f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +66 -12
M Makefile +1 -1
@@ -69,7 +69,7 @@ adapter-imports:
6969 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7070 # Fast, mandatory guards for tiled content publications and SHM copies.
7171 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1
72+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1
7373 check: vet test arch fakes-check log-check perf-check
7474
7575 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -69,7 +69,7 @@ adapter-imports:
69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
70 # Fast, mandatory guards for tiled content publications and SHM copies.70 # Fast, mandatory guards for tiled content publications and SHM copies.
71 perf-check:71 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=172+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1
73 check: vet test arch fakes-check log-check perf-check73 check: vet test arch fakes-check log-check perf-check
74 74
75 # On-demand SDR/HDR colour check with examples/testpattern on a headless75 # On-demand SDR/HDR colour check with examples/testpattern on a headless
M internal/app/outputrouting_test.go +18 -4
@@ -3,6 +3,7 @@ package app
33 import (
44 "context"
55 "os"
6+ "sync"
67 "testing"
78 "time"
89
@@ -164,8 +165,21 @@ func TestBlockedStartDoesNotDelayRouting(t *testing.T) {
164165 return nil, ret
165166 }).Once()
166167 attacher.EXPECT().DetachOutput(mock.Anything).Maybe()
167- started := make(chan bool)
168- go func() { started <- set.start(ctx, "B", idleRun) }()
168+ releaseB := sync.OnceFunc(func() { close(release) })
169+ wait := sync.OnceValue(set.wait)
170+ started := make(chan bool, 1)
171+ startDone := make(chan struct{})
172+ go func() {
173+ defer close(startDone)
174+ started <- set.start(ctx, "B", idleRun)
175+ }()
176+ // A failed assertion still unblocks and joins B's start.
177+ t.Cleanup(func() {
178+ releaseB()
179+ cancel()
180+ <-startDone
181+ _ = wait()
182+ })
169183 <-attaching
170184 a := set.outs["A"]
171185 deadline := time.After(2 * time.Second)
@@ -192,11 +206,11 @@ func TestBlockedStartDoesNotDelayRouting(t *testing.T) {
192206 if got := <-a.cursor; got.Shape != "text" {
193207 t.Fatalf("A cursor %+v", got)
194208 }
195- close(release)
209+ releaseB()
196210 if !<-started {
197211 t.Fatal("B did not start")
198212 }
199- if err := set.wait(); err != nil {
213+ if err := wait(); err != nil {
200214 t.Fatal(err)
201215 }
202216 }
@@ -3,6 +3,7 @@ package app
3 import (3 import (
4 "context"4 "context"
5 "os"5 "os"
6+ "sync"
6 "testing"7 "testing"
7 "time"8 "time"
8 9
@@ -164,8 +165,21 @@ func TestBlockedStartDoesNotDelayRouting(t *testing.T) {
164 return nil, ret165 return nil, ret
165 }).Once()166 }).Once()
166 attacher.EXPECT().DetachOutput(mock.Anything).Maybe()167 attacher.EXPECT().DetachOutput(mock.Anything).Maybe()
167- started := make(chan bool)168+ releaseB := sync.OnceFunc(func() { close(release) })
168- go func() { started <- set.start(ctx, "B", idleRun) }()169+ wait := sync.OnceValue(set.wait)
170+ started := make(chan bool, 1)
171+ startDone := make(chan struct{})
172+ go func() {
173+ defer close(startDone)
174+ started <- set.start(ctx, "B", idleRun)
175+ }()
176+ // A failed assertion still unblocks and joins B's start.
177+ t.Cleanup(func() {
178+ releaseB()
179+ cancel()
180+ <-startDone
181+ _ = wait()
182+ })
169 <-attaching183 <-attaching
170 a := set.outs["A"]184 a := set.outs["A"]
171 deadline := time.After(2 * time.Second)185 deadline := time.After(2 * time.Second)
@@ -192,11 +206,11 @@ func TestBlockedStartDoesNotDelayRouting(t *testing.T) {
192 if got := <-a.cursor; got.Shape != "text" {206 if got := <-a.cursor; got.Shape != "text" {
193 t.Fatalf("A cursor %+v", got)207 t.Fatalf("A cursor %+v", got)
194 }208 }
195- close(release)209+ releaseB()
196 if !<-started {210 if !<-started {
197 t.Fatal("B did not start")211 t.Fatal("B did not start")
198 }212 }
199- if err := set.wait(); err != nil {213+ if err := wait(); err != nil {
200 t.Fatal(err)214 t.Fatal(err)
201 }215 }
202 }216 }
M internal/core/outbox.go +16 -7
@@ -25,7 +25,8 @@ type outbox struct {
2525 // sending: a command pushed then must queue behind it.
2626 busy bool
2727 // pushed and sent count the entries queued and delivered (a merge adds
28- // none); progress is closed and replaced at each delivery, for flush.
28+ // none); progress is signalled (never blocking) at each delivery, for
29+ // flush. The owner is its only waiter, so one pending signal is enough.
2930 pushed, sent uint64
3031 progress chan struct{}
3132 // running is set while a sender (run) owns delivery.
@@ -63,7 +64,7 @@ func (o *outbox) backlog() uint64 {
6364 }
6465
6566 func newOutbox() outbox {
66- return outbox{wake: make(chan struct{}, 1), progress: make(chan struct{})}
67+ return outbox{wake: make(chan struct{}, 1), progress: make(chan struct{}, 1)}
6768 }
6869
6970 // command returns what the sender delivers.
@@ -91,7 +92,10 @@ func (o *outbox) push(out chan<- ports.ClientCommand, v ports.ClientCommand, sta
9192 e := outCmd{cmd: v, state: state, secured: secured}
9293 e.motion, e.isMotion = v.(ports.PointerMotionTo)
9394 o.mu.Lock()
94- if !o.busy && len(o.queue) == 0 {
95+ // A full channel would make the attempt fail after building the
96+ // envelope; queue at once instead. The length is only a hint: queued
97+ // delivery keeps the order either way.
98+ if !o.busy && len(o.queue) == 0 && (cap(out) == 0 || len(out) < cap(out)) {
9599 // A send that cannot block is safe under the lock.
96100 select {
97101 case out <- e.command():
@@ -135,7 +139,7 @@ func (o *outbox) push(out chan<- ports.ClientCommand, v ports.ClientCommand, sta
135139 func (o *outbox) flush(ctx context.Context, out chan<- ports.ClientCommand, seq uint64) error {
136140 for {
137141 o.mu.Lock()
138- done, progress, running := o.sent >= seq, o.progress, o.running
142+ done, running := o.sent >= seq, o.running
139143 o.mu.Unlock()
140144 if done {
141145 return nil
@@ -149,8 +153,9 @@ func (o *outbox) flush(ctx context.Context, out chan<- ports.ClientCommand, seq
149153 }
150154 continue
151155 }
156+ // A signal left from an earlier delivery only costs a recheck.
152157 select {
153- case <-progress:
158+ case <-o.progress:
154159 case <-ctx.Done():
155160 return ctx.Err()
156161 }
@@ -178,10 +183,14 @@ func (o *outbox) recycle(batch []outCmd) {
178183 o.busy = false
179184 if n > 0 {
180185 o.sent += n
181- close(o.progress)
182- o.progress = make(chan struct{})
183186 }
184187 o.mu.Unlock()
188+ if n > 0 {
189+ select {
190+ case o.progress <- struct{}{}:
191+ default:
192+ }
193+ }
185194 }
186195
187196 // start marks a sender as running; call it before starting run, so that a
@@ -25,7 +25,8 @@ type outbox struct {
25 // sending: a command pushed then must queue behind it.25 // sending: a command pushed then must queue behind it.
26 busy bool26 busy bool
27 // pushed and sent count the entries queued and delivered (a merge adds27 // pushed and sent count the entries queued and delivered (a merge adds
28- // none); progress is closed and replaced at each delivery, for flush.28+ // none); progress is signalled (never blocking) at each delivery, for
29+ // flush. The owner is its only waiter, so one pending signal is enough.
29 pushed, sent uint6430 pushed, sent uint64
30 progress chan struct{}31 progress chan struct{}
31 // running is set while a sender (run) owns delivery.32 // running is set while a sender (run) owns delivery.
@@ -63,7 +64,7 @@ func (o *outbox) backlog() uint64 {
63 }64 }
64 65
65 func newOutbox() outbox {66 func newOutbox() outbox {
66- return outbox{wake: make(chan struct{}, 1), progress: make(chan struct{})}67+ return outbox{wake: make(chan struct{}, 1), progress: make(chan struct{}, 1)}
67 }68 }
68 69
69 // command returns what the sender delivers.70 // command returns what the sender delivers.
@@ -91,7 +92,10 @@ func (o *outbox) push(out chan<- ports.ClientCommand, v ports.ClientCommand, sta
91 e := outCmd{cmd: v, state: state, secured: secured}92 e := outCmd{cmd: v, state: state, secured: secured}
92 e.motion, e.isMotion = v.(ports.PointerMotionTo)93 e.motion, e.isMotion = v.(ports.PointerMotionTo)
93 o.mu.Lock()94 o.mu.Lock()
94- if !o.busy && len(o.queue) == 0 {95+ // A full channel would make the attempt fail after building the
96+ // envelope; queue at once instead. The length is only a hint: queued
97+ // delivery keeps the order either way.
98+ if !o.busy && len(o.queue) == 0 && (cap(out) == 0 || len(out) < cap(out)) {
95 // A send that cannot block is safe under the lock.99 // A send that cannot block is safe under the lock.
96 select {100 select {
97 case out <- e.command():101 case out <- e.command():
@@ -135,7 +139,7 @@ func (o *outbox) push(out chan<- ports.ClientCommand, v ports.ClientCommand, sta
135 func (o *outbox) flush(ctx context.Context, out chan<- ports.ClientCommand, seq uint64) error {139 func (o *outbox) flush(ctx context.Context, out chan<- ports.ClientCommand, seq uint64) error {
136 for {140 for {
137 o.mu.Lock()141 o.mu.Lock()
138- done, progress, running := o.sent >= seq, o.progress, o.running142+ done, running := o.sent >= seq, o.running
139 o.mu.Unlock()143 o.mu.Unlock()
140 if done {144 if done {
141 return nil145 return nil
@@ -149,8 +153,9 @@ func (o *outbox) flush(ctx context.Context, out chan<- ports.ClientCommand, seq
149 }153 }
150 continue154 continue
151 }155 }
156+ // A signal left from an earlier delivery only costs a recheck.
152 select {157 select {
153- case <-progress:158+ case <-o.progress:
154 case <-ctx.Done():159 case <-ctx.Done():
155 return ctx.Err()160 return ctx.Err()
156 }161 }
@@ -178,10 +183,14 @@ func (o *outbox) recycle(batch []outCmd) {
178 o.busy = false183 o.busy = false
179 if n > 0 {184 if n > 0 {
180 o.sent += n185 o.sent += n
181- close(o.progress)
182- o.progress = make(chan struct{})
183 }186 }
184 o.mu.Unlock()187 o.mu.Unlock()
188+ if n > 0 {
189+ select {
190+ case o.progress <- struct{}{}:
191+ default:
192+ }
193+ }
185 }194 }
186 195
187 // start marks a sender as running; call it before starting run, so that a196 // start marks a sender as running; call it before starting run, so that a
M internal/core/outbox_test.go +31 -0
@@ -122,3 +122,34 @@ func TestOutboxIsBounded(t *testing.T) {
122122 }
123123 })
124124 }
125+
126+// Queuing a command and delivering it allocates nothing once warm: the
127+// batches are recycled and flush's progress signal is reused.
128+func TestOutboxQueuedDeliveryAllocations(t *testing.T) {
129+ o := newOutbox()
130+ full := make(chan ports.ClientCommand, 1)
131+ full <- ports.FocusWindow{}
132+ sink := make(chan ports.ClientCommand, 8)
133+ var cmd ports.ClientCommand = ports.FocusWindow{ID: 1}
134+ var state ports.SecurityState
135+ ctx := context.Background()
136+ cycle := func() {
137+ // full has no room: push queues, even for a secured command.
138+ o.push(full, cmd, state, false)
139+ o.push(full, cmd, state, true)
140+ o.deliver(ctx, sink, o.take())
141+ <-sink
142+ <-sink
143+ select {
144+ case <-o.progress:
145+ default:
146+ t.Fatal("delivery left no progress signal")
147+ }
148+ }
149+ cycle()
150+ // The secured delivery boxes its envelope once; the failed direct send
151+ // must not box another.
152+ if n := testing.AllocsPerRun(100, cycle); n > 1 {
153+ t.Fatalf("%v allocations per queued delivery, want at most 1", n)
154+ }
155+}
@@ -122,3 +122,34 @@ func TestOutboxIsBounded(t *testing.T) {
122 }122 }
123 })123 })
124 }124 }
125+
126+// Queuing a command and delivering it allocates nothing once warm: the
127+// batches are recycled and flush's progress signal is reused.
128+func TestOutboxQueuedDeliveryAllocations(t *testing.T) {
129+ o := newOutbox()
130+ full := make(chan ports.ClientCommand, 1)
131+ full <- ports.FocusWindow{}
132+ sink := make(chan ports.ClientCommand, 8)
133+ var cmd ports.ClientCommand = ports.FocusWindow{ID: 1}
134+ var state ports.SecurityState
135+ ctx := context.Background()
136+ cycle := func() {
137+ // full has no room: push queues, even for a secured command.
138+ o.push(full, cmd, state, false)
139+ o.push(full, cmd, state, true)
140+ o.deliver(ctx, sink, o.take())
141+ <-sink
142+ <-sink
143+ select {
144+ case <-o.progress:
145+ default:
146+ t.Fatal("delivery left no progress signal")
147+ }
148+ }
149+ cycle()
150+ // The secured delivery boxes its envelope once; the failed direct send
151+ // must not box another.
152+ if n := testing.AllocsPerRun(100, cycle); n > 1 {
153+ t.Fatalf("%v allocations per queued delivery, want at most 1", n)
154+ }
155+}

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 25

Tags 10

↑↓ moveenter openesc close