detached 91e8bae

fix(daemon): preserve render deadline across ack

commit
91e8bae4162b7b30401e942f2825a388e338af23
author
brice · · 2026-07-11 18:55 UTC
parent
dfcd8eb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +25 -8
M internal/usecase/daemon/render_coordinator.go +25 -8
@@ -113,7 +113,10 @@ type renderCoordinator struct {
113113 // viewers may consume a coalesced target snapshot while that target is
114114 // still ACK-blocked.
115115 pendingPreview bool
116- coalesced int
116+ // ackDeferred records that an expired deadline or watchdog was blocked by
117+ // output-window capacity. ACK notifications may flush only this state.
118+ ackDeferred bool
119+ coalesced int
117120 // outputPressure carries recent bulk-burst pressure between completed
118121 // batches. It selects a bounded 8–16ms deadline without letting a busy
119122 // batch perpetually reset its own timer.
@@ -205,6 +208,9 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
205208 }
206209 c.metrics.invalidations.Add(1)
207210 wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview
211+ if !wasPending {
212+ c.ackDeferred = false
213+ }
208214 c.pending = true
209215 c.pendingReset = c.pendingReset || inv.reset
210216 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent
@@ -238,7 +244,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
238244 if timerC == nil {
239245 c.mu.Unlock()
240246 timer.Stop()
241- c.fire(gen, false)
247+ c.fire(gen, false, true)
242248 return
243249 }
244250 cancel := make(chan struct{})
@@ -249,7 +255,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
249255 defer close(done)
250256 select {
251257 case <-timerC:
252- c.fire(gen, false)
258+ c.fire(gen, false, true)
253259 case <-cancel:
254260 }
255261 }()
@@ -259,12 +265,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
259265 }
260266
261267 // notifyAck reports that the client acknowledged an output state, releasing
262-// at most one deferred wake.
268+// at most one deadline/watchdog-deferred wake. It never bypasses an unexpired
269+// normal or urgent deadline.
263270 func (c *renderCoordinator) notifyAck() {
264271 c.mu.Lock()
272+ if !c.ackDeferred || !c.pending {
273+ c.mu.Unlock()
274+ return
275+ }
265276 gen := c.generation
266277 c.mu.Unlock()
267- c.fire(gen, false)
278+ c.fire(gen, false, false)
268279 }
269280
270281 // noteSyncBegin records a synchronized-output batch for its stable pane and
@@ -401,6 +412,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) {
401412 if c.attachment == ac {
402413 c.attachment = nil
403414 c.pending = false
415+ c.ackDeferred = false
404416 c.pendingPreview = false
405417 c.generation++
406418 c.armed = false
@@ -417,6 +429,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {
417429 if c.attachment == old {
418430 c.attachment = replacement
419431 c.pending = false
432+ c.ackDeferred = false
420433 c.pendingPreview = false
421434 c.generation++
422435 c.armed = false
@@ -437,6 +450,7 @@ func (c *renderCoordinator) noteSessionTeardown() {
437450 c.previewWake = nil
438451 c.previewWakes = nil
439452 c.pending = false
453+ c.ackDeferred = false
440454 c.pendingPreview = false
441455 c.generation++
442456 c.armed = false
@@ -487,7 +501,7 @@ func (c *renderCoordinator) fireCurrent(watchdog bool) {
487501 c.mu.Lock()
488502 gen := c.generation
489503 c.mu.Unlock()
490- c.fire(gen, watchdog)
504+ c.fire(gen, watchdog, watchdog)
491505 }
492506
493507 // invalidateRender is the sole producer fan-in. In tests and transitional
@@ -521,7 +535,7 @@ func (d *Daemon) invalidateRenderNow(sess *session, ac *attachedClient, reset bo
521535 }
522536 }
523537
524-func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
538+func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) {
525539 c.mu.Lock()
526540 if c.torndown || !c.pending || (!watchdog && gen != c.generation) {
527541 c.mu.Unlock()
@@ -534,6 +548,9 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
534548 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
535549 preview, previews := c.takePendingPreviewsLocked()
536550 if c.opts.ackReady != nil && !c.opts.ackReady() {
551+ if deadline {
552+ c.ackDeferred = true
553+ }
537554 c.mu.Unlock()
538555 c.notifyPreviews(w, preview, previews)
539556 return
@@ -551,7 +568,7 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
551568 }
552569 }
553570 c.cancelNormalTimerLocked()
554- c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false
571+ c.pending, c.pendingReset, c.pendingUrgent, c.ackDeferred, c.coalesced, c.armed = false, false, false, false, 0, false
555572 wake := c.opts.wake
556573 c.mu.Unlock()
557574 if wake != nil {
@@ -113,7 +113,10 @@ type renderCoordinator struct {
113 // viewers may consume a coalesced target snapshot while that target is113 // viewers may consume a coalesced target snapshot while that target is
114 // still ACK-blocked.114 // still ACK-blocked.
115 pendingPreview bool115 pendingPreview bool
116- coalesced int116+ // ackDeferred records that an expired deadline or watchdog was blocked by
117+ // output-window capacity. ACK notifications may flush only this state.
118+ ackDeferred bool
119+ coalesced int
117 // outputPressure carries recent bulk-burst pressure between completed120 // outputPressure carries recent bulk-burst pressure between completed
118 // batches. It selects a bounded 8–16ms deadline without letting a busy121 // batches. It selects a bounded 8–16ms deadline without letting a busy
119 // batch perpetually reset its own timer.122 // batch perpetually reset its own timer.
@@ -205,6 +208,9 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
205 }208 }
206 c.metrics.invalidations.Add(1)209 c.metrics.invalidations.Add(1)
207 wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview210 wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview
211+ if !wasPending {
212+ c.ackDeferred = false
213+ }
208 c.pending = true214 c.pending = true
209 c.pendingReset = c.pendingReset || inv.reset215 c.pendingReset = c.pendingReset || inv.reset
210 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent216 c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent
@@ -238,7 +244,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
238 if timerC == nil {244 if timerC == nil {
239 c.mu.Unlock()245 c.mu.Unlock()
240 timer.Stop()246 timer.Stop()
241- c.fire(gen, false)247+ c.fire(gen, false, true)
242 return248 return
243 }249 }
244 cancel := make(chan struct{})250 cancel := make(chan struct{})
@@ -249,7 +255,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
249 defer close(done)255 defer close(done)
250 select {256 select {
251 case <-timerC:257 case <-timerC:
252- c.fire(gen, false)258+ c.fire(gen, false, true)
253 case <-cancel:259 case <-cancel:
254 }260 }
255 }()261 }()
@@ -259,12 +265,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
259 }265 }
260 266
261 // notifyAck reports that the client acknowledged an output state, releasing267 // notifyAck reports that the client acknowledged an output state, releasing
262-// at most one deferred wake.268+// at most one deadline/watchdog-deferred wake. It never bypasses an unexpired
269+// normal or urgent deadline.
263 func (c *renderCoordinator) notifyAck() {270 func (c *renderCoordinator) notifyAck() {
264 c.mu.Lock()271 c.mu.Lock()
272+ if !c.ackDeferred || !c.pending {
273+ c.mu.Unlock()
274+ return
275+ }
265 gen := c.generation276 gen := c.generation
266 c.mu.Unlock()277 c.mu.Unlock()
267- c.fire(gen, false)278+ c.fire(gen, false, false)
268 }279 }
269 280
270 // noteSyncBegin records a synchronized-output batch for its stable pane and281 // noteSyncBegin records a synchronized-output batch for its stable pane and
@@ -401,6 +412,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) {
401 if c.attachment == ac {412 if c.attachment == ac {
402 c.attachment = nil413 c.attachment = nil
403 c.pending = false414 c.pending = false
415+ c.ackDeferred = false
404 c.pendingPreview = false416 c.pendingPreview = false
405 c.generation++417 c.generation++
406 c.armed = false418 c.armed = false
@@ -417,6 +429,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {
417 if c.attachment == old {429 if c.attachment == old {
418 c.attachment = replacement430 c.attachment = replacement
419 c.pending = false431 c.pending = false
432+ c.ackDeferred = false
420 c.pendingPreview = false433 c.pendingPreview = false
421 c.generation++434 c.generation++
422 c.armed = false435 c.armed = false
@@ -437,6 +450,7 @@ func (c *renderCoordinator) noteSessionTeardown() {
437 c.previewWake = nil450 c.previewWake = nil
438 c.previewWakes = nil451 c.previewWakes = nil
439 c.pending = false452 c.pending = false
453+ c.ackDeferred = false
440 c.pendingPreview = false454 c.pendingPreview = false
441 c.generation++455 c.generation++
442 c.armed = false456 c.armed = false
@@ -487,7 +501,7 @@ func (c *renderCoordinator) fireCurrent(watchdog bool) {
487 c.mu.Lock()501 c.mu.Lock()
488 gen := c.generation502 gen := c.generation
489 c.mu.Unlock()503 c.mu.Unlock()
490- c.fire(gen, watchdog)504+ c.fire(gen, watchdog, watchdog)
491 }505 }
492 506
493 // invalidateRender is the sole producer fan-in. In tests and transitional507 // invalidateRender is the sole producer fan-in. In tests and transitional
@@ -521,7 +535,7 @@ func (d *Daemon) invalidateRenderNow(sess *session, ac *attachedClient, reset bo
521 }535 }
522 }536 }
523 537
524-func (c *renderCoordinator) fire(gen uint64, watchdog bool) {538+func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) {
525 c.mu.Lock()539 c.mu.Lock()
526 if c.torndown || !c.pending || (!watchdog && gen != c.generation) {540 if c.torndown || !c.pending || (!watchdog && gen != c.generation) {
527 c.mu.Unlock()541 c.mu.Unlock()
@@ -534,6 +548,9 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
534 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}548 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
535 preview, previews := c.takePendingPreviewsLocked()549 preview, previews := c.takePendingPreviewsLocked()
536 if c.opts.ackReady != nil && !c.opts.ackReady() {550 if c.opts.ackReady != nil && !c.opts.ackReady() {
551+ if deadline {
552+ c.ackDeferred = true
553+ }
537 c.mu.Unlock()554 c.mu.Unlock()
538 c.notifyPreviews(w, preview, previews)555 c.notifyPreviews(w, preview, previews)
539 return556 return
@@ -551,7 +568,7 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
551 }568 }
552 }569 }
553 c.cancelNormalTimerLocked()570 c.cancelNormalTimerLocked()
554- c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false571+ c.pending, c.pendingReset, c.pendingUrgent, c.ackDeferred, c.coalesced, c.armed = false, false, false, false, 0, false
555 wake := c.opts.wake572 wake := c.opts.wake
556 c.mu.Unlock()573 c.mu.Unlock()
557 if wake != nil {574 if wake != nil {

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