v0.8.1 1f54f61

render_coordinator.go RawHistoryFolder

1package daemon
2
3import (
4	"runtime"
5	"sync"
6	"time"
7
8	"github.com/bnema/vev/internal/ports"
9)
10
11type renderCoordinator struct {
12	mu   sync.Mutex
13	opts renderCoordinatorOptions
14
15	// Coalesced pending state: latest wins, reset stays sticky, and at most
16	// one deadline wake may be armed at a time (cap-1).
17	pending       bool
18	pendingReset  bool
19	pendingUrgent bool
20	// queueMarked spans the coordinator's actual pending interval, from first
21	// enqueue until fire consumes the coalesced work.
22	queueMarked      bool
23	queueCorrelation ports.RuntimeCorrelation
24	// pendingPreview is accounted separately from the attachment wake:
25	// viewers may consume a coalesced target snapshot while output is
26	// still ACK-blocked.
27	pendingPreview bool
28	// ackDeferred records that at least one attachment's render wake was
29	// blocked by output-window capacity. ACK notifications use ackDeferredFor
30	// to flush only the acknowledging attachment's work.
31	ackDeferred    bool
32	ackDeferredFor map[*attachedClient]bool
33	// pendingGeneration identifies the latest coalesced session mutation.
34	// delivered records which attachment incarnations have consumed it, so a
35	// blocked peer can catch up without replaying the wake to healthy peers.
36	pendingGeneration uint64
37	delivered         map[*attachedClient]uint64
38	// pendingShared is set by session-owned producers. Attachment-owned
39	// invalidations retain only their exact target lease here.
40	pendingShared  bool
41	pendingTargets map[*attachedClient]*attachmentLease
42	// ackBlocked retains the first rejected ACK-capacity probe until the
43	// matching consume or lifecycle closure releases that exact interval.
44	ackBlocked *ackBlockedSpan
45	// deadlineDue closes the handoff between a timer worker receiving its tick
46	// and fire observing unavailable ACK capacity.
47	deadlineDue bool
48	coalesced   int
49	// outputPressure carries recent bulk-burst pressure between completed
50	// batches. It selects a bounded 8–16ms deadline without letting a busy
51	// batch perpetually reset its own timer.
52	outputPressure int
53
54	// previewWakes tracks preview subscriptions by observer, so one watcher
55	// cannot replace another's live preview.
56	previewWakes map[previewObserver]previewSubscription
57
58	// attachments and leases are coordinator-owned lifecycle snapshots. Shared
59	// render work is not tied to any one attachment; leases only fence callbacks
60	// for the attachment that captured them.
61	attachments map[*attachedClient]struct{}
62	leases      map[*attachedClient]*attachmentLease
63	torndown    bool
64
65	resize resizeRequestMetadata
66	// Each lane owns one complete immutable worker token at a time.
67	resizeLane timerLane
68	retryLane  timerLane
69	metrics    renderCoordinatorBurstMetrics
70	// normalLane owns render invalidation deadlines. The supervisor is only
71	// waited by the session owner after terminal teardown has rejected future
72	// registrations.
73	normalLane timerLane
74	armed      bool
75	supervisor timerSupervisor
76	// syncBatches is keyed by the stable pane owner. Each pane owns an
77	// independent watchdog; composition remains gated until every batch ends.
78	syncBatches map[*pane]*syncBatch
79	// syncRegistryVersion changes for every batch identity mutation. fire
80	// snapshots it before evaluating visibility and retries if a producer
81	// publishes or completes a batch during that unlocked evaluation.
82	syncRegistryVersion uint64
83}
84
85// newRenderCoordinator constructs a coordinator bound to opts. It starts no
86// goroutine; deadline scheduling is armed lazily by invalidate.
87func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator {
88	return &renderCoordinator{opts: opts}
89}
90
91// detachNormalTimerLocked removes the complete current token. The caller may
92// stop it only after dropping c.mu and must never wait for its worker.
93func (c *renderCoordinator) detachNormalTimerLocked() *timerToken {
94	return c.normalLane.detachLocked()
95}
96
97type syncBatch struct {
98	generation uint64
99	// renderable is evaluated outside c.mu: tab/session visibility locks may be
100	// acquired by this predicate.
101	renderable func() bool
102	force      func()
103	lane       timerLane
104}
105
106func detachSyncBatchLocked(batch *syncBatch) *timerToken { return batch.lane.detachLocked() }
107
108func (c *renderCoordinator) detachSyncTimersLocked() []*timerToken {
109	workers := make([]*timerToken, 0, len(c.syncBatches))
110	for pane, batch := range c.syncBatches {
111		workers = append(workers, detachSyncBatchLocked(batch))
112		delete(c.syncBatches, pane)
113		c.syncRegistryVersion++
114	}
115	return workers
116}
117
118// invalidate publishes a coordinator-owned producer transition. Callers that
119// carry an attachment identity must use invalidateForAttachment instead.
120func (c *renderCoordinator) invalidate(inv renderInvalidation) bool {
121	return c.invalidateForAttachment(nil, inv)
122}
123
124// invalidateForAttachment publishes only while source is still registered.
125// This check shares c.mu with attachment lifecycle changes, making a stale
126// timer callback unable to enqueue work for a newer attachment incarnation.
127// A nil source represents an internal pane/session invalidation.
128func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) bool {
129	return c.invalidateForAttachmentAtResizeEpoch(source, 0, inv)
130}
131
132// invalidateForLease publishes an attachment-owned transition only for the
133// exact lifecycle incarnation that captured it.
134func (c *renderCoordinator) invalidateForLease(source *attachedClient, lease *attachmentLease, inv renderInvalidation) bool {
135	return c.invalidateForLeaseAtResizeEpoch(source, lease, 0, inv)
136}
137
138// invalidateForAttachmentAtResizeEpoch additionally requires epoch to remain
139// current when nonzero, so a completed resize cannot publish after a newer
140// request supersedes it.
141func (c *renderCoordinator) invalidateForAttachmentAtResizeEpoch(source *attachedClient, epoch uint64, inv renderInvalidation) bool {
142	return c.invalidateForLeaseAtResizeEpoch(source, nil, epoch, inv)
143}
144
145// renderInvalidationReservation is the in-memory half of an invalidation.
146// finish performs observer, timer, and callback work after ownership fences are
147// released.
148type renderInvalidationReservation struct {
149	coordinator      *renderCoordinator
150	invalidation     renderInvalidation
151	onInvalidate     func(renderInvalidation)
152	queueStart       bool
153	queueCorrelation ports.RuntimeCorrelation
154	observer         ports.RuntimeObserver
155	old              *timerToken
156	arm              bool
157	generation       uint64
158	delay            time.Duration
159	clock            ports.Clock
160}
161
162// reserveInvalidationForLeaseAtResizeEpoch publishes only coordinator memory.
163// It is safe inside a short pane-owner fence and performs no timer or observer
164// calls.
165func (c *renderCoordinator) reserveInvalidationForLeaseAtResizeEpoch(source *attachedClient, lease *attachmentLease, epoch uint64, inv renderInvalidation) (*renderInvalidationReservation, bool) {
166	c.mu.Lock()
167	defer c.mu.Unlock()
168	return c.reserveInvalidationForLeaseAtResizeEpochLocked(source, lease, epoch, inv)
169}
170
171func (c *renderCoordinator) reserveInvalidationForLeaseAtResizeEpochLocked(source *attachedClient, lease *attachmentLease, epoch uint64, inv renderInvalidation) (*renderInvalidationReservation, bool) {
172	if c.torndown || (lease != nil && (!c.leaseCurrentLocked(lease, true) || lease.attachment != source)) ||
173		(source != nil && !c.attachmentRegisteredLocked(source)) ||
174		(epoch != 0 && (c.resize.epoch != epoch || c.resize.source != source || (lease != nil && c.resize.lease != lease))) {
175		return nil, false
176	}
177	reservation := &renderInvalidationReservation{coordinator: c, invalidation: inv, onInvalidate: c.opts.onInvalidate}
178	c.metrics.invalidations.Add(1)
179	c.pendingGeneration++
180	if c.pendingGeneration == 0 {
181		c.pendingGeneration = 1
182	}
183	wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview
184	if !wasPending {
185		c.ackDeferred = false
186		c.ackDeferredFor = nil
187		c.deadlineDue = false
188		if c.opts.observer != nil {
189			c.queueMarked = true
190			c.queueCorrelation = ports.NewRuntimeCorrelation()
191			reservation.queueCorrelation = c.queueCorrelation
192			reservation.queueStart = true
193		}
194	}
195	c.pending = true
196	if source == nil {
197		c.pendingShared = true
198		c.pendingTargets = nil
199	} else if !c.pendingShared {
200		if c.pendingTargets == nil {
201			c.pendingTargets = make(map[*attachedClient]*attachmentLease)
202		}
203		targetLease := lease
204		if targetLease == nil {
205			targetLease = c.leases[source]
206		}
207		if targetLease != nil {
208			c.pendingTargets[source] = targetLease
209		}
210	}
211	c.pendingReset = c.pendingReset || inv.reset
212	c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent
213	c.pendingPreview = c.pendingPreview || len(c.previewWakes) != 0
214	c.coalesced++
215	reservation.arm = !wasPending || !c.armed || (!wasUrgent && c.pendingUrgent) || (!wasPreviewPending && c.pendingPreview) || c.deadlineDue
216	if reservation.arm {
217		_, reservation.old = c.normalLane.replaceLocked()
218	}
219	reservation.generation = c.normalLane.generation
220	reservation.delay = minOutputRenderDeadline + time.Duration(c.outputPressure)*time.Millisecond
221	if c.pendingUrgent {
222		reservation.delay = urgentRenderDeadline
223	}
224	reservation.clock = c.opts.clock
225	reservation.observer = c.opts.observer
226	c.armed = c.armed || reservation.arm
227	return reservation, true
228}
229
230func (r *renderInvalidationReservation) finish() {
231	if r == nil || r.coordinator == nil {
232		return
233	}
234	c := r.coordinator
235	if r.queueStart && r.observer != nil {
236		r.observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", r.queueCorrelation, ports.RuntimeQueueEnqueued, 0, true))
237	}
238	stopDetachedTimer(r.old)
239	if !r.arm || r.clock == nil {
240		if r.onInvalidate != nil {
241			r.onInvalidate(r.invalidation)
242		}
243		return
244	}
245	timer := r.clock.NewTimer(r.delay)
246	timerC := timer.C()
247	c.mu.Lock()
248	valid := !c.torndown && c.pending && c.normalLane.generation == r.generation
249	var token *timerToken
250	if valid && timerC != nil {
251		token = c.normalLane.publishLocked(r.generation, timer)
252		valid = token != nil
253		if valid {
254			c.supervisor.startLocked(token, timerC, func() {
255				c.markDeadlineDue(token)
256				c.fireFromTimer(token, false, true)
257			})
258		}
259	}
260	c.mu.Unlock()
261	if !valid {
262		timer.Stop()
263	} else if timerC == nil {
264		timer.Stop()
265		c.fire(r.generation, false, true)
266	}
267	if r.onInvalidate != nil {
268		r.onInvalidate(r.invalidation)
269	}
270	if valid && timerC != nil {
271		runtime.Gosched()
272	}
273}
274
275// invalidateForLeaseAtResizeEpoch is the attachment-callback variant: it
276// rejects a callback whose source pointer still matches but whose lifecycle
277// lease has been revoked.
278func (c *renderCoordinator) invalidateForLeaseAtResizeEpoch(source *attachedClient, lease *attachmentLease, epoch uint64, inv renderInvalidation) bool {
279	reservation, ok := c.reserveInvalidationForLeaseAtResizeEpoch(source, lease, epoch, inv)
280	if !ok {
281		return false
282	}
283	reservation.finish()
284	return true
285}
286
287// notifyAck reports that the client acknowledged an output state, releasing
288// at most one attachment's blocked wake. It never bypasses an unexpired normal
289// or urgent deadline.
290func (c *renderCoordinator) notifyAck() { c.notifyAckForLease(nil) }
291
292// notifyAckForLease prevents an acknowledgement captured by an obsolete active
293// frame from flushing another attachment's render queue. A nil lease is
294// reserved for coordinator-owned/direct test callers.
295func (c *renderCoordinator) notifyAckForLease(lease *attachmentLease) {
296	c.mu.Lock()
297	if lease != nil {
298		if !c.leaseCurrentLocked(lease, true) || !c.ackDeferredFor[lease.attachment] {
299			c.mu.Unlock()
300			return
301		}
302	} else if !c.ackDeferred && !c.deadlineDue {
303		c.mu.Unlock()
304		return
305	}
306	if !c.pending {
307		c.mu.Unlock()
308		return
309	}
310	gen := c.normalLane.generation
311	blocked := c.ackBlocked
312	c.ackBlocked = nil
313	c.mu.Unlock()
314	if blocked != nil {
315		blocked.finish(true)
316	}
317	c.fire(gen, false, false)
318}
319
320// retryCapacity restores only the attachment whose wake failed the final
321// capacity check. Readiness is an atomic observation, so paint may discover
322// backpressure after fire has marked the wake delivered. Requeueing the exact
323// wake generation keeps that stale result from clearing shared work and avoids
324// making a healthy peer wait for the blocked attachment.
325func (c *renderCoordinator) retryCapacity(w renderWake, lease *attachmentLease) {
326	if lease == nil || w.generation == 0 {
327		return
328	}
329	var queueStart bool
330	c.mu.Lock()
331	if !c.leaseCurrentLocked(lease, true) || c.pendingGeneration < w.generation {
332		c.mu.Unlock()
333		return
334	}
335	// A newer wake already attempted this lease; its callback owns any
336	// capacity retry. An older callback may still widen a newer targeted wake
337	// to include this lease when that newer wake never reached it.
338	if c.pendingGeneration > w.generation && lease.deliveredGeneration == c.pendingGeneration {
339		c.mu.Unlock()
340		return
341	}
342	if !c.pending {
343		c.pending = true
344		c.pendingReset = w.reset
345		c.pendingUrgent = w.urgent
346		c.pendingShared = false
347		c.pendingTargets = map[*attachedClient]*attachmentLease{lease.attachment: lease}
348		c.coalesced = max(1, w.coalesced)
349		c.deadlineDue = false
350		c.armed = false
351		if c.opts.observer != nil && !c.queueMarked {
352			c.queueMarked = true
353			c.queueCorrelation = ports.NewRuntimeCorrelation()
354			queueStart = true
355		}
356	} else {
357		c.pendingReset = c.pendingReset || w.reset
358		c.pendingUrgent = c.pendingUrgent || w.urgent
359		if !c.pendingShared {
360			if c.pendingTargets == nil {
361				c.pendingTargets = make(map[*attachedClient]*attachmentLease)
362			}
363			c.pendingTargets[lease.attachment] = lease
364		}
365	}
366	if c.delivered != nil {
367		delete(c.delivered, lease.attachment)
368	}
369	if c.ackDeferredFor == nil {
370		c.ackDeferredFor = make(map[*attachedClient]bool)
371	}
372	c.ackDeferredFor[lease.attachment] = true
373	c.ackDeferred = true
374	generation := c.pendingGeneration
375	queueCorrelation := c.queueCorrelation
376	observer := c.opts.observer
377	c.supervisor.startTaskLocked(func() { c.retryPendingGeneration(generation) })
378	c.mu.Unlock()
379	if queueStart && observer != nil {
380		observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", queueCorrelation, ports.RuntimeQueueEnqueued, 0, true))
381	}
382}
383
384func (c *renderCoordinator) retryPendingGeneration(generation uint64) {
385	c.mu.Lock()
386	if c.torndown || !c.pending || c.pendingGeneration != generation {
387		c.mu.Unlock()
388		return
389	}
390	gen, token := c.normalLane.generation, c.normalLane.token
391	c.mu.Unlock()
392	c.fireWithTimerToken(token, gen, false, false)
393}
394
395// noteSyncBegin records a synchronized-output batch for its stable pane and
396// arms that pane's watchdog. Overlapping pane batches are independent.
397// markDeadlineDue records a received timer tick before the worker probes ACK
398// readiness, so a concurrent ACK cannot miss an already-expired deadline.
399func (c *renderCoordinator) markDeadlineDue(token *timerToken) {
400	c.mu.Lock()
401	if c.fireValidLocked(token, token.generation, false) {
402		c.deadlineDue = true
403	}
404	c.mu.Unlock()
405}
406
407func (c *renderCoordinator) noteSyncBegin(p *pane, gen uint64, force ...func()) {
408	var renderable func() bool
409	if c.opts.syncRenderable != nil {
410		renderable = func() bool { return c.opts.syncRenderable(p) }
411	}
412	c.noteSyncBeginWithRenderability(p, gen, renderable, force...)
413}
414
415// syncTimerCleanup carries detached timer work across an enclosing pane or
416// coordinator critical section. finish never waits for a worker.
417type syncTimerCleanup struct{ tokens []*timerToken }
418
419func (cleanup *syncTimerCleanup) add(token *timerToken) {
420	if token != nil {
421		cleanup.tokens = append(cleanup.tokens, token)
422	}
423}
424
425func (cleanup *syncTimerCleanup) append(other syncTimerCleanup) {
426	cleanup.tokens = append(cleanup.tokens, other.tokens...)
427}
428
429func (cleanup syncTimerCleanup) finish() {
430	for _, token := range cleanup.tokens {
431		stopDetachedTimer(token)
432	}
433}
434
435// noteSyncBeginWithRenderability records lifecycle unconditionally while the
436// supplied predicate decides dynamically whether this batch gates composition.
437func (c *renderCoordinator) noteSyncBeginWithRenderability(p *pane, gen uint64, renderable func() bool, force ...func()) {
438	c.beginSyncBatchWithRenderability(p, gen, renderable, force...).finish()
439}
440
441// beginSyncBatchWithRenderability publishes a batch and returns every timer
442// detached while doing so. Callers that already own pane.mu defer finish until
443// after releasing the parsing fence.
444func (c *renderCoordinator) beginSyncBatchWithRenderability(p *pane, gen uint64, renderable func() bool, force ...func()) syncTimerCleanup {
445	var cleanup syncTimerCleanup
446	c.mu.Lock()
447	if c.torndown {
448		c.mu.Unlock()
449		return cleanup
450	}
451	if c.syncBatches == nil {
452		c.syncBatches = make(map[*pane]*syncBatch)
453	}
454	if previous := c.syncBatches[p]; previous != nil {
455		cleanup.add(detachSyncBatchLocked(previous))
456	}
457	batch := &syncBatch{generation: gen, renderable: renderable}
458	if len(force) != 0 {
459		batch.force = force[0]
460	}
461	c.syncBatches[p] = batch
462	c.syncRegistryVersion++
463	clock := c.opts.clock
464	c.mu.Unlock()
465	if clock == nil {
466		return cleanup
467	}
468
469	// Both timer operations are external. A reentrant callback may replace or
470	// remove batch; identity validation below rejects this stale timer.
471	timer := clock.NewTimer(maxSyncUpdateDuration)
472	timerC := timer.C()
473	c.mu.Lock()
474	valid := !c.torndown && c.syncBatches[p] == batch && batch.generation == gen && batch.lane.generation == 0
475	var token *timerToken
476	if valid && timerC != nil {
477		batch.lane.generation = gen
478		token = batch.lane.publishLocked(gen, timer)
479		valid = token != nil
480		if valid {
481			c.supervisor.startLocked(token, timerC, func() { c.runSyncWatchdog(p, batch, token) })
482		}
483	}
484	c.mu.Unlock()
485	if !valid || timerC == nil {
486		cleanup.add(&timerToken{timer: timer})
487	}
488	return cleanup
489}
490
491// detachSyncBatchGeneration removes only the exact pane generation. It returns
492// timer cleanup to the caller so no Timer method runs under an enclosing pane
493// or coordinator lock.
494func (c *renderCoordinator) detachSyncBatchGeneration(p *pane, gen uint64) syncTimerCleanup {
495	var cleanup syncTimerCleanup
496	c.mu.Lock()
497	batch := c.syncBatches[p]
498	if batch != nil && batch.generation == gen {
499		delete(c.syncBatches, p)
500		c.syncRegistryVersion++
501		cleanup.add(detachSyncBatchLocked(batch))
502	}
503	c.mu.Unlock()
504	return cleanup
505}
506
507func (c *renderCoordinator) runSyncWatchdog(p *pane, batch *syncBatch, token *timerToken) {
508	// Keep the batch registered while force runs. A concurrent render must
509	// continue to observe the synchronized-output gate until the VT has
510	// authoritatively closed the batch.
511	c.mu.Lock()
512	current := c.syncBatches[p]
513	valid := !c.torndown && current == batch && current.generation == token.generation && current.lane.token == token
514	c.mu.Unlock()
515	if !valid {
516		return
517	}
518	if batch.force != nil {
519		batch.force()
520	}
521
522	// force may synchronously end this batch, replace it with a newer
523	// generation, or tear down the coordinator. Remove only the exact
524	// snapshot that expired, then publish one urgent completion wake.
525	c.mu.Lock()
526	current = c.syncBatches[p]
527	valid = !c.torndown && current == batch && current.generation == token.generation && current.lane.token == token
528	var stopped *timerToken
529	if valid {
530		delete(c.syncBatches, p)
531		c.syncRegistryVersion++
532		stopped = detachSyncBatchLocked(batch)
533		if len(c.syncBatches) == 0 && c.pending {
534			c.pendingUrgent = true
535		}
536	}
537	c.mu.Unlock()
538	if valid {
539		stopDetachedTimer(stopped)
540		// Reevaluate the aggregate gate only after forcing this pane.
541		c.fireCurrent(true)
542	}
543}
544
545// noteSyncEnd records completion for exactly one pane batch. It only flushes
546// after the aggregate gate opens.
547func (c *renderCoordinator) noteSyncEnd(p *pane, gen uint64) {
548	// Visibility may acquire session/tab locks, so evaluate it outside c.mu.
549	c.mu.Lock()
550	batch := c.syncBatches[p]
551	if batch == nil || batch.generation != gen {
552		c.mu.Unlock()
553		return
554	}
555	renderable := batch.renderable
556	c.mu.Unlock()
557	wasRenderable := renderable == nil || renderable()
558	if c.removeSyncEnd(p, gen, wasRenderable) {
559		c.fireCurrent(false)
560	}
561}
562
563// removeSyncEnd removes a batch without invoking a predicate or callback. PTY
564// processing calls it while pane.mu is still held, so a deadline can never
565// compose the post-Write partial screen between mutation and publication.
566// The caller must invoke fireCurrent only after releasing pane.mu.
567func (c *renderCoordinator) removeSyncEnd(p *pane, gen uint64, renderable bool) bool {
568	c.mu.Lock()
569	batch := c.syncBatches[p]
570	if batch == nil || batch.generation != gen {
571		c.mu.Unlock()
572		return false
573	}
574	delete(c.syncBatches, p)
575	c.syncRegistryVersion++
576	worker := detachSyncBatchLocked(batch)
577	if renderable && len(c.syncBatches) == 0 && c.pending {
578		c.pendingUrgent = true
579	}
580	c.mu.Unlock()
581	stopDetachedTimer(worker)
582	return renderable
583}
584
585// noteSyncPaneRemoved releases a pane watchdog when pane lifecycle ends.
586func (c *renderCoordinator) noteSyncPaneRemoved(p *pane) {
587	c.mu.Lock()
588	var worker *timerToken
589	if batch := c.syncBatches[p]; batch != nil {
590		delete(c.syncBatches, p)
591		c.syncRegistryVersion++
592		worker = detachSyncBatchLocked(batch)
593	}
594	c.mu.Unlock()
595	stopDetachedTimer(worker)
596}
597
598// previewObserver identifies one preview subscription by pointer identity.
599// The coordinator never calls into it; it only keys the subscription, so any
600// pointer owned by the watcher (for example its watch state) qualifies.
601type previewObserver any
602
603type previewSubscription struct {
604	generation uint64
605	fn         func(renderWake)
606}
607
608// subscribePreviewFor installs the preview observer's wake callback. fn runs
609// without coordinator locks on every coalesced wake and must never block. A
610// newer generation wins, so a delayed subscription cannot replace it.
611func (c *renderCoordinator) subscribePreviewFor(observer previewObserver, generation uint64, fn func(renderWake)) bool {
612	if observer == nil || fn == nil {
613		return false
614	}
615	c.mu.Lock()
616	defer c.mu.Unlock()
617	if c.torndown {
618		return false
619	}
620	if current, ok := c.previewWakes[observer]; ok && current.generation > generation {
621		return false
622	}
623	if c.previewWakes == nil {
624		c.previewWakes = make(map[previewObserver]previewSubscription)
625	}
626	c.previewWakes[observer] = previewSubscription{generation: generation, fn: fn}
627	return true
628}
629
630// teardownPreviewFor removes observer's subscription only when it still
631// belongs to generation. A delayed teardown cannot clear a newer subscription.
632func (c *renderCoordinator) teardownPreviewFor(observer previewObserver, generation uint64) {
633	c.mu.Lock()
634	if current, ok := c.previewWakes[observer]; ok && current.generation == generation {
635		delete(c.previewWakes, observer)
636	}
637	c.mu.Unlock()
638}
639
640// hasPreviewSubscribers reports whether any presentation is currently
641// previewing this coordinator's tab. A headless tab stays renderable while a
642// preview subscriber watches it.
643func (c *renderCoordinator) hasPreviewSubscribers() bool {
644	if c == nil {
645		return false
646	}
647	c.mu.Lock()
648	defer c.mu.Unlock()
649	return len(c.previewWakes) != 0
650}
651
652func (c *renderCoordinator) attachmentRegisteredLocked(ac *attachedClient) bool {
653	if ac == nil {
654		return false
655	}
656	_, ok := c.attachments[ac]
657	return ok
658}
659
660func (c *renderCoordinator) installLeaseLocked(ac *attachedClient, ready bool) *attachmentLease {
661	if ac == nil {
662		return nil
663	}
664	if c.attachments == nil {
665		c.attachments = make(map[*attachedClient]struct{})
666	}
667	if c.leases == nil {
668		c.leases = make(map[*attachedClient]*attachmentLease)
669	}
670	c.attachments[ac] = struct{}{}
671	if lease := c.leases[ac]; lease != nil && lease.active {
672		lease.ready = ready
673		return lease
674	}
675	lease := &attachmentLease{attachment: ac, ready: ready, active: true}
676	c.leases[ac] = lease
677	if c.delivered != nil {
678		delete(c.delivered, ac)
679	}
680	if c.ackDeferredFor != nil {
681		delete(c.ackDeferredFor, ac)
682	}
683	return lease
684}
685
686// attachmentLease returns the exact currently published lease for ac. Welcome
687// continuations capture this before their transport write and revalidate it at
688// every side-effect boundary.
689func (c *renderCoordinator) attachmentLease(ac *attachedClient) *attachmentLease {
690	c.mu.Lock()
691	defer c.mu.Unlock()
692	if c.torndown {
693		return nil
694	}
695	lease := c.leases[ac]
696	if lease == nil || !lease.active {
697		return nil
698	}
699	return lease
700}
701
702func (c *renderCoordinator) leaseCurrent(lease *attachmentLease, ready bool) bool {
703	c.mu.Lock()
704	defer c.mu.Unlock()
705	return c.leaseCurrentLocked(lease, ready)
706}
707
708// leaseCurrentLocked is a short, non-reentrant effect admission check. It
709// must not be used to retain c.mu across input routing, transport operations,
710// or other arbitrary handler work.
711func (c *renderCoordinator) leaseCurrentLocked(lease *attachmentLease, ready bool) bool {
712	return !c.torndown && lease != nil && lease.active &&
713		c.leases[lease.attachment] == lease && (!ready || lease.ready)
714}
715
716// attach binds an already-handshaken internal attachment. Route/resume use
717// attachWithReadiness(..., false) until their Welcome frame is accepted.
718func (c *renderCoordinator) attach(ac *attachedClient) { c.attachWithReadiness(ac, true) }
719
720func (c *renderCoordinator) attachWithReadiness(ac *attachedClient, ready bool) *attachmentLease {
721	c.mu.Lock()
722	defer c.mu.Unlock()
723	return c.attachWithReadinessLocked(ac, ready)
724}
725
726// attachWithReadinessLocked publishes one attachment while the coordinator
727// lock is already held by an ordered transition publication.
728func (c *renderCoordinator) attachWithReadinessLocked(ac *attachedClient, ready bool) *attachmentLease {
729	if c.torndown {
730		return nil
731	}
732	return c.installLeaseLocked(ac, ready)
733}
734
735// rebindAttachmentWithReadinessLocked retires the previous lease before
736// publishing a new one for the same attachment object. A queued wake carries
737// the old lease pointer, so reusing it would let that wake paint a rebound
738// transport.
739func (c *renderCoordinator) rebindAttachmentWithReadinessLocked(ac *attachedClient, ready bool) *attachmentLease {
740	if c.torndown {
741		return nil
742	}
743	if lease := c.leases[ac]; lease != nil {
744		lease.cancelUIFenceLocked()
745		lease.active = false
746		delete(c.leases, ac)
747	}
748	return c.installLeaseLocked(ac, ready)
749}
750
751// markAttachmentReady completes only the captured attachment incarnation.
752func (c *renderCoordinator) markAttachmentReady(lease *attachmentLease) bool {
753	c.mu.Lock()
754	defer c.mu.Unlock()
755	if !c.leaseCurrentLocked(lease, false) {
756		return false
757	}
758	lease.ready = true
759	return true
760}
761
762// wakeCurrent validates an attachment-specific wake. Shared wakes have no
763// lease and remain valid while the coordinator is live; their paint callback
764// rechecks each attachment's connection fence.
765func (c *renderCoordinator) wakeCurrent(w renderWake) bool {
766	c.mu.Lock()
767	defer c.mu.Unlock()
768	return !c.torndown && (w.lease == nil || c.leaseCurrentLocked(w.lease, true))
769}
770
771type renderLifecycleCleanup struct {
772	tokens []*timerToken
773	// previews wake every torn-down preview observer once, so a watcher
774	// revalidates its target instead of waiting for its revalidation timer.
775	previews         []func(renderWake)
776	observer         ports.RuntimeObserver
777	queueCorrelation ports.RuntimeCorrelation
778	queueMarked      bool
779	ackBlocked       *ackBlockedSpan
780}
781
782func (cleanup renderLifecycleCleanup) finish() {
783	for _, token := range cleanup.tokens {
784		stopDetachedTimer(token)
785	}
786	for _, fn := range cleanup.previews {
787		fn(renderWake{})
788	}
789	if cleanup.queueMarked && cleanup.observer != nil {
790		cleanup.observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", cleanup.queueCorrelation, ports.RuntimeQueueDequeued, 0, false))
791	}
792	if cleanup.ackBlocked != nil {
793		cleanup.ackBlocked.finish(false)
794	}
795}
796
797// beginDetach invalidates pending work. It never waits for detached workers.
798func (c *renderCoordinator) beginDetach(ac *attachedClient) renderLifecycleCleanup {
799	c.mu.Lock()
800	cleanup := c.beginDetachLocked(ac)
801	c.mu.Unlock()
802	return cleanup
803}
804
805// beginDetachLocked is the prelocked lifecycle seam used when registry,
806// current-session, and lease publication must share one commit boundary.
807func (c *renderCoordinator) beginDetachLocked(ac *attachedClient) renderLifecycleCleanup {
808	if ac == nil || !c.attachmentRegisteredLocked(ac) {
809		return renderLifecycleCleanup{}
810	}
811	delete(c.attachments, ac)
812	if lease := c.leases[ac]; lease != nil {
813		lease.cancelUIFenceLocked()
814		lease.active = false
815		delete(c.leases, ac)
816	}
817	delete(c.delivered, ac)
818	delete(c.ackDeferredFor, ac)
819	// Shared pending output belongs to the session, not the detached
820	// attachment. Keep it alive while another attachment remains registered.
821	if len(c.attachments) != 0 {
822		var cleanup renderLifecycleCleanup
823		if c.resize.source == ac {
824			_, resizeTimer := c.resizeLane.replaceLocked()
825			_, retryTimer := c.retryLane.replaceLocked()
826			cleanup.tokens = []*timerToken{resizeTimer, retryTimer}
827		}
828		return cleanup
829	}
830	return c.resetAttachmentLifecycleLocked()
831}
832
833func (c *renderCoordinator) resetAttachmentLifecycleLocked() renderLifecycleCleanup {
834	c.pending = false
835	c.pendingReset = false
836	c.pendingUrgent = false
837	c.ackDeferred = false
838	c.ackDeferredFor = nil
839	c.delivered = nil
840	c.pendingPreview = false
841	c.pendingShared = false
842	c.pendingTargets = nil
843	c.coalesced = 0
844	_, timer := c.normalLane.replaceLocked()
845	c.armed = false
846	_, resizeTimer := c.resizeLane.replaceLocked()
847	_, retryTimer := c.retryLane.replaceLocked()
848	cleanup := renderLifecycleCleanup{
849		tokens:           []*timerToken{timer, resizeTimer, retryTimer},
850		observer:         c.opts.observer,
851		queueCorrelation: c.queueCorrelation,
852		queueMarked:      c.queueMarked,
853		ackBlocked:       c.ackBlocked,
854	}
855	c.queueMarked, c.ackBlocked = false, nil
856	return cleanup
857}
858
859// noteDetach is for callers that hold no outer lifecycle locks.
860func (c *renderCoordinator) noteDetach(ac *attachedClient) {
861	c.beginDetach(ac).finish()
862}
863
864// beginSessionTeardown is terminal phase one. It rejects future worker
865// registration and atomically detaches every complete lane token.
866func (c *renderCoordinator) beginSessionTeardown() renderLifecycleCleanup {
867	c.mu.Lock()
868	if c.torndown {
869		c.mu.Unlock()
870		return renderLifecycleCleanup{}
871	}
872	c.torndown = true
873	for ac, lease := range c.leases {
874		lease.cancelUIFenceLocked()
875		lease.active = false
876		delete(c.attachments, ac)
877	}
878	c.leases = nil
879	c.attachments = nil
880	previews := make([]func(renderWake), 0, len(c.previewWakes))
881	for _, subscription := range c.previewWakes {
882		previews = append(previews, subscription.fn)
883	}
884	c.previewWakes = nil
885	c.pending = false
886	_, resizeTimer := c.resizeLane.replaceLocked()
887	_, retryTimer := c.retryLane.replaceLocked()
888	c.ackDeferred = false
889	c.ackDeferredFor = nil
890	c.delivered = nil
891	c.pendingPreview = false
892	c.pendingShared = false
893	c.pendingTargets = nil
894	_, timer := c.normalLane.replaceLocked()
895	c.armed = false
896	workers := c.detachSyncTimersLocked()
897	observer, queueCorrelation, queueMarked := c.opts.observer, c.queueCorrelation, c.queueMarked
898	ackBlocked := c.ackBlocked
899	c.queueMarked, c.ackBlocked = false, nil
900	c.mu.Unlock()
901	return renderLifecycleCleanup{
902		tokens:           append([]*timerToken{timer, resizeTimer, retryTimer}, workers...),
903		previews:         previews,
904		observer:         observer,
905		queueCorrelation: queueCorrelation,
906		queueMarked:      queueMarked,
907		ackBlocked:       ackBlocked,
908	}
909}
910
911// waitForTimerWorkers is terminal phase two. Only the session owner invokes it,
912// after beginSessionTeardown has made future supervisor registration impossible.
913func (c *renderCoordinator) waitForTimerWorkers() { c.supervisor.wait() }
914
915// invalidateRender is the sole producer fan-in. In tests and transitional
916// headless paths without an attached coordinator it retains the old private
917// compositor; attached sessions always schedule through their coordinator.
918func (d *Daemon) invalidateRender(entry *session, ac *attachedClient, reset bool, producer string) {
919	if rc := attachmentRenderCoordinator(entry); rc != nil {
920		invalidation := renderInvalidation{class: invalidateUrgent, reset: reset, producer: producer}
921		if rc.invalidateForAttachment(ac, invalidation) {
922			return
923		}
924		// Additional attachments do not borrow another attachment's lease.
925		// Their shared mutation still schedules the session wake, whose fanout
926		// paints every live attachment with its own output state.
927		if ac != nil && attachmentRegistered(entry, ac) && ac.currentAttachmentSession() == entry {
928			if rc.invalidate(invalidation) {
929				return
930			}
931			d.paint(entry, ac, reset, nil)
932		}
933		return
934	}
935	if ac != nil {
936		d.paint(entry, ac, reset, nil)
937	}
938}
939
940// invalidateRenderNow publishes through the coordinator but immediately
941// flushes the wake when the client can accept state. Attach uses this path so
942// the required first full frame never depends on a debounce timer.
943func (d *Daemon) invalidateRenderNow(entry *session, ac *attachedClient, reset bool, producer string) {
944	if rc := attachmentRenderCoordinator(entry); rc != nil {
945		if rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, reset: reset, producer: producer}) {
946			rc.fireCurrent(false)
947			return
948		}
949		if ac != nil && attachmentRegistered(entry, ac) && ac.currentAttachmentSession() == entry {
950			d.paint(entry, ac, reset, nil)
951		}
952		return
953	}
954	if ac != nil {
955		d.paint(entry, ac, reset, nil)
956	}
957}
958
959func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) {
960	c.mu.Lock()
961	if !watchdog && c.normalLane.generation != gen {
962		c.mu.Unlock()
963		return
964	}
965	token := c.normalLane.token
966	c.mu.Unlock()
967	c.fireWithTimerToken(token, gen, watchdog, deadline)
968}
969
970// fireCurrentForLease makes the selected attachment's wake synchronous. The
971// handshake must not finish before that attachment's first output is emitted,
972// while other attachments remain independently asynchronous.
973func (c *renderCoordinator) fireCurrentForLease(lease *attachmentLease) {
974	c.mu.Lock()
975	gen := c.normalLane.generation
976	token := c.normalLane.token
977	c.mu.Unlock()
978	c.fireWithTimerTokenAndLease(token, gen, false, false, lease)
979}
980
981// fireFromTimer consumes a normal deadline using its exact immutable token.
982func (c *renderCoordinator) fireFromTimer(token *timerToken, watchdog, deadline bool) {
983	c.fireWithTimerToken(token, token.generation, watchdog, deadline)
984}
985
986func (c *renderCoordinator) fireWithTimerToken(token *timerToken, gen uint64, watchdog, deadline bool) {
987	c.fireWithTimerTokenAndLease(token, gen, watchdog, deadline, nil)
988}
989
990func (c *renderCoordinator) attachmentAckReady(ac *attachedClient) bool {
991	if c.opts.ackReadyFor != nil {
992		return c.opts.ackReadyFor(ac)
993	}
994	return true
995}
996
997func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen uint64, watchdog, deadline bool, syncLease *attachmentLease) {
998	// Readiness probes enter each attachment's send path, which may in turn
999	// read coordinator metadata. Never hold c.mu across those callbacks.
1000	c.mu.Lock()
1001	if !c.fireValidLocked(token, gen, watchdog) {
1002		c.mu.Unlock()
1003		return
1004	}
1005	if c.pendingGeneration == 0 {
1006		c.pendingGeneration = 1
1007	}
1008	// lease.ready is guarded by c.mu (markAttachmentReady writes it), so only
1009	// leases already ready under the lock are probed after unlocking.
1010	probe := make(map[*attachedClient]*attachmentLease, len(c.leases))
1011	for ac, lease := range c.leases {
1012		if lease != nil && lease.active && lease.ready {
1013			probe[ac] = lease
1014		}
1015	}
1016	c.mu.Unlock()
1017
1018	ready := make(map[*attachmentLease]bool, len(probe))
1019	for ac, lease := range probe {
1020		// Welcome holds attachment.sendMu while its transport Send is in
1021		// flight. Do not probe an incarnation before it is ready.
1022		ready[lease] = c.attachmentAckReady(ac)
1023	}
1024
1025	// The readiness callback can detach, replace, or publish a sync batch.
1026	// syncGateOpenLocked retries predicate evaluation on every registry change
1027	// and returns holding c.mu, so pending consumption has no intervening gap.
1028	c.mu.Lock()
1029	if !c.fireValidLocked(token, gen, watchdog) {
1030		c.mu.Unlock()
1031		return
1032	}
1033	if !c.syncGateOpenLocked() {
1034		c.mu.Unlock()
1035		return
1036	}
1037	if !c.fireValidLocked(token, gen, watchdog) {
1038		c.mu.Unlock()
1039		return
1040	}
1041
1042	// Re-snapshot after every external readiness probe. A replacement may have
1043	// retired one of the leases we probed while the callback was unlocked.
1044	leases := make(map[*attachedClient]*attachmentLease, len(c.leases))
1045	for ac, lease := range c.leases {
1046		if lease != nil && lease.active {
1047			leases[ac] = lease
1048		}
1049	}
1050	var attachmentLeases map[*attachedClient]*attachmentLease
1051	if len(leases) != 0 {
1052		attachmentLeases = make(map[*attachedClient]*attachmentLease, len(leases))
1053	}
1054	blockedByAck := false
1055	allDelivered := true
1056	if len(leases) != 0 {
1057		for ac, lease := range leases {
1058			if !c.pendingShared && len(c.pendingTargets) != 0 {
1059				targetLease, targeted := c.pendingTargets[ac]
1060				if !targeted || targetLease != lease {
1061					continue
1062				}
1063			}
1064			if c.delivered[ac] == c.pendingGeneration {
1065				continue
1066			}
1067			allDelivered = false
1068			if !lease.ready {
1069				continue
1070			}
1071			canReady, probed := ready[lease]
1072			if !probed {
1073				continue
1074			}
1075			if canReady {
1076				attachmentLeases[ac] = lease
1077				if c.ackDeferredFor != nil {
1078					delete(c.ackDeferredFor, ac)
1079				}
1080				continue
1081			}
1082			blockedByAck = true
1083			if c.ackDeferredFor == nil {
1084				c.ackDeferredFor = make(map[*attachedClient]bool)
1085			}
1086			c.ackDeferredFor[ac] = true
1087		}
1088	}
1089	// Without any attachment lease (headless sessions and coordinator-only
1090	// callers) nothing gates output, so a shared invalidation renders directly.
1091	leaselessOutput := len(leases) == 0 && (c.pendingShared || len(c.pendingTargets) == 0)
1092	outputReady := len(attachmentLeases) != 0 || leaselessOutput
1093	if len(leases) != 0 {
1094		for ac, lease := range leases {
1095			if !c.pendingShared && len(c.pendingTargets) != 0 {
1096				targetLease, targeted := c.pendingTargets[ac]
1097				if !targeted || targetLease != lease {
1098					continue
1099				}
1100			}
1101			if c.delivered[ac] != c.pendingGeneration {
1102				allDelivered = false
1103				break
1104			}
1105		}
1106	}
1107	if leaselessOutput {
1108		allDelivered = true
1109	}
1110	wakeGeneration := uint64(0)
1111	if len(leases) != 0 {
1112		wakeGeneration = c.pendingGeneration
1113	}
1114	w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, generation: wakeGeneration, attachmentLeases: attachmentLeases, watchdog: watchdog}
1115	if syncLease != nil {
1116		if selected, ok := attachmentLeases[syncLease.attachment]; ok && selected == syncLease {
1117			w.lease = syncLease
1118		}
1119	}
1120	previews := c.takePendingPreviewsLocked()
1121	worker := c.detachNormalTimerLocked()
1122	c.armed = false
1123
1124	var ackStart *ackBlockedSpan
1125	if blockedByAck && deadline && c.ackBlocked == nil && c.opts.observer != nil {
1126		// Install the span while coordinator state is owned. Its Start
1127		// publication runs unlocked, and its own handoff defers End until that
1128		// call has returned.
1129		ackStart = newACKBlockedSpan(c.opts.observer)
1130		c.ackBlocked = ackStart
1131	}
1132	c.ackDeferred = len(c.ackDeferredFor) != 0
1133	if !outputReady && !allDelivered {
1134		c.mu.Unlock()
1135		stopDetachedTimer(worker)
1136		if ackStart != nil {
1137			ackStart.publishStart()
1138		}
1139		c.notifyPreviews(w, previews)
1140		return
1141	}
1142
1143	if outputReady {
1144		for ac, lease := range attachmentLeases {
1145			if c.delivered == nil {
1146				c.delivered = make(map[*attachedClient]uint64)
1147			}
1148			c.delivered[ac] = c.pendingGeneration
1149			lease.deliveredGeneration = c.pendingGeneration
1150		}
1151	}
1152	if len(leases) != 0 {
1153		allDelivered = true
1154		for ac, lease := range leases {
1155			if !c.pendingShared && len(c.pendingTargets) != 0 {
1156				targetLease, targeted := c.pendingTargets[ac]
1157				if !targeted || targetLease != lease {
1158					continue
1159				}
1160			}
1161			if c.delivered[ac] != c.pendingGeneration {
1162				allDelivered = false
1163				break
1164			}
1165		}
1166	}
1167	complete := allDelivered || (len(leases) == 0 && outputReady)
1168	if complete {
1169		c.pending, c.pendingReset, c.pendingUrgent, c.ackDeferred, c.deadlineDue, c.coalesced, c.armed = false, false, false, false, false, 0, false
1170		c.ackDeferredFor = nil
1171		c.delivered = nil
1172		c.pendingShared = false
1173		c.pendingTargets = nil
1174	}
1175	if outputReady {
1176		c.metrics.wakes.Add(1)
1177		c.metrics.coalesced.Add(uint64(w.coalesced))
1178		if !w.urgent {
1179			if w.coalesced > 1 {
1180				c.outputPressure += w.coalesced - 1
1181				if c.outputPressure > int((maxOutputRenderDeadline-minOutputRenderDeadline)/time.Millisecond) {
1182					c.outputPressure = int((maxOutputRenderDeadline - minOutputRenderDeadline) / time.Millisecond)
1183				}
1184			} else if c.outputPressure > 0 {
1185				c.outputPressure--
1186			}
1187		}
1188	}
1189	observer, queueCorrelation, queueMarked := c.opts.observer, c.queueCorrelation, c.queueMarked
1190	ackBlocked := c.ackBlocked
1191	if complete {
1192		c.queueMarked, c.ackBlocked = false, nil
1193	}
1194	wake := c.opts.wake
1195	c.mu.Unlock()
1196	stopDetachedTimer(worker)
1197	if ackStart != nil {
1198		ackStart.publishStart()
1199	}
1200	if complete && queueMarked && observer != nil {
1201		observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", queueCorrelation, ports.RuntimeQueueDequeued, 0, true))
1202	}
1203	if complete && ackBlocked != nil {
1204		ackBlocked.finish(true)
1205	}
1206	if outputReady && wake != nil {
1207		wake(w)
1208	}
1209	c.notifyPreviews(w, previews)
1210}
1211
1212func (c *renderCoordinator) fireValidLocked(token *timerToken, gen uint64, watchdog bool) bool {
1213	if c.torndown || !c.pending || watchdog {
1214		return !c.torndown && c.pending
1215	}
1216	return c.normalLane.generation == gen && (token == nil || c.normalLane.token == token)
1217}
1218
1219// syncGateOpen evaluates pane visibility without c.mu, then reacquires and
1220// validates the complete batch identity set before accepting that result.
1221// syncGateOpen evaluates visibility outside c.mu and returns with c.mu held
1222// only when the registry version still matches the snapshot. The caller must
1223// consume pending state before unlocking, eliminating the gate-to-consume gap.
1224func (c *renderCoordinator) syncGateOpenLocked() bool {
1225	for {
1226		version := c.syncRegistryVersion
1227		batches := make([]*syncBatch, 0, len(c.syncBatches))
1228		for _, batch := range c.syncBatches {
1229			batches = append(batches, batch)
1230		}
1231		c.mu.Unlock()
1232
1233		gated := false
1234		for _, batch := range batches {
1235			if batch.renderable == nil || batch.renderable() {
1236				gated = true
1237				break
1238			}
1239		}
1240		if hook := c.opts.afterSyncGateEvaluated; hook != nil {
1241			hook()
1242		}
1243
1244		c.mu.Lock()
1245		if version == c.syncRegistryVersion {
1246			return !gated
1247		}
1248	}
1249}
1250
1251// takePendingPreviewsLocked snapshots and consumes picker preview delivery
1252// for this target generation. Preview callbacks are never ACK-gated.
1253func (c *renderCoordinator) takePendingPreviewsLocked() []func(renderWake) {
1254	if !c.pendingPreview {
1255		return nil
1256	}
1257	c.pendingPreview = false
1258	previews := make([]func(renderWake), 0, len(c.previewWakes))
1259	for _, subscription := range c.previewWakes {
1260		previews = append(previews, subscription.fn)
1261	}
1262	return previews
1263}
1264
1265func (c *renderCoordinator) notifyPreviews(w renderWake, previews []func(renderWake)) {
1266	for _, fn := range previews {
1267		fn(w)
1268	}
1269}

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