detached cea7d5e

perf(wayland): drop per-commit and per-tick allocations

- releaseHeld reuses a Server scratch slice for the live output reports
  instead of making one every pacer tick.
- Damage and DamageBuffer collect into slices that travel with the pooled
  update: queueUpdate hands the retired update's capacity to the surface's
  next commit, so rects stop growing a fresh array per commit. The queued
  update keeps its own rects until it retires.
- An explicit-sync commit keeps its points in a per-surface scratch that
  queueUpdate moves into storage inside the update, instead of a heap
  commitSync per commit.
- emitContent appends the damage history inside one backing array, always
  past the end of every published slice, and publishes a capacity-clipped
  slice, so readers still see an immutable history and cannot append into
  the spare. A new array is allocated once per ~29 publications instead of
  every one. The content-change channel is created lazily by the only
  waiter (forwardContents) and closed/cleared on publish, so a commit
  allocates a channel only when someone waits.

Tests assert zero allocations for releaseHeld, damage+queueUpdate+apply and
emitContent, and that retained histories and queued damage never change.
commit
cea7d5e477dbc7c6e48719f3012a749c2dfdd4e6
author
brice · · 2026-10-10 14:29 UTC
committer
brice · · 2026-10-10 15:39 UTC
parent
af1828e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +202 -18
A internal/adapters/wayland/alloc_hotpath_test.go +145 -0
@@ -0,0 +1,145 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/stretchr/testify/mock"
9+)
10+
11+// The steady-state hot paths that run for every frame or commit allocate
12+// nothing (or one pooled object per damage-history generation).
13+
14+func TestReleaseHeldAllocations(t *testing.T) {
15+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
16+ s := &Server{outputs: []*output{out}, reports: map[string]ports.OutputPresented{}}
17+ reports := []ports.OutputPresented{{Output: "DP-2", Seen: map[ports.WindowID]uint64{1: 1}}}
18+ now := time.Unix(100, 0)
19+ s.releaseHeld(now, reports)
20+ if allocs := testing.AllocsPerRun(100, func() { s.releaseHeld(now, reports) }); allocs != 0 {
21+ t.Errorf("releaseHeld: %.1f allocs, want 0", allocs)
22+ }
23+}
24+
25+func TestCommitDamageAllocations(t *testing.T) {
26+ srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
27+ surf := &surface{server: srv}
28+ commit := func() {
29+ surf.Damage(nil, 0, 0, 4, 4)
30+ surf.DamageBuffer(nil, 1, 1, 2, 2)
31+ surf.queueUpdate()
32+ surf.queue[0].applyGraph()
33+ }
34+ commit()
35+ if allocs := testing.AllocsPerRun(100, commit); allocs != 0 {
36+ t.Errorf("damage requests + queueUpdate + apply: %.1f allocs, want 0", allocs)
37+ }
38+ // The queued update keeps its rects while the next commit collects its own.
39+ surf.Damage(nil, 0, 0, 1, 1)
40+ surf.queueUpdate()
41+ first := surf.queue[0]
42+ surf.Damage(nil, 5, 5, 2, 2)
43+ if len(first.damage) != 1 || first.damage[0].W != 1 || len(surf.next.damage) != 1 || surf.next.damage[0].W != 2 {
44+ t.Fatalf("damage shared between commits: queued %v pending %v", first.damage, surf.next.damage)
45+ }
46+ surf.queueUpdate()
47+ if second := surf.queue[1]; len(second.damage) != 1 || second.damage[0].W != 2 || first.damage[0].W != 1 {
48+ t.Fatalf("damage mixed up: %v %v", first.damage, second.damage)
49+ }
50+}
51+
52+// An explicit-sync commit moves its points into the update: the commit
53+// itself allocates no commitSync, and a pending one is not overwritten.
54+func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
55+ dev := newMocksyncobjDevice(t)
56+ dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil)
57+ srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
58+ sw, err := newSyncWaiter(func() {})
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+ defer sw.pipeR.Close()
63+ defer sw.close()
64+ srv.syncWait = sw
65+ tl := &timeline{dev: dev, handle: 7, alive: true}
66+ surf := &surface{server: srv, sync: &syncState{}}
67+ take := func(point uint64) {
68+ surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: point}, syncPoint{tl: tl, point: point + 1}
69+ surf.takeSyncPoints()
70+ surf.queueUpdate()
71+ }
72+ take(10)
73+ take(20)
74+ a, b := surf.queue[0], surf.queue[1]
75+ if a.sync == nil || b.sync == nil || a.sync == b.sync || a.sync.acquire.point != 10 || b.sync.acquire.point != 20 || surf.next.sync != nil {
76+ t.Fatalf("sync points mixed up: %+v %+v pending %+v", a.sync, b.sync, surf.next.sync)
77+ }
78+ // The commit's points live in the surface until queueUpdate: no heap
79+ // commitSync per commit (the mocked device's own allocations would drown
80+ // an AllocsPerRun count here).
81+ surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: 30}, syncPoint{tl: tl, point: 31}
82+ surf.takeSyncPoints()
83+ if surf.next.sync != &surf.syncScratch {
84+ t.Fatal("commit points not held in the surface scratch")
85+ }
86+}
87+
88+func TestEmitContentAllocations(t *testing.T) {
89+ s := &Server{
90+ channels: Channels{Contents: make(chan ports.SurfaceContent)},
91+ contents: map[ports.WindowID]ports.SurfaceContent{},
92+ contentSeq: map[ports.WindowID]uint64{},
93+ damage: map[ports.WindowID][]ports.SeqDamage{},
94+ contentReady: make(chan struct{}, 1),
95+ }
96+ c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}
97+ var held []ports.SurfaceContent // readers retaining published histories
98+ var snapshots [][]ports.SeqDamage
99+ for range 3 * damageHistoryCap {
100+ s.emitContent(c, damage{full: true})
101+ got := s.contents[1]
102+ held = append(held, got)
103+ snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...))
104+ }
105+ for i, h := range held {
106+ // A published history never changes, nor can its owner append to it.
107+ if len(h.DamageHistory) != len(snapshots[i]) || cap(h.DamageHistory) != len(h.DamageHistory) {
108+ t.Fatalf("history %d: len %d cap %d", i, len(h.DamageHistory), cap(h.DamageHistory))
109+ }
110+ for j, d := range h.DamageHistory {
111+ if d.Seq != snapshots[i][j].Seq || d.Full != snapshots[i][j].Full {
112+ t.Fatalf("history %d changed at %d: %+v want %+v", i, j, d, snapshots[i][j])
113+ }
114+ }
115+ if n := len(h.DamageHistory); h.DamageHistory[n-1].Seq != h.Seq || n > damageHistory {
116+ t.Fatalf("history %d: %+v", i, h.DamageHistory)
117+ }
118+ }
119+ allocs := testing.AllocsPerRun(100, func() { s.emitContent(c, damage{full: true}) })
120+ if allocs != 0 {
121+ t.Errorf("emitContent: %.2f allocs, want 0 (amortized)", allocs)
122+ }
123+ t.Logf("emitContent: %.2f allocs", allocs)
124+}
125+
126+// A reader waiting for newer content is woken by the next publication.
127+func TestEmitContentWakesWaiter(t *testing.T) {
128+ s := &Server{
129+ channels: Channels{Contents: make(chan ports.SurfaceContent)},
130+ contents: map[ports.WindowID]ports.SurfaceContent{},
131+ contentSeq: map[ports.WindowID]uint64{},
132+ damage: map[ports.WindowID][]ports.SeqDamage{},
133+ contentReady: make(chan struct{}, 1),
134+ }
135+ s.contentMu.Lock()
136+ s.contentNotify = make(chan struct{})
137+ notify := s.contentNotify
138+ s.contentMu.Unlock()
139+ s.emitContent(ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}, damage{full: true})
140+ select {
141+ case <-notify:
142+ default:
143+ t.Fatal("waiter not woken")
144+ }
145+}
@@ -0,0 +1,145 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/stretchr/testify/mock"
9+)
10+
11+// The steady-state hot paths that run for every frame or commit allocate
12+// nothing (or one pooled object per damage-history generation).
13+
14+func TestReleaseHeldAllocations(t *testing.T) {
15+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
16+ s := &Server{outputs: []*output{out}, reports: map[string]ports.OutputPresented{}}
17+ reports := []ports.OutputPresented{{Output: "DP-2", Seen: map[ports.WindowID]uint64{1: 1}}}
18+ now := time.Unix(100, 0)
19+ s.releaseHeld(now, reports)
20+ if allocs := testing.AllocsPerRun(100, func() { s.releaseHeld(now, reports) }); allocs != 0 {
21+ t.Errorf("releaseHeld: %.1f allocs, want 0", allocs)
22+ }
23+}
24+
25+func TestCommitDamageAllocations(t *testing.T) {
26+ srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
27+ surf := &surface{server: srv}
28+ commit := func() {
29+ surf.Damage(nil, 0, 0, 4, 4)
30+ surf.DamageBuffer(nil, 1, 1, 2, 2)
31+ surf.queueUpdate()
32+ surf.queue[0].applyGraph()
33+ }
34+ commit()
35+ if allocs := testing.AllocsPerRun(100, commit); allocs != 0 {
36+ t.Errorf("damage requests + queueUpdate + apply: %.1f allocs, want 0", allocs)
37+ }
38+ // The queued update keeps its rects while the next commit collects its own.
39+ surf.Damage(nil, 0, 0, 1, 1)
40+ surf.queueUpdate()
41+ first := surf.queue[0]
42+ surf.Damage(nil, 5, 5, 2, 2)
43+ if len(first.damage) != 1 || first.damage[0].W != 1 || len(surf.next.damage) != 1 || surf.next.damage[0].W != 2 {
44+ t.Fatalf("damage shared between commits: queued %v pending %v", first.damage, surf.next.damage)
45+ }
46+ surf.queueUpdate()
47+ if second := surf.queue[1]; len(second.damage) != 1 || second.damage[0].W != 2 || first.damage[0].W != 1 {
48+ t.Fatalf("damage mixed up: %v %v", first.damage, second.damage)
49+ }
50+}
51+
52+// An explicit-sync commit moves its points into the update: the commit
53+// itself allocates no commitSync, and a pending one is not overwritten.
54+func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
55+ dev := newMocksyncobjDevice(t)
56+ dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil)
57+ srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
58+ sw, err := newSyncWaiter(func() {})
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+ defer sw.pipeR.Close()
63+ defer sw.close()
64+ srv.syncWait = sw
65+ tl := &timeline{dev: dev, handle: 7, alive: true}
66+ surf := &surface{server: srv, sync: &syncState{}}
67+ take := func(point uint64) {
68+ surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: point}, syncPoint{tl: tl, point: point + 1}
69+ surf.takeSyncPoints()
70+ surf.queueUpdate()
71+ }
72+ take(10)
73+ take(20)
74+ a, b := surf.queue[0], surf.queue[1]
75+ if a.sync == nil || b.sync == nil || a.sync == b.sync || a.sync.acquire.point != 10 || b.sync.acquire.point != 20 || surf.next.sync != nil {
76+ t.Fatalf("sync points mixed up: %+v %+v pending %+v", a.sync, b.sync, surf.next.sync)
77+ }
78+ // The commit's points live in the surface until queueUpdate: no heap
79+ // commitSync per commit (the mocked device's own allocations would drown
80+ // an AllocsPerRun count here).
81+ surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: 30}, syncPoint{tl: tl, point: 31}
82+ surf.takeSyncPoints()
83+ if surf.next.sync != &surf.syncScratch {
84+ t.Fatal("commit points not held in the surface scratch")
85+ }
86+}
87+
88+func TestEmitContentAllocations(t *testing.T) {
89+ s := &Server{
90+ channels: Channels{Contents: make(chan ports.SurfaceContent)},
91+ contents: map[ports.WindowID]ports.SurfaceContent{},
92+ contentSeq: map[ports.WindowID]uint64{},
93+ damage: map[ports.WindowID][]ports.SeqDamage{},
94+ contentReady: make(chan struct{}, 1),
95+ }
96+ c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}
97+ var held []ports.SurfaceContent // readers retaining published histories
98+ var snapshots [][]ports.SeqDamage
99+ for range 3 * damageHistoryCap {
100+ s.emitContent(c, damage{full: true})
101+ got := s.contents[1]
102+ held = append(held, got)
103+ snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...))
104+ }
105+ for i, h := range held {
106+ // A published history never changes, nor can its owner append to it.
107+ if len(h.DamageHistory) != len(snapshots[i]) || cap(h.DamageHistory) != len(h.DamageHistory) {
108+ t.Fatalf("history %d: len %d cap %d", i, len(h.DamageHistory), cap(h.DamageHistory))
109+ }
110+ for j, d := range h.DamageHistory {
111+ if d.Seq != snapshots[i][j].Seq || d.Full != snapshots[i][j].Full {
112+ t.Fatalf("history %d changed at %d: %+v want %+v", i, j, d, snapshots[i][j])
113+ }
114+ }
115+ if n := len(h.DamageHistory); h.DamageHistory[n-1].Seq != h.Seq || n > damageHistory {
116+ t.Fatalf("history %d: %+v", i, h.DamageHistory)
117+ }
118+ }
119+ allocs := testing.AllocsPerRun(100, func() { s.emitContent(c, damage{full: true}) })
120+ if allocs != 0 {
121+ t.Errorf("emitContent: %.2f allocs, want 0 (amortized)", allocs)
122+ }
123+ t.Logf("emitContent: %.2f allocs", allocs)
124+}
125+
126+// A reader waiting for newer content is woken by the next publication.
127+func TestEmitContentWakesWaiter(t *testing.T) {
128+ s := &Server{
129+ channels: Channels{Contents: make(chan ports.SurfaceContent)},
130+ contents: map[ports.WindowID]ports.SurfaceContent{},
131+ contentSeq: map[ports.WindowID]uint64{},
132+ damage: map[ports.WindowID][]ports.SeqDamage{},
133+ contentReady: make(chan struct{}, 1),
134+ }
135+ s.contentMu.Lock()
136+ s.contentNotify = make(chan struct{})
137+ notify := s.contentNotify
138+ s.contentMu.Unlock()
139+ s.emitContent(ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}, damage{full: true})
140+ select {
141+ case <-notify:
142+ default:
143+ t.Fatal("waiter not woken")
144+ }
145+}
M internal/adapters/wayland/fifo.go +15 -4
@@ -209,6 +209,7 @@ type update struct {
209209 deps []*update
210210 owner *surface
211211 implicit [4]*syncWait
212+ syncStore commitSync // storage of sync when the commit had explicit-sync points
212213 prev *update
213214 synced, bound bool
214215 xdg *xdgSurface
@@ -237,8 +238,8 @@ var updatePool sync.Pool
237238 func (u *update) releaseRef() {
238239 u.refs--
239240 if u.refs == 0 && u.retired {
240- deps := u.deps[:0]
241- *u = update{deps: deps}
241+ deps, dmg, bufDmg := u.deps[:0], u.damage[:0], u.bufDamage[:0]
242+ *u = update{deps: deps, damage: dmg, bufDamage: bufDmg}
242243 updatePool.Put(u)
243244 }
244245 }
@@ -257,7 +258,7 @@ func (u *update) retireUpdate() {
257258 dep.releaseRef()
258259 }
259260 if u.refs == 0 {
260- *u = update{deps: u.deps}
261+ *u = update{deps: u.deps, damage: u.damage[:0], bufDamage: u.bufDamage[:0]}
261262 updatePool.Put(u)
262263 }
263264 }
@@ -346,9 +347,19 @@ func (s *surface) queueUpdate() {
346347 if u == nil {
347348 u = new(update)
348349 }
349- deps := u.deps[:0]
350+ deps, dmg, bufDmg := u.deps[:0], u.damage[:0], u.bufDamage[:0]
350351 *u = s.takePending()
351352 u.deps = deps
353+ // The pooled update brings the capacity a retired commit's damage had;
354+ // the surface collects the next commit's rects in it. The update keeps
355+ // the rects of this commit until it retires.
356+ s.next.damage, s.next.bufDamage = dmg, bufDmg
357+ if u.sync != nil && u.sync == &s.syncScratch {
358+ // The update owns its points from here: the surface's scratch is
359+ // free for the next commit while this one waits in the queue.
360+ u.syncStore, s.syncScratch = s.syncScratch, commitSync{}
361+ u.sync = &u.syncStore
362+ }
352363 u.owner, u.synced = s, s.effectivelySynced()
353364 if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil {
354365 if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok {
@@ -209,6 +209,7 @@ type update struct {
209 deps []*update209 deps []*update
210 owner *surface210 owner *surface
211 implicit [4]*syncWait211 implicit [4]*syncWait
212+ syncStore commitSync // storage of sync when the commit had explicit-sync points
212 prev *update213 prev *update
213 synced, bound bool214 synced, bound bool
214 xdg *xdgSurface215 xdg *xdgSurface
@@ -237,8 +238,8 @@ var updatePool sync.Pool
237 func (u *update) releaseRef() {238 func (u *update) releaseRef() {
238 u.refs--239 u.refs--
239 if u.refs == 0 && u.retired {240 if u.refs == 0 && u.retired {
240- deps := u.deps[:0]241+ deps, dmg, bufDmg := u.deps[:0], u.damage[:0], u.bufDamage[:0]
241- *u = update{deps: deps}242+ *u = update{deps: deps, damage: dmg, bufDamage: bufDmg}
242 updatePool.Put(u)243 updatePool.Put(u)
243 }244 }
244 }245 }
@@ -257,7 +258,7 @@ func (u *update) retireUpdate() {
257 dep.releaseRef()258 dep.releaseRef()
258 }259 }
259 if u.refs == 0 {260 if u.refs == 0 {
260- *u = update{deps: u.deps}261+ *u = update{deps: u.deps, damage: u.damage[:0], bufDamage: u.bufDamage[:0]}
261 updatePool.Put(u)262 updatePool.Put(u)
262 }263 }
263 }264 }
@@ -346,9 +347,19 @@ func (s *surface) queueUpdate() {
346 if u == nil {347 if u == nil {
347 u = new(update)348 u = new(update)
348 }349 }
349- deps := u.deps[:0]350+ deps, dmg, bufDmg := u.deps[:0], u.damage[:0], u.bufDamage[:0]
350 *u = s.takePending()351 *u = s.takePending()
351 u.deps = deps352 u.deps = deps
353+ // The pooled update brings the capacity a retired commit's damage had;
354+ // the surface collects the next commit's rects in it. The update keeps
355+ // the rects of this commit until it retires.
356+ s.next.damage, s.next.bufDamage = dmg, bufDmg
357+ if u.sync != nil && u.sync == &s.syncScratch {
358+ // The update owns its points from here: the surface's scratch is
359+ // free for the next commit while this one waits in the queue.
360+ u.syncStore, s.syncScratch = s.syncScratch, commitSync{}
361+ u.sync = &u.syncStore
362+ }
352 u.owner, u.synced = s, s.effectivelySynced()363 u.owner, u.synced = s, s.effectivelySynced()
353 if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil {364 if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil {
354 if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok {365 if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok {
M internal/adapters/wayland/release.go +4 -1
@@ -95,7 +95,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
9595 for _, r := range reports {
9696 s.reports[r.Output] = r
9797 }
98- live := make([]ports.OutputPresented, 0, len(s.outputs))
98+ live := s.heldLive[:0]
9999 for name := range s.reports {
100100 if s.outputByNameExact(name) == nil {
101101 delete(s.reports, name)
@@ -104,6 +104,8 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
104104 for _, r := range s.reports {
105105 live = append(live, r)
106106 }
107+ // keepHeld only reads live: the scratch holds nothing across calls.
108+ s.heldLive = live
107109 kept := s.held[:0]
108110 for _, h := range s.held {
109111 if s.bufferReferenced(h.res) || keepHeld(h, live, now) {
@@ -114,6 +116,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
114116 }
115117 clear(s.held[len(kept):])
116118 s.held = kept
119+ clear(live) // the reports carry maps: do not retain them
117120 return len(s.held) > 0
118121 }
119122
@@ -95,7 +95,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
95 for _, r := range reports {95 for _, r := range reports {
96 s.reports[r.Output] = r96 s.reports[r.Output] = r
97 }97 }
98- live := make([]ports.OutputPresented, 0, len(s.outputs))98+ live := s.heldLive[:0]
99 for name := range s.reports {99 for name := range s.reports {
100 if s.outputByNameExact(name) == nil {100 if s.outputByNameExact(name) == nil {
101 delete(s.reports, name)101 delete(s.reports, name)
@@ -104,6 +104,8 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
104 for _, r := range s.reports {104 for _, r := range s.reports {
105 live = append(live, r)105 live = append(live, r)
106 }106 }
107+ // keepHeld only reads live: the scratch holds nothing across calls.
108+ s.heldLive = live
107 kept := s.held[:0]109 kept := s.held[:0]
108 for _, h := range s.held {110 for _, h := range s.held {
109 if s.bufferReferenced(h.res) || keepHeld(h, live, now) {111 if s.bufferReferenced(h.res) || keepHeld(h, live, now) {
@@ -114,6 +116,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
114 }116 }
115 clear(s.held[len(kept):])117 clear(s.held[len(kept):])
116 s.held = kept118 s.held = kept
119+ clear(live) // the reports carry maps: do not retain them
117 return len(s.held) > 0120 return len(s.held) > 0
118 }121 }
119 122
M internal/adapters/wayland/server.go +31 -12
@@ -164,10 +164,12 @@ type Server struct {
164164 frameReports []ports.OutputPresented
165165 // held buffers wait until no output reads them (release.go); reports
166166 // are the outputs' latest ports.OutputPresented.
167- held []heldBuffer
168- reports map[string]ports.OutputPresented
169- frames uint64
170- started time.Time
167+ held []heldBuffer
168+ // heldLive is releaseHeld's scratch (display goroutine only).
169+ heldLive []ports.OutputPresented
170+ reports map[string]ports.OutputPresented
171+ frames uint64
172+ started time.Time
171173 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
172174 // lastFlip is each output's latest page flip.
173175 fifoSurfaces map[*surface]struct{}
@@ -335,7 +337,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
335337 dataSources: map[*server.Resource]*clipSource{},
336338 primarySources: map[*server.Resource]*clipSource{},
337339 controlSources: map[*server.Resource]*clipSource{},
338- contentNotify: make(chan struct{}),
339340 regions: map[*server.Resource]*region{},
340341 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
341342 gestures: map[server.Client][]*gestureObject{},
@@ -570,6 +571,10 @@ func (s *Server) forwardOutputApplied(ctx context.Context) {
570571 // enough for a renderer target a few frames old.
571572 const damageHistory = 4
572573
574+// damageHistoryCap is the capacity of a history's backing array: one
575+// allocation serves damageHistoryCap-damageHistory+1 publications.
576+const damageHistoryCap = 8 * damageHistory
577+
573578 // forwardOutputFormats applies the outputs' scanout formats on the
574579 // display goroutine.
575580 func (s *Server) forwardOutputFormats(ctx context.Context) {
@@ -639,23 +644,34 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
639644 s.contentMu.Lock()
640645 s.contentSeq[c.ID]++
641646 c.Seq = s.contentSeq[c.ID]
642- // Copy-on-write: previous publications can still be in an output's hands.
647+ // Published histories are immutable: an output may hold one for as long
648+ // as it likes. The history grows inside one backing array, appended past
649+ // the end of every published slice (never over an element a reader can
650+ // see), and moves to a fresh array only when that one is full. Readers
651+ // get a capacity-clipped slice, so they cannot append into the spare.
643652 old := s.damage[c.ID]
644653 if len(old) >= damageHistory {
645654 old = old[len(old)-damageHistory+1:]
646655 }
647- h := make([]ports.SeqDamage, len(old)+1)
648- copy(h, old)
649- h[len(old)] = ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}
656+ if len(old) == cap(old) {
657+ grown := make([]ports.SeqDamage, len(old), damageHistoryCap)
658+ copy(grown, old)
659+ old = grown
660+ }
661+ h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
650662 if c.Empty() {
651663 delete(s.damage, c.ID)
652664 } else {
653665 s.damage[c.ID] = h
654666 }
655- c.DamageHistory = h
667+ c.DamageHistory = h[:len(h):len(h)]
656668 s.contents[c.ID] = c
657- close(s.contentNotify)
658- s.contentNotify = make(chan struct{})
669+ if s.contentNotify != nil {
670+ // Only forwardContents waits on it, and it creates the channel when
671+ // it does: no channel is allocated per commit.
672+ close(s.contentNotify)
673+ s.contentNotify = nil
674+ }
659675 s.contentMu.Unlock()
660676 select {
661677 case s.contentReady <- struct{}{}:
@@ -676,6 +692,9 @@ func (s *Server) forwardContents(ctx context.Context) {
676692 seq = s.contentSeq[id]
677693 break
678694 }
695+ if s.contentNotify == nil {
696+ s.contentNotify = make(chan struct{})
697+ }
679698 notify = s.contentNotify
680699 s.contentMu.Unlock()
681700 if !found {
@@ -164,10 +164,12 @@ type Server struct {
164 frameReports []ports.OutputPresented164 frameReports []ports.OutputPresented
165 // held buffers wait until no output reads them (release.go); reports165 // held buffers wait until no output reads them (release.go); reports
166 // are the outputs' latest ports.OutputPresented.166 // are the outputs' latest ports.OutputPresented.
167- held []heldBuffer167+ held []heldBuffer
168- reports map[string]ports.OutputPresented168+ // heldLive is releaseHeld's scratch (display goroutine only).
169- frames uint64169+ heldLive []ports.OutputPresented
170- started time.Time170+ reports map[string]ports.OutputPresented
171+ frames uint64
172+ started time.Time
171 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);173 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
172 // lastFlip is each output's latest page flip.174 // lastFlip is each output's latest page flip.
173 fifoSurfaces map[*surface]struct{}175 fifoSurfaces map[*surface]struct{}
@@ -335,7 +337,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
335 dataSources: map[*server.Resource]*clipSource{},337 dataSources: map[*server.Resource]*clipSource{},
336 primarySources: map[*server.Resource]*clipSource{},338 primarySources: map[*server.Resource]*clipSource{},
337 controlSources: map[*server.Resource]*clipSource{},339 controlSources: map[*server.Resource]*clipSource{},
338- contentNotify: make(chan struct{}),
339 regions: map[*server.Resource]*region{},340 regions: map[*server.Resource]*region{},
340 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},341 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
341 gestures: map[server.Client][]*gestureObject{},342 gestures: map[server.Client][]*gestureObject{},
@@ -570,6 +571,10 @@ func (s *Server) forwardOutputApplied(ctx context.Context) {
570 // enough for a renderer target a few frames old.571 // enough for a renderer target a few frames old.
571 const damageHistory = 4572 const damageHistory = 4
572 573
574+// damageHistoryCap is the capacity of a history's backing array: one
575+// allocation serves damageHistoryCap-damageHistory+1 publications.
576+const damageHistoryCap = 8 * damageHistory
577+
573 // forwardOutputFormats applies the outputs' scanout formats on the578 // forwardOutputFormats applies the outputs' scanout formats on the
574 // display goroutine.579 // display goroutine.
575 func (s *Server) forwardOutputFormats(ctx context.Context) {580 func (s *Server) forwardOutputFormats(ctx context.Context) {
@@ -639,23 +644,34 @@ func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
639 s.contentMu.Lock()644 s.contentMu.Lock()
640 s.contentSeq[c.ID]++645 s.contentSeq[c.ID]++
641 c.Seq = s.contentSeq[c.ID]646 c.Seq = s.contentSeq[c.ID]
642- // Copy-on-write: previous publications can still be in an output's hands.647+ // Published histories are immutable: an output may hold one for as long
648+ // as it likes. The history grows inside one backing array, appended past
649+ // the end of every published slice (never over an element a reader can
650+ // see), and moves to a fresh array only when that one is full. Readers
651+ // get a capacity-clipped slice, so they cannot append into the spare.
643 old := s.damage[c.ID]652 old := s.damage[c.ID]
644 if len(old) >= damageHistory {653 if len(old) >= damageHistory {
645 old = old[len(old)-damageHistory+1:]654 old = old[len(old)-damageHistory+1:]
646 }655 }
647- h := make([]ports.SeqDamage, len(old)+1)656+ if len(old) == cap(old) {
648- copy(h, old)657+ grown := make([]ports.SeqDamage, len(old), damageHistoryCap)
649- h[len(old)] = ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}658+ copy(grown, old)
659+ old = grown
660+ }
661+ h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
650 if c.Empty() {662 if c.Empty() {
651 delete(s.damage, c.ID)663 delete(s.damage, c.ID)
652 } else {664 } else {
653 s.damage[c.ID] = h665 s.damage[c.ID] = h
654 }666 }
655- c.DamageHistory = h667+ c.DamageHistory = h[:len(h):len(h)]
656 s.contents[c.ID] = c668 s.contents[c.ID] = c
657- close(s.contentNotify)669+ if s.contentNotify != nil {
658- s.contentNotify = make(chan struct{})670+ // Only forwardContents waits on it, and it creates the channel when
671+ // it does: no channel is allocated per commit.
672+ close(s.contentNotify)
673+ s.contentNotify = nil
674+ }
659 s.contentMu.Unlock()675 s.contentMu.Unlock()
660 select {676 select {
661 case s.contentReady <- struct{}{}:677 case s.contentReady <- struct{}{}:
@@ -676,6 +692,9 @@ func (s *Server) forwardContents(ctx context.Context) {
676 seq = s.contentSeq[id]692 seq = s.contentSeq[id]
677 break693 break
678 }694 }
695+ if s.contentNotify == nil {
696+ s.contentNotify = make(chan struct{})
697+ }
679 notify = s.contentNotify698 notify = s.contentNotify
680 s.contentMu.Unlock()699 s.contentMu.Unlock()
681 if !found {700 if !found {
M internal/adapters/wayland/surface.go +3 -0
@@ -33,6 +33,9 @@ type surface struct {
3333 committedViewport viewportState // value snapshot; no allocation on viewport commits
3434 // next is the state requested since the last commit (see pendingCommit).
3535 next pendingCommit
36+ // syncScratch holds next.sync between takeSyncPoints and queueUpdate,
37+ // which moves it into the update.
38+ syncScratch commitSync
3639 // bufferScale, transform and opaque are committed state. formatOpaque is
3740 // the current buffer's own opacity (x formats).
3841 bufferScale int
@@ -33,6 +33,9 @@ type surface struct {
33 committedViewport viewportState // value snapshot; no allocation on viewport commits33 committedViewport viewportState // value snapshot; no allocation on viewport commits
34 // next is the state requested since the last commit (see pendingCommit).34 // next is the state requested since the last commit (see pendingCommit).
35 next pendingCommit35 next pendingCommit
36+ // syncScratch holds next.sync between takeSyncPoints and queueUpdate,
37+ // which moves it into the update.
38+ syncScratch commitSync
36 // bufferScale, transform and opaque are committed state. formatOpaque is39 // bufferScale, transform and opaque are committed state. formatOpaque is
37 // the current buffer's own opacity (x formats).40 // the current buffer's own opacity (x formats).
38 bufferScale int41 bufferScale int
M internal/adapters/wayland/syncobj.go +4 -1
@@ -487,7 +487,10 @@ func (s *surface) takeSyncPoints() {
487487 if st == nil || !st.acquire.set() {
488488 return
489489 }
490- cs := &commitSync{acquire: st.acquire, release: st.release}
490+ // The commit's points wait in the surface until queueUpdate moves them
491+ // into the update: no allocation per commit.
492+ cs := &s.syncScratch
493+ *cs = commitSync{acquire: st.acquire, release: st.release}
491494 st.acquire, st.release = syncPoint{}, syncPoint{}
492495 cs.acquire.tl.uses++
493496 cs.release.tl.uses++
@@ -487,7 +487,10 @@ func (s *surface) takeSyncPoints() {
487 if st == nil || !st.acquire.set() {487 if st == nil || !st.acquire.set() {
488 return488 return
489 }489 }
490- cs := &commitSync{acquire: st.acquire, release: st.release}490+ // The commit's points wait in the surface until queueUpdate moves them
491+ // into the update: no allocation per commit.
492+ cs := &s.syncScratch
493+ *cs = commitSync{acquire: st.acquire, release: st.release}
491 st.acquire, st.release = syncPoint{}, syncPoint{}494 st.acquire, st.release = syncPoint{}, syncPoint{}
492 cs.acquire.tl.uses++495 cs.acquire.tl.uses++
493 cs.release.tl.uses++496 cs.release.tl.uses++

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