detached 4c53368

refactor(daemon): extract graphics namespace pool

commit
4c53368af4d9a34040302254fd6501d18cc50a9a
author
brice · · 2026-10-08 06:03 UTC
committer
brice · · 2026-10-08 08:12 UTC
parent
f04184f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +158 -117
M internal/usecase/daemon/daemon.go +26 -38
@@ -148,18 +148,9 @@ type Daemon struct {
148148 // re-suspension, detach, or session teardown can never be retired by a
149149 // stale timer. Guarded by mu.
150150 suspended map[*attachedClient]*suspendedAttachmentRetention
151- // graphicsNamespaces reserves deterministic, attachment/session-scoped Kitty
152- // ID blocks. Once a block may have reached an outer terminal it remains in
153- // this bounded table for the daemon lifetime: side-effect Output frames have
154- // no terminal ACK. Pool exhaustion disables graphics for new attachments and
155- // leaves their ordinary text output intact.
156- graphicsNamespaces map[uint64]struct{}
157- graphicsNamespaceFences map[uint64]uint64
158- graphicsNamespaceQuarantines map[uint64]*graphicsNamespaceQuarantine
159- // graphicsNamespaceSalt separates daemon lifetimes before attachment keys
160- // are hashed. Kitty IDs are terminal-global, so a restarted or neighboring
161- // daemon must not deterministically reopen the previous daemon's block.
162- graphicsNamespaceSalt uint64
151+ // graphics reserves attachment/session-scoped Kitty ID blocks. Guarded by
152+ // mu; see graphicsNamespacePool.
153+ graphics graphicsNamespacePool
163154
164155 attnMu sync.Mutex
165156 animFrame int
@@ -708,32 +699,29 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
708699 }
709700 paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background())
710701 d := &Daemon{
711- sessions: make(map[domain.SessionID]*session),
712- inactive: make(map[string]inactiveSession),
713- creating: make(map[string]struct{}),
714- resume: newResumeCredentials(),
715- suspended: make(map[*attachedClient]*suspendedAttachmentRetention),
716- graphicsNamespaces: make(map[uint64]struct{}),
717- graphicsNamespaceFences: make(map[uint64]uint64),
718- graphicsNamespaceQuarantines: make(map[uint64]*graphicsNamespaceQuarantine),
719- graphicsNamespaceSalt: newGraphicsNamespaceSalt(),
720- paneProcessCtx: paneProcessCtx,
721- paneProcessCancel: paneProcessCancel,
722- ptys: ptys,
723- clock: clock,
724- log: log,
725- baseEnv: os.Environ(),
726- shell: defaultShellCommand,
727- dirOrHome: dirOrHome,
728- done: make(chan struct{}),
729- restoreDone: make(chan struct{}),
730- animWake: make(chan struct{}, 1),
731- snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity),
732- snapshotAdmitted: make(map[*snapshotCapture]struct{}),
733- snapshotWake: make(chan struct{}, 1),
734- notices: newNoticeCenter(),
735- resumeParkGrace: defaultResumeParkGrace,
736- suspendedSafetyExpiry: defaultSuspendedSafetyExpiry,
702+ sessions: make(map[domain.SessionID]*session),
703+ inactive: make(map[string]inactiveSession),
704+ creating: make(map[string]struct{}),
705+ resume: newResumeCredentials(),
706+ suspended: make(map[*attachedClient]*suspendedAttachmentRetention),
707+ graphics: newGraphicsNamespacePool(),
708+ paneProcessCtx: paneProcessCtx,
709+ paneProcessCancel: paneProcessCancel,
710+ ptys: ptys,
711+ clock: clock,
712+ log: log,
713+ baseEnv: os.Environ(),
714+ shell: defaultShellCommand,
715+ dirOrHome: dirOrHome,
716+ done: make(chan struct{}),
717+ restoreDone: make(chan struct{}),
718+ animWake: make(chan struct{}, 1),
719+ snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity),
720+ snapshotAdmitted: make(map[*snapshotCapture]struct{}),
721+ snapshotWake: make(chan struct{}, 1),
722+ notices: newNoticeCenter(),
723+ resumeParkGrace: defaultResumeParkGrace,
724+ suspendedSafetyExpiry: defaultSuspendedSafetyExpiry,
737725 barScripts: &barScriptState{
738726 cfg: barConfigFromDomain(domain.Defaults().Bar),
739727 outputs: make(map[domain.SessionID]barScriptOutputs),
@@ -148,18 +148,9 @@ type Daemon struct {
148 // re-suspension, detach, or session teardown can never be retired by a148 // re-suspension, detach, or session teardown can never be retired by a
149 // stale timer. Guarded by mu.149 // stale timer. Guarded by mu.
150 suspended map[*attachedClient]*suspendedAttachmentRetention150 suspended map[*attachedClient]*suspendedAttachmentRetention
151- // graphicsNamespaces reserves deterministic, attachment/session-scoped Kitty151+ // graphics reserves attachment/session-scoped Kitty ID blocks. Guarded by
152- // ID blocks. Once a block may have reached an outer terminal it remains in152+ // mu; see graphicsNamespacePool.
153- // this bounded table for the daemon lifetime: side-effect Output frames have153+ graphics graphicsNamespacePool
154- // no terminal ACK. Pool exhaustion disables graphics for new attachments and
155- // leaves their ordinary text output intact.
156- graphicsNamespaces map[uint64]struct{}
157- graphicsNamespaceFences map[uint64]uint64
158- graphicsNamespaceQuarantines map[uint64]*graphicsNamespaceQuarantine
159- // graphicsNamespaceSalt separates daemon lifetimes before attachment keys
160- // are hashed. Kitty IDs are terminal-global, so a restarted or neighboring
161- // daemon must not deterministically reopen the previous daemon's block.
162- graphicsNamespaceSalt uint64
163 154
164 attnMu sync.Mutex155 attnMu sync.Mutex
165 animFrame int156 animFrame int
@@ -708,32 +699,29 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
708 }699 }
709 paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background())700 paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background())
710 d := &Daemon{701 d := &Daemon{
711- sessions: make(map[domain.SessionID]*session),702+ sessions: make(map[domain.SessionID]*session),
712- inactive: make(map[string]inactiveSession),703+ inactive: make(map[string]inactiveSession),
713- creating: make(map[string]struct{}),704+ creating: make(map[string]struct{}),
714- resume: newResumeCredentials(),705+ resume: newResumeCredentials(),
715- suspended: make(map[*attachedClient]*suspendedAttachmentRetention),706+ suspended: make(map[*attachedClient]*suspendedAttachmentRetention),
716- graphicsNamespaces: make(map[uint64]struct{}),707+ graphics: newGraphicsNamespacePool(),
717- graphicsNamespaceFences: make(map[uint64]uint64),708+ paneProcessCtx: paneProcessCtx,
718- graphicsNamespaceQuarantines: make(map[uint64]*graphicsNamespaceQuarantine),709+ paneProcessCancel: paneProcessCancel,
719- graphicsNamespaceSalt: newGraphicsNamespaceSalt(),710+ ptys: ptys,
720- paneProcessCtx: paneProcessCtx,711+ clock: clock,
721- paneProcessCancel: paneProcessCancel,712+ log: log,
722- ptys: ptys,713+ baseEnv: os.Environ(),
723- clock: clock,714+ shell: defaultShellCommand,
724- log: log,715+ dirOrHome: dirOrHome,
725- baseEnv: os.Environ(),716+ done: make(chan struct{}),
726- shell: defaultShellCommand,717+ restoreDone: make(chan struct{}),
727- dirOrHome: dirOrHome,718+ animWake: make(chan struct{}, 1),
728- done: make(chan struct{}),719+ snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity),
729- restoreDone: make(chan struct{}),720+ snapshotAdmitted: make(map[*snapshotCapture]struct{}),
730- animWake: make(chan struct{}, 1),721+ snapshotWake: make(chan struct{}, 1),
731- snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity),722+ notices: newNoticeCenter(),
732- snapshotAdmitted: make(map[*snapshotCapture]struct{}),723+ resumeParkGrace: defaultResumeParkGrace,
733- snapshotWake: make(chan struct{}, 1),724+ suspendedSafetyExpiry: defaultSuspendedSafetyExpiry,
734- notices: newNoticeCenter(),
735- resumeParkGrace: defaultResumeParkGrace,
736- suspendedSafetyExpiry: defaultSuspendedSafetyExpiry,
737 barScripts: &barScriptState{725 barScripts: &barScriptState{
738 cfg: barConfigFromDomain(domain.Defaults().Bar),726 cfg: barConfigFromDomain(domain.Defaults().Bar),
739 outputs: make(map[domain.SessionID]barScriptOutputs),727 outputs: make(map[domain.SessionID]barScriptOutputs),
A internal/usecase/daemon/graphics_namespace_pool.go +116 -0
@@ -0,0 +1,116 @@
1+package daemon
2+
3+import (
4+ "crypto/sha256"
5+ "encoding/binary"
6+)
7+
8+type graphicsNamespaceQuarantine struct {
9+ base uint64
10+ fence uint64
11+}
12+
13+// graphicsNamespacePool reserves deterministic, attachment/session-scoped Kitty
14+// ID blocks. Once a block may have reached an outer terminal it remains in
15+// this bounded table for the daemon lifetime: side-effect Output frames have
16+// no terminal ACK. Pool exhaustion disables graphics for new attachments and
17+// leaves their ordinary text output intact.
18+//
19+// The pool is guarded by Daemon.mu and has no lock of its own: every method
20+// requires the caller to hold it. The zero value is usable; the maps are
21+// created lazily and a zero salt simply skips salting.
22+type graphicsNamespacePool struct {
23+ reserved map[uint64]struct{}
24+ fences map[uint64]uint64
25+ quarantines map[uint64]*graphicsNamespaceQuarantine
26+ // salt separates daemon lifetimes before attachment keys are hashed. Kitty
27+ // IDs are terminal-global, so a restarted or neighboring daemon must not
28+ // deterministically reopen the previous daemon's block.
29+ salt uint64
30+}
31+
32+func newGraphicsNamespacePool() graphicsNamespacePool {
33+ p := graphicsNamespacePool{salt: newGraphicsNamespaceSalt()}
34+ p.init()
35+ return p
36+}
37+
38+func (p *graphicsNamespacePool) init() {
39+ if p.reserved == nil {
40+ p.reserved = make(map[uint64]struct{})
41+ }
42+ if p.fences == nil {
43+ p.fences = make(map[uint64]uint64)
44+ }
45+ if p.quarantines == nil {
46+ p.quarantines = make(map[uint64]*graphicsNamespaceQuarantine)
47+ }
48+}
49+
50+func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) {
51+ p.init()
52+ hashInput := []byte(key)
53+ if p.salt != 0 {
54+ salt := make([]byte, 8, 8+len(key))
55+ binary.BigEndian.PutUint64(salt, p.salt)
56+ hashInput = append(salt, hashInput...)
57+ }
58+ digest := sha256.Sum256(hashInput)
59+ preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount
60+ for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ {
61+ block := (preferred + offset) % graphicsIDNamespaceCount
62+ if _, exists := p.reserved[block]; exists {
63+ continue
64+ }
65+ fence := nextGraphicsNamespaceFence()
66+ p.reserved[block] = struct{}{}
67+ p.fences[block] = fence
68+ return block*graphicsIDNamespaceSize + 1, fence
69+ }
70+ return 0, 0
71+}
72+
73+// releaseBase is the legacy base-only release. A quarantined block is never
74+// released by it.
75+func (p *graphicsNamespacePool) releaseBase(base uint64) {
76+ if base == 0 || base%graphicsIDNamespaceSize != 1 {
77+ return
78+ }
79+ block := (base - 1) / graphicsIDNamespaceSize
80+ if _, quarantined := p.quarantines[block]; quarantined {
81+ return
82+ }
83+ delete(p.reserved, block)
84+ delete(p.fences, block)
85+}
86+
87+func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) {
88+ if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
89+ return
90+ }
91+ block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
92+ if _, quarantined := p.quarantines[block]; quarantined {
93+ return
94+ }
95+ if current := p.fences[block]; current != 0 && current != state.namespaceFence {
96+ return
97+ }
98+ delete(p.reserved, block)
99+ delete(p.fences, block)
100+}
101+
102+func (p *graphicsNamespacePool) quarantine(state *graphicsOutputState) *graphicsNamespaceQuarantine {
103+ if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
104+ return nil
105+ }
106+ block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
107+ p.init()
108+ if _, exists := p.quarantines[block]; exists {
109+ return nil
110+ }
111+ p.reserved[block] = struct{}{}
112+ p.fences[block] = state.namespaceFence
113+ q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence}
114+ p.quarantines[block] = q
115+ return q
116+}
@@ -0,0 +1,116 @@
1+package daemon
2+
3+import (
4+ "crypto/sha256"
5+ "encoding/binary"
6+)
7+
8+type graphicsNamespaceQuarantine struct {
9+ base uint64
10+ fence uint64
11+}
12+
13+// graphicsNamespacePool reserves deterministic, attachment/session-scoped Kitty
14+// ID blocks. Once a block may have reached an outer terminal it remains in
15+// this bounded table for the daemon lifetime: side-effect Output frames have
16+// no terminal ACK. Pool exhaustion disables graphics for new attachments and
17+// leaves their ordinary text output intact.
18+//
19+// The pool is guarded by Daemon.mu and has no lock of its own: every method
20+// requires the caller to hold it. The zero value is usable; the maps are
21+// created lazily and a zero salt simply skips salting.
22+type graphicsNamespacePool struct {
23+ reserved map[uint64]struct{}
24+ fences map[uint64]uint64
25+ quarantines map[uint64]*graphicsNamespaceQuarantine
26+ // salt separates daemon lifetimes before attachment keys are hashed. Kitty
27+ // IDs are terminal-global, so a restarted or neighboring daemon must not
28+ // deterministically reopen the previous daemon's block.
29+ salt uint64
30+}
31+
32+func newGraphicsNamespacePool() graphicsNamespacePool {
33+ p := graphicsNamespacePool{salt: newGraphicsNamespaceSalt()}
34+ p.init()
35+ return p
36+}
37+
38+func (p *graphicsNamespacePool) init() {
39+ if p.reserved == nil {
40+ p.reserved = make(map[uint64]struct{})
41+ }
42+ if p.fences == nil {
43+ p.fences = make(map[uint64]uint64)
44+ }
45+ if p.quarantines == nil {
46+ p.quarantines = make(map[uint64]*graphicsNamespaceQuarantine)
47+ }
48+}
49+
50+func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) {
51+ p.init()
52+ hashInput := []byte(key)
53+ if p.salt != 0 {
54+ salt := make([]byte, 8, 8+len(key))
55+ binary.BigEndian.PutUint64(salt, p.salt)
56+ hashInput = append(salt, hashInput...)
57+ }
58+ digest := sha256.Sum256(hashInput)
59+ preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount
60+ for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ {
61+ block := (preferred + offset) % graphicsIDNamespaceCount
62+ if _, exists := p.reserved[block]; exists {
63+ continue
64+ }
65+ fence := nextGraphicsNamespaceFence()
66+ p.reserved[block] = struct{}{}
67+ p.fences[block] = fence
68+ return block*graphicsIDNamespaceSize + 1, fence
69+ }
70+ return 0, 0
71+}
72+
73+// releaseBase is the legacy base-only release. A quarantined block is never
74+// released by it.
75+func (p *graphicsNamespacePool) releaseBase(base uint64) {
76+ if base == 0 || base%graphicsIDNamespaceSize != 1 {
77+ return
78+ }
79+ block := (base - 1) / graphicsIDNamespaceSize
80+ if _, quarantined := p.quarantines[block]; quarantined {
81+ return
82+ }
83+ delete(p.reserved, block)
84+ delete(p.fences, block)
85+}
86+
87+func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) {
88+ if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
89+ return
90+ }
91+ block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
92+ if _, quarantined := p.quarantines[block]; quarantined {
93+ return
94+ }
95+ if current := p.fences[block]; current != 0 && current != state.namespaceFence {
96+ return
97+ }
98+ delete(p.reserved, block)
99+ delete(p.fences, block)
100+}
101+
102+func (p *graphicsNamespacePool) quarantine(state *graphicsOutputState) *graphicsNamespaceQuarantine {
103+ if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
104+ return nil
105+ }
106+ block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
107+ p.init()
108+ if _, exists := p.quarantines[block]; exists {
109+ return nil
110+ }
111+ p.reserved[block] = struct{}{}
112+ p.fences[block] = state.namespaceFence
113+ q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence}
114+ p.quarantines[block] = q
115+ return q
116+}
M internal/usecase/daemon/graphics_output.go +7 -70
@@ -5,7 +5,6 @@ import (
55 "compress/zlib"
66 "crypto/sha256"
77 "encoding/base64"
8- "encoding/binary"
98 "errors"
109 "fmt"
1110 "sort"
@@ -106,11 +105,6 @@ var (
106105 graphicsNamespaceFence atomic.Uint64
107106 )
108107
109-type graphicsNamespaceQuarantine struct {
110- base uint64
111- fence uint64
112-}
113-
114108 func nextGraphicsNamespaceFence() uint64 {
115109 fence := graphicsNamespaceFence.Add(1)
116110 if fence == 0 {
@@ -167,34 +161,7 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64
167161 if d == nil {
168162 return 0, 0
169163 }
170- if d.graphicsNamespaces == nil {
171- d.graphicsNamespaces = make(map[uint64]struct{})
172- }
173- if d.graphicsNamespaceFences == nil {
174- d.graphicsNamespaceFences = make(map[uint64]uint64)
175- }
176- if d.graphicsNamespaceQuarantines == nil {
177- d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine)
178- }
179- hashInput := []byte(key)
180- if d.graphicsNamespaceSalt != 0 {
181- salt := make([]byte, 8, 8+len(key))
182- binary.BigEndian.PutUint64(salt, d.graphicsNamespaceSalt)
183- hashInput = append(salt, hashInput...)
184- }
185- digest := sha256.Sum256(hashInput)
186- preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount
187- for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ {
188- block := (preferred + offset) % graphicsIDNamespaceCount
189- if _, exists := d.graphicsNamespaces[block]; exists {
190- continue
191- }
192- fence := nextGraphicsNamespaceFence()
193- d.graphicsNamespaces[block] = struct{}{}
194- d.graphicsNamespaceFences[block] = fence
195- return block*graphicsIDNamespaceSize + 1, fence
196- }
197- return 0, 0
164+ return d.graphics.reserveLease(key)
198165 }
199166
200167 func (d *Daemon) releaseGraphicsNamespace(base uint64) {
@@ -210,30 +177,17 @@ func (d *Daemon) releaseGraphicsNamespace(base uint64) {
210177 // already held. A quarantined block is intentionally not released by this
211178 // legacy base-only helper; only its fenced cleanup lifecycle may retire it.
212179 func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) {
213- if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 {
214- return
215- }
216- block := (base - 1) / graphicsIDNamespaceSize
217- if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined {
180+ if d == nil {
218181 return
219182 }
220- delete(d.graphicsNamespaces, block)
221- delete(d.graphicsNamespaceFences, block)
183+ d.graphics.releaseBase(base)
222184 }
223185
224186 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {
225- if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
226- return
227- }
228- block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
229- if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined {
230- return
231- }
232- if current := d.graphicsNamespaceFences[block]; current != 0 && current != state.namespaceFence {
187+ if d == nil {
233188 return
234189 }
235- delete(d.graphicsNamespaces, block)
236- delete(d.graphicsNamespaceFences, block)
190+ d.graphics.releaseLease(state)
237191 }
238192
239193 // quarantineGraphicsNamespaceLocked fences one exact namespace instance for
@@ -241,27 +195,10 @@ func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState)
241195 // completion must never release a block that may have reached an outer terminal.
242196 // The caller holds d.mu.
243197 func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine {
244- if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {
245- return nil
246- }
247- block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
248- if d.graphicsNamespaces == nil {
249- d.graphicsNamespaces = make(map[uint64]struct{})
250- }
251- if d.graphicsNamespaceFences == nil {
252- d.graphicsNamespaceFences = make(map[uint64]uint64)
253- }
254- if d.graphicsNamespaceQuarantines == nil {
255- d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine)
256- }
257- if _, exists := d.graphicsNamespaceQuarantines[block]; exists {
198+ if d == nil {
258199 return nil
259200 }
260- d.graphicsNamespaces[block] = struct{}{}
261- d.graphicsNamespaceFences[block] = state.namespaceFence
262- q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence}
263- d.graphicsNamespaceQuarantines[block] = q
264- return q
201+ return d.graphics.quarantine(state)
265202 }
266203
267204 func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState {
@@ -5,7 +5,6 @@ import (
5 "compress/zlib"5 "compress/zlib"
6 "crypto/sha256"6 "crypto/sha256"
7 "encoding/base64"7 "encoding/base64"
8- "encoding/binary"
9 "errors"8 "errors"
10 "fmt"9 "fmt"
11 "sort"10 "sort"
@@ -106,11 +105,6 @@ var (
106 graphicsNamespaceFence atomic.Uint64105 graphicsNamespaceFence atomic.Uint64
107 )106 )
108 107
109-type graphicsNamespaceQuarantine struct {
110- base uint64
111- fence uint64
112-}
113-
114 func nextGraphicsNamespaceFence() uint64 {108 func nextGraphicsNamespaceFence() uint64 {
115 fence := graphicsNamespaceFence.Add(1)109 fence := graphicsNamespaceFence.Add(1)
116 if fence == 0 {110 if fence == 0 {
@@ -167,34 +161,7 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64
167 if d == nil {161 if d == nil {
168 return 0, 0162 return 0, 0
169 }163 }
170- if d.graphicsNamespaces == nil {164+ return d.graphics.reserveLease(key)
171- d.graphicsNamespaces = make(map[uint64]struct{})
172- }
173- if d.graphicsNamespaceFences == nil {
174- d.graphicsNamespaceFences = make(map[uint64]uint64)
175- }
176- if d.graphicsNamespaceQuarantines == nil {
177- d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine)
178- }
179- hashInput := []byte(key)
180- if d.graphicsNamespaceSalt != 0 {
181- salt := make([]byte, 8, 8+len(key))
182- binary.BigEndian.PutUint64(salt, d.graphicsNamespaceSalt)
183- hashInput = append(salt, hashInput...)
184- }
185- digest := sha256.Sum256(hashInput)
186- preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount
187- for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ {
188- block := (preferred + offset) % graphicsIDNamespaceCount
189- if _, exists := d.graphicsNamespaces[block]; exists {
190- continue
191- }
192- fence := nextGraphicsNamespaceFence()
193- d.graphicsNamespaces[block] = struct{}{}
194- d.graphicsNamespaceFences[block] = fence
195- return block*graphicsIDNamespaceSize + 1, fence
196- }
197- return 0, 0
198 }165 }
199 166
200 func (d *Daemon) releaseGraphicsNamespace(base uint64) {167 func (d *Daemon) releaseGraphicsNamespace(base uint64) {
@@ -210,30 +177,17 @@ func (d *Daemon) releaseGraphicsNamespace(base uint64) {
210 // already held. A quarantined block is intentionally not released by this177 // already held. A quarantined block is intentionally not released by this
211 // legacy base-only helper; only its fenced cleanup lifecycle may retire it.178 // legacy base-only helper; only its fenced cleanup lifecycle may retire it.
212 func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) {179 func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) {
213- if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 {180+ if d == nil {
214- return
215- }
216- block := (base - 1) / graphicsIDNamespaceSize
217- if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined {
218 return181 return
219 }182 }
220- delete(d.graphicsNamespaces, block)183+ d.graphics.releaseBase(base)
221- delete(d.graphicsNamespaceFences, block)
222 }184 }
223 185
224 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {186 func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) {
225- if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {187+ if d == nil {
226- return
227- }
228- block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
229- if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined {
230- return
231- }
232- if current := d.graphicsNamespaceFences[block]; current != 0 && current != state.namespaceFence {
233 return188 return
234 }189 }
235- delete(d.graphicsNamespaces, block)190+ d.graphics.releaseLease(state)
236- delete(d.graphicsNamespaceFences, block)
237 }191 }
238 192
239 // quarantineGraphicsNamespaceLocked fences one exact namespace instance for193 // quarantineGraphicsNamespaceLocked fences one exact namespace instance for
@@ -241,27 +195,10 @@ func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState)
241 // completion must never release a block that may have reached an outer terminal.195 // completion must never release a block that may have reached an outer terminal.
242 // The caller holds d.mu.196 // The caller holds d.mu.
243 func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine {197 func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine {
244- if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 {198+ if d == nil {
245- return nil
246- }
247- block := (state.namespaceBase - 1) / graphicsIDNamespaceSize
248- if d.graphicsNamespaces == nil {
249- d.graphicsNamespaces = make(map[uint64]struct{})
250- }
251- if d.graphicsNamespaceFences == nil {
252- d.graphicsNamespaceFences = make(map[uint64]uint64)
253- }
254- if d.graphicsNamespaceQuarantines == nil {
255- d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine)
256- }
257- if _, exists := d.graphicsNamespaceQuarantines[block]; exists {
258 return nil199 return nil
259 }200 }
260- d.graphicsNamespaces[block] = struct{}{}201+ return d.graphics.quarantine(state)
261- d.graphicsNamespaceFences[block] = state.namespaceFence
262- q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence}
263- d.graphicsNamespaceQuarantines[block] = q
264- return q
265 }202 }
266 203
267 func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState {204 func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState {
M internal/usecase/daemon/graphics_output_test.go +9 -9
@@ -398,13 +398,13 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T)
398398 done: make(chan struct{}),
399399 }
400400 d.mu.Lock()
401- d.graphicsNamespaces[block] = struct{}{}
401+ d.graphics.reserved[block] = struct{}{}
402402 d.resume.parked[1] = parked
403403 d.mu.Unlock()
404404 d.expireParked(1, parked)
405405 require.Nil(t, parked.ac.output.graphicsOutput)
406406 d.mu.Lock()
407- _, reserved := d.graphicsNamespaces[block]
407+ _, reserved := d.graphics.reserved[block]
408408 d.mu.Unlock()
409409 require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace")
410410
@@ -417,12 +417,12 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T)
417417 output: attachmentOutputWithGraphics(failedState),
418418 }
419419 d.mu.Lock()
420- d.graphicsNamespaces[block] = struct{}{}
420+ d.graphics.reserved[block] = struct{}{}
421421 d.mu.Unlock()
422422 require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error")
423423 require.Nil(t, failed.output.graphicsOutput)
424424 d.mu.Lock()
425- _, reserved = d.graphicsNamespaces[block]
425+ _, reserved = d.graphics.reserved[block]
426426 d.mu.Unlock()
427427 require.True(t, reserved, "failed terminal cleanup must quarantine the namespace")
428428 }
@@ -453,7 +453,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterSocketSendSuccessWithoutTerminalF
453453 require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame")
454454
455455 d.mu.Lock()
456- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]
456+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
457457 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
458458 d.mu.Unlock()
459459 require.True(t, quarantined, "socket-send success cannot prove terminal cleanup")
@@ -491,7 +491,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterFinalDeleteAndParkExpiry(t *testi
491491 d.expireParked(1, parked)
492492
493493 d.mu.Lock()
494- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]
494+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
495495 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
496496 d.mu.Unlock()
497497 require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history")
@@ -502,7 +502,7 @@ func TestGraphicsNamespacePoolExhaustionFallsBackToText(t *testing.T) {
502502 d := newTestDaemon(t, nil, stubClock{})
503503 d.mu.Lock()
504504 for block := uint64(0); block < graphicsIDNamespaceCount; block++ {
505- d.graphicsNamespaces[block] = struct{}{}
505+ d.graphics.reserved[block] = struct{}{}
506506 }
507507 ac := d.prepareAttachedClientLocked(
508508 &session{sessionCore: sessionCore{id: "work"}},
@@ -552,7 +552,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) {
552552
553553 d.mu.Lock()
554554 newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key)
555- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]
555+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
556556 d.mu.Unlock()
557557 require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined")
558558 require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace")
@@ -575,7 +575,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) {
575575 require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10))
576576 require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment")
577577 d.mu.Lock()
578- _, retained := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]
578+ _, retained := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
579579 d.mu.Unlock()
580580 require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace")
581581 }
@@ -398,13 +398,13 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T)
398 done: make(chan struct{}),398 done: make(chan struct{}),
399 }399 }
400 d.mu.Lock()400 d.mu.Lock()
401- d.graphicsNamespaces[block] = struct{}{}401+ d.graphics.reserved[block] = struct{}{}
402 d.resume.parked[1] = parked402 d.resume.parked[1] = parked
403 d.mu.Unlock()403 d.mu.Unlock()
404 d.expireParked(1, parked)404 d.expireParked(1, parked)
405 require.Nil(t, parked.ac.output.graphicsOutput)405 require.Nil(t, parked.ac.output.graphicsOutput)
406 d.mu.Lock()406 d.mu.Lock()
407- _, reserved := d.graphicsNamespaces[block]407+ _, reserved := d.graphics.reserved[block]
408 d.mu.Unlock()408 d.mu.Unlock()
409 require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace")409 require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace")
410 410
@@ -417,12 +417,12 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T)
417 output: attachmentOutputWithGraphics(failedState),417 output: attachmentOutputWithGraphics(failedState),
418 }418 }
419 d.mu.Lock()419 d.mu.Lock()
420- d.graphicsNamespaces[block] = struct{}{}420+ d.graphics.reserved[block] = struct{}{}
421 d.mu.Unlock()421 d.mu.Unlock()
422 require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error")422 require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error")
423 require.Nil(t, failed.output.graphicsOutput)423 require.Nil(t, failed.output.graphicsOutput)
424 d.mu.Lock()424 d.mu.Lock()
425- _, reserved = d.graphicsNamespaces[block]425+ _, reserved = d.graphics.reserved[block]
426 d.mu.Unlock()426 d.mu.Unlock()
427 require.True(t, reserved, "failed terminal cleanup must quarantine the namespace")427 require.True(t, reserved, "failed terminal cleanup must quarantine the namespace")
428 }428 }
@@ -453,7 +453,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterSocketSendSuccessWithoutTerminalF
453 require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame")453 require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame")
454 454
455 d.mu.Lock()455 d.mu.Lock()
456- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]456+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
457 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)457 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
458 d.mu.Unlock()458 d.mu.Unlock()
459 require.True(t, quarantined, "socket-send success cannot prove terminal cleanup")459 require.True(t, quarantined, "socket-send success cannot prove terminal cleanup")
@@ -491,7 +491,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterFinalDeleteAndParkExpiry(t *testi
491 d.expireParked(1, parked)491 d.expireParked(1, parked)
492 492
493 d.mu.Lock()493 d.mu.Lock()
494- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]494+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
495 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)495 newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key)
496 d.mu.Unlock()496 d.mu.Unlock()
497 require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history")497 require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history")
@@ -502,7 +502,7 @@ func TestGraphicsNamespacePoolExhaustionFallsBackToText(t *testing.T) {
502 d := newTestDaemon(t, nil, stubClock{})502 d := newTestDaemon(t, nil, stubClock{})
503 d.mu.Lock()503 d.mu.Lock()
504 for block := uint64(0); block < graphicsIDNamespaceCount; block++ {504 for block := uint64(0); block < graphicsIDNamespaceCount; block++ {
505- d.graphicsNamespaces[block] = struct{}{}505+ d.graphics.reserved[block] = struct{}{}
506 }506 }
507 ac := d.prepareAttachedClientLocked(507 ac := d.prepareAttachedClientLocked(
508 &session{sessionCore: sessionCore{id: "work"}},508 &session{sessionCore: sessionCore{id: "work"}},
@@ -552,7 +552,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) {
552 552
553 d.mu.Lock()553 d.mu.Lock()
554 newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key)554 newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key)
555- _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]555+ _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
556 d.mu.Unlock()556 d.mu.Unlock()
557 require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined")557 require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined")
558 require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace")558 require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace")
@@ -575,7 +575,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) {
575 require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10))575 require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10))
576 require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment")576 require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment")
577 d.mu.Lock()577 d.mu.Lock()
578- _, retained := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize]578+ _, retained := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize]
579 d.mu.Unlock()579 d.mu.Unlock()
580 require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace")580 require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace")
581 }581 }

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 12

Tags 11

↑↓ moveenter openesc close