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}