detached f02b08f

perf(core): store objects in mutex-guarded maps so recycled IDs do not allocate (#6)

commit
f02b08f059aa2943c856a60567e9cd8fe9d48272
author
bnema · · 2026-10-01 20:20 UTC
committer
GitHub · · 2026-10-01 20:20 UTC
parent
b45de7b
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.6.2
4 files · +74 -3
M alloc_test.go +15 -0
@@ -39,4 +39,19 @@ func TestNumericPathsDoNotAllocate(t *testing.T) {
3939 t.Fatalf("proxy received %d, want 42", p.value)
4040 }
4141 })
42+
43+ // A frame callback registers and retires one object per frame under a
44+ // recycled ID; the object tables must reuse their storage.
45+ t.Run("RegisterRecycledID", func(t *testing.T) {
46+ d := newDisplay(&chunkConn{})
47+ p := &BaseProxy{id: d.AllocateID(), context: d.context}
48+ cycle := func() {
49+ d.context.Register(p)
50+ d.context.Unregister(p)
51+ }
52+ cycle()
53+ if n := testing.AllocsPerRun(100, cycle); n != 0 {
54+ t.Fatalf("register/unregister allocated %v times per cycle, want 0", n)
55+ }
56+ })
4257 }
@@ -39,4 +39,19 @@ func TestNumericPathsDoNotAllocate(t *testing.T) {
39 t.Fatalf("proxy received %d, want 42", p.value)39 t.Fatalf("proxy received %d, want 42", p.value)
40 }40 }
41 })41 })
42+
43+ // A frame callback registers and retires one object per frame under a
44+ // recycled ID; the object tables must reuse their storage.
45+ t.Run("RegisterRecycledID", func(t *testing.T) {
46+ d := newDisplay(&chunkConn{})
47+ p := &BaseProxy{id: d.AllocateID(), context: d.context}
48+ cycle := func() {
49+ d.context.Register(p)
50+ d.context.Unregister(p)
51+ }
52+ cycle()
53+ if n := testing.AllocsPerRun(100, cycle); n != 0 {
54+ t.Fatalf("register/unregister allocated %v times per cycle, want 0", n)
55+ }
56+ })
42 }57 }
M client.go +2 -2
@@ -46,7 +46,7 @@ type Object interface {
4646 type Display struct {
4747 conn net.Conn
4848 unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49- objects sync.Map // map[uint32]Object
49+ objects objectTable[Object]
5050 nextID uint32
5151 idMu sync.Mutex
5252 freeIDs []uint32
@@ -608,7 +608,7 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
608608 }
609609 // For a zombie the descriptors were still consumed above so the stream
610610 // stays aligned; dispatchFrame closes them without running any handler.
611- return obj.(Object), ev, nil
611+ return obj, ev, nil
612612 }
613613
614614 type signatureKey struct {
@@ -46,7 +46,7 @@ type Object interface {
46 type Display struct {46 type Display struct {
47 conn net.Conn47 conn net.Conn
48 unix *net.UnixConn // set when conn is a Unix socket, nil otherwise48 unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49- objects sync.Map // map[uint32]Object49+ objects objectTable[Object]
50 nextID uint3250 nextID uint32
51 idMu sync.Mutex51 idMu sync.Mutex
52 freeIDs []uint3252 freeIDs []uint32
@@ -608,7 +608,7 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
608 }608 }
609 // For a zombie the descriptors were still consumed above so the stream609 // For a zombie the descriptors were still consumed above so the stream
610 // stays aligned; dispatchFrame closes them without running any handler.610 // stays aligned; dispatchFrame closes them without running any handler.
611- return obj.(Object), ev, nil611+ return obj, ev, nil
612 }612 }
613 613
614 type signatureKey struct {614 type signatureKey struct {
M core.go +1 -1
@@ -13,7 +13,7 @@ import (
1313 // Context provides a compatibility layer for wl.Context
1414 type Context struct {
1515 display *Display
16- proxies sync.Map // map[uint32]Proxy
16+ proxies objectTable[Proxy]
1717 closed atomic.Bool
1818 }
1919
@@ -13,7 +13,7 @@ import (
13 // Context provides a compatibility layer for wl.Context13 // Context provides a compatibility layer for wl.Context
14 type Context struct {14 type Context struct {
15 display *Display15 display *Display
16- proxies sync.Map // map[uint32]Proxy16+ proxies objectTable[Proxy]
17 closed atomic.Bool17 closed atomic.Bool
18 }18 }
19 19
A objects.go +56 -0
@@ -0,0 +1,56 @@
1+package wlturbo
2+
3+import "sync"
4+
5+// objectTable maps object IDs to objects. Unlike sync.Map, storing an ID that
6+// was deleted earlier reuses the map's storage, so the steady-state cycle of
7+// server-created objects (wl_callback per frame, recycled IDs) does not
8+// allocate.
9+type objectTable[V comparable] struct {
10+ mu sync.RWMutex
11+ m map[uint32]V
12+}
13+
14+func (t *objectTable[V]) Load(id uint32) (V, bool) {
15+ t.mu.RLock()
16+ v, ok := t.m[id]
17+ t.mu.RUnlock()
18+ return v, ok
19+}
20+
21+func (t *objectTable[V]) Store(id uint32, v V) {
22+ t.mu.Lock()
23+ if t.m == nil {
24+ t.m = make(map[uint32]V)
25+ }
26+ t.m[id] = v
27+ t.mu.Unlock()
28+}
29+
30+func (t *objectTable[V]) Delete(id uint32) {
31+ t.mu.Lock()
32+ delete(t.m, id)
33+ t.mu.Unlock()
34+}
35+
36+// CompareAndDelete deletes id only while it maps to old.
37+func (t *objectTable[V]) CompareAndDelete(id uint32, old V) bool {
38+ t.mu.Lock()
39+ defer t.mu.Unlock()
40+ if v, ok := t.m[id]; !ok || v != old {
41+ return false
42+ }
43+ delete(t.m, id)
44+ return true
45+}
46+
47+// CompareAndSwap replaces id's value only while it maps to old.
48+func (t *objectTable[V]) CompareAndSwap(id uint32, old, new V) bool {
49+ t.mu.Lock()
50+ defer t.mu.Unlock()
51+ if v, ok := t.m[id]; !ok || v != old {
52+ return false
53+ }
54+ t.m[id] = new
55+ return true
56+}
@@ -0,0 +1,56 @@
1+package wlturbo
2+
3+import "sync"
4+
5+// objectTable maps object IDs to objects. Unlike sync.Map, storing an ID that
6+// was deleted earlier reuses the map's storage, so the steady-state cycle of
7+// server-created objects (wl_callback per frame, recycled IDs) does not
8+// allocate.
9+type objectTable[V comparable] struct {
10+ mu sync.RWMutex
11+ m map[uint32]V
12+}
13+
14+func (t *objectTable[V]) Load(id uint32) (V, bool) {
15+ t.mu.RLock()
16+ v, ok := t.m[id]
17+ t.mu.RUnlock()
18+ return v, ok
19+}
20+
21+func (t *objectTable[V]) Store(id uint32, v V) {
22+ t.mu.Lock()
23+ if t.m == nil {
24+ t.m = make(map[uint32]V)
25+ }
26+ t.m[id] = v
27+ t.mu.Unlock()
28+}
29+
30+func (t *objectTable[V]) Delete(id uint32) {
31+ t.mu.Lock()
32+ delete(t.m, id)
33+ t.mu.Unlock()
34+}
35+
36+// CompareAndDelete deletes id only while it maps to old.
37+func (t *objectTable[V]) CompareAndDelete(id uint32, old V) bool {
38+ t.mu.Lock()
39+ defer t.mu.Unlock()
40+ if v, ok := t.m[id]; !ok || v != old {
41+ return false
42+ }
43+ delete(t.m, id)
44+ return true
45+}
46+
47+// CompareAndSwap replaces id's value only while it maps to old.
48+func (t *objectTable[V]) CompareAndSwap(id uint32, old, new V) bool {
49+ t.mu.Lock()
50+ defer t.mu.Unlock()
51+ if v, ok := t.m[id]; !ok || v != old {
52+ return false
53+ }
54+ t.m[id] = new
55+ return true
56+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close