detached bfbfc09

fix(daemon): decouple preview wakes from target ack

commit
bfbfc090851f3e7308f4ff8c9f715aa37609e404
author
brice · · 2026-07-11 18:35 UTC
parent
f50c254
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +37 -10
M internal/usecase/daemon/render_coordinator.go +37 -10
@@ -109,7 +109,11 @@ type renderCoordinator struct {
109109 pending bool
110110 pendingReset bool
111111 pendingUrgent bool
112- coalesced int
112+ // pendingPreview is accounted separately from the target primary wake:
113+ // viewers may consume a coalesced target snapshot while that target is
114+ // still ACK-blocked.
115+ pendingPreview bool
116+ coalesced int
113117 // outputPressure carries recent bulk-burst pressure between completed
114118 // batches. It selects a bounded 8–16ms deadline without letting a busy
115119 // batch perpetually reset its own timer.
@@ -187,13 +191,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
187191 c.opts.onInvalidate(inv)
188192 }
189193 c.metrics.invalidations.Add(1)
190- wasPending, wasUrgent := c.pending, c.pendingUrgent
194+ wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview
191195 c.pending = true
192196 c.pendingReset = c.pendingReset || inv.reset
193197 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent
198+ c.pendingPreview = c.pendingPreview || c.previewWake != nil || len(c.previewWakes) != 0
194199 c.coalesced++
195- // Cap one normal deadline; an urgent transition may supersede it.
196- arm := !wasPending || (!wasUrgent && c.pendingUrgent)
200+ // Cap one normal deadline for each primary or preview batch. Once a
201+ // preview has been delivered while the target is ACK-blocked, a later
202+ // target transition needs its own deadline even though the primary remains
203+ // pending.
204+ arm := !wasPending || (!wasUrgent && c.pendingUrgent) || (!wasPreviewPending && c.pendingPreview)
197205 if arm {
198206 c.generation++
199207 c.cancelNormalTimerLocked()
@@ -360,6 +368,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) {
360368 if c.attachment == ac {
361369 c.attachment = nil
362370 c.pending = false
371+ c.pendingPreview = false
363372 c.generation++
364373 c.armed = false
365374 c.cancelNormalTimerLocked()
@@ -375,6 +384,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {
375384 if c.attachment == old {
376385 c.attachment = replacement
377386 c.pending = false
387+ c.pendingPreview = false
378388 c.generation++
379389 c.armed = false
380390 c.cancelNormalTimerLocked()
@@ -394,6 +404,7 @@ func (c *renderCoordinator) noteSessionTeardown() {
394404 c.previewWake = nil
395405 c.previewWakes = nil
396406 c.pending = false
407+ c.pendingPreview = false
397408 c.generation++
398409 c.armed = false
399410 c.cancelNormalTimerLocked()
@@ -487,11 +498,13 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
487498 c.mu.Unlock()
488499 return
489500 }
501+ w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
502+ preview, previews := c.takePendingPreviewsLocked()
490503 if c.opts.ackReady != nil && !c.opts.ackReady() {
491504 c.mu.Unlock()
505+ c.notifyPreviews(w, preview, previews)
492506 return
493507 }
494- w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
495508 c.metrics.wakes.Add(1)
496509 c.metrics.coalesced.Add(uint64(w.coalesced))
497510 if !w.urgent {
@@ -506,15 +519,29 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
506519 }
507520 c.cancelNormalTimerLocked()
508521 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false
509- wake, preview := c.opts.wake, c.previewWake
510- previews := make([]func(renderWake), 0, len(c.previewWakes))
511- for _, fn := range c.previewWakes {
512- previews = append(previews, fn)
513- }
522+ wake := c.opts.wake
514523 c.mu.Unlock()
515524 if wake != nil {
516525 wake(w)
517526 }
527+ c.notifyPreviews(w, preview, previews)
528+}
529+
530+// takePendingPreviewsLocked snapshots and consumes the preview delivery for
531+// this target generation. Unlike the target primary, this is never ACK-gated.
532+func (c *renderCoordinator) takePendingPreviewsLocked() (func(renderWake), []func(renderWake)) {
533+ if !c.pendingPreview {
534+ return nil, nil
535+ }
536+ c.pendingPreview = false
537+ previews := make([]func(renderWake), 0, len(c.previewWakes))
538+ for _, fn := range c.previewWakes {
539+ previews = append(previews, fn)
540+ }
541+ return c.previewWake, previews
542+}
543+
544+func (c *renderCoordinator) notifyPreviews(w renderWake, preview func(renderWake), previews []func(renderWake)) {
518545 if preview != nil {
519546 preview(w)
520547 }
@@ -109,7 +109,11 @@ type renderCoordinator struct {
109 pending bool109 pending bool
110 pendingReset bool110 pendingReset bool
111 pendingUrgent bool111 pendingUrgent bool
112- coalesced int112+ // pendingPreview is accounted separately from the target primary wake:
113+ // viewers may consume a coalesced target snapshot while that target is
114+ // still ACK-blocked.
115+ pendingPreview bool
116+ coalesced int
113 // outputPressure carries recent bulk-burst pressure between completed117 // outputPressure carries recent bulk-burst pressure between completed
114 // batches. It selects a bounded 8–16ms deadline without letting a busy118 // batches. It selects a bounded 8–16ms deadline without letting a busy
115 // batch perpetually reset its own timer.119 // batch perpetually reset its own timer.
@@ -187,13 +191,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
187 c.opts.onInvalidate(inv)191 c.opts.onInvalidate(inv)
188 }192 }
189 c.metrics.invalidations.Add(1)193 c.metrics.invalidations.Add(1)
190- wasPending, wasUrgent := c.pending, c.pendingUrgent194+ wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview
191 c.pending = true195 c.pending = true
192 c.pendingReset = c.pendingReset || inv.reset196 c.pendingReset = c.pendingReset || inv.reset
193 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent197 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent
198+ c.pendingPreview = c.pendingPreview || c.previewWake != nil || len(c.previewWakes) != 0
194 c.coalesced++199 c.coalesced++
195- // Cap one normal deadline; an urgent transition may supersede it.200+ // Cap one normal deadline for each primary or preview batch. Once a
196- arm := !wasPending || (!wasUrgent && c.pendingUrgent)201+ // preview has been delivered while the target is ACK-blocked, a later
202+ // target transition needs its own deadline even though the primary remains
203+ // pending.
204+ arm := !wasPending || (!wasUrgent && c.pendingUrgent) || (!wasPreviewPending && c.pendingPreview)
197 if arm {205 if arm {
198 c.generation++206 c.generation++
199 c.cancelNormalTimerLocked()207 c.cancelNormalTimerLocked()
@@ -360,6 +368,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) {
360 if c.attachment == ac {368 if c.attachment == ac {
361 c.attachment = nil369 c.attachment = nil
362 c.pending = false370 c.pending = false
371+ c.pendingPreview = false
363 c.generation++372 c.generation++
364 c.armed = false373 c.armed = false
365 c.cancelNormalTimerLocked()374 c.cancelNormalTimerLocked()
@@ -375,6 +384,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {
375 if c.attachment == old {384 if c.attachment == old {
376 c.attachment = replacement385 c.attachment = replacement
377 c.pending = false386 c.pending = false
387+ c.pendingPreview = false
378 c.generation++388 c.generation++
379 c.armed = false389 c.armed = false
380 c.cancelNormalTimerLocked()390 c.cancelNormalTimerLocked()
@@ -394,6 +404,7 @@ func (c *renderCoordinator) noteSessionTeardown() {
394 c.previewWake = nil404 c.previewWake = nil
395 c.previewWakes = nil405 c.previewWakes = nil
396 c.pending = false406 c.pending = false
407+ c.pendingPreview = false
397 c.generation++408 c.generation++
398 c.armed = false409 c.armed = false
399 c.cancelNormalTimerLocked()410 c.cancelNormalTimerLocked()
@@ -487,11 +498,13 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
487 c.mu.Unlock()498 c.mu.Unlock()
488 return499 return
489 }500 }
501+ w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
502+ preview, previews := c.takePendingPreviewsLocked()
490 if c.opts.ackReady != nil && !c.opts.ackReady() {503 if c.opts.ackReady != nil && !c.opts.ackReady() {
491 c.mu.Unlock()504 c.mu.Unlock()
505+ c.notifyPreviews(w, preview, previews)
492 return506 return
493 }507 }
494- w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
495 c.metrics.wakes.Add(1)508 c.metrics.wakes.Add(1)
496 c.metrics.coalesced.Add(uint64(w.coalesced))509 c.metrics.coalesced.Add(uint64(w.coalesced))
497 if !w.urgent {510 if !w.urgent {
@@ -506,15 +519,29 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
506 }519 }
507 c.cancelNormalTimerLocked()520 c.cancelNormalTimerLocked()
508 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false521 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false
509- wake, preview := c.opts.wake, c.previewWake522+ wake := c.opts.wake
510- previews := make([]func(renderWake), 0, len(c.previewWakes))
511- for _, fn := range c.previewWakes {
512- previews = append(previews, fn)
513- }
514 c.mu.Unlock()523 c.mu.Unlock()
515 if wake != nil {524 if wake != nil {
516 wake(w)525 wake(w)
517 }526 }
527+ c.notifyPreviews(w, preview, previews)
528+}
529+
530+// takePendingPreviewsLocked snapshots and consumes the preview delivery for
531+// this target generation. Unlike the target primary, this is never ACK-gated.
532+func (c *renderCoordinator) takePendingPreviewsLocked() (func(renderWake), []func(renderWake)) {
533+ if !c.pendingPreview {
534+ return nil, nil
535+ }
536+ c.pendingPreview = false
537+ previews := make([]func(renderWake), 0, len(c.previewWakes))
538+ for _, fn := range c.previewWakes {
539+ previews = append(previews, fn)
540+ }
541+ return c.previewWake, previews
542+}
543+
544+func (c *renderCoordinator) notifyPreviews(w renderWake, preview func(renderWake), previews []func(renderWake)) {
518 if preview != nil {545 if preview != nil {
519 preview(w)546 preview(w)
520 }547 }

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