fix(daemon): preserve render deadline across ack
- commit
- 91e8bae4162b7b30401e942f2825a388e338af23
- author
- brice · · 2026-07-11 18:55 UTC
- parent
- dfcd8eb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 113 | 113 | // viewers may consume a coalesced target snapshot while that target is | |
| 114 | 114 | // still ACK-blocked. | |
| 115 | 115 | 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 | |
| 117 | 120 | // outputPressure carries recent bulk-burst pressure between completed | |
| 118 | 121 | // batches. It selects a bounded 8–16ms deadline without letting a busy | |
| 119 | 122 | // batch perpetually reset its own timer. | |
| @@ -205,6 +208,9 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 205 | 208 | } | |
| 206 | 209 | c.metrics.invalidations.Add(1) | |
| 207 | 210 | wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview | |
| 211 | + | if !wasPending { | |
| 212 | + | c.ackDeferred = false | |
| 213 | + | } | |
| 208 | 214 | c.pending = true | |
| 209 | 215 | c.pendingReset = c.pendingReset || inv.reset | |
| 210 | 216 | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | |
| @@ -238,7 +244,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 238 | 244 | if timerC == nil { | |
| 239 | 245 | c.mu.Unlock() | |
| 240 | 246 | timer.Stop() | |
| 241 | - | c.fire(gen, false) | |
| 247 | + | c.fire(gen, false, true) | |
| 242 | 248 | return | |
| 243 | 249 | } | |
| 244 | 250 | cancel := make(chan struct{}) | |
| @@ -249,7 +255,7 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 249 | 255 | defer close(done) | |
| 250 | 256 | select { | |
| 251 | 257 | case <-timerC: | |
| 252 | - | c.fire(gen, false) | |
| 258 | + | c.fire(gen, false, true) | |
| 253 | 259 | case <-cancel: | |
| 254 | 260 | } | |
| 255 | 261 | }() | |
| @@ -259,12 +265,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 259 | 265 | } | |
| 260 | 266 | ||
| 261 | 267 | // 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 | 270 | func (c *renderCoordinator) notifyAck() { | |
| 264 | 271 | c.mu.Lock() | |
| 272 | + | if !c.ackDeferred || !c.pending { | |
| 273 | + | c.mu.Unlock() | |
| 274 | + | return | |
| 275 | + | } | |
| 265 | 276 | gen := c.generation | |
| 266 | 277 | c.mu.Unlock() | |
| 267 | - | c.fire(gen, false) | |
| 278 | + | c.fire(gen, false, false) | |
| 268 | 279 | } | |
| 269 | 280 | ||
| 270 | 281 | // noteSyncBegin records a synchronized-output batch for its stable pane and | |
| @@ -401,6 +412,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) { | |||
| 401 | 412 | if c.attachment == ac { | |
| 402 | 413 | c.attachment = nil | |
| 403 | 414 | c.pending = false | |
| 415 | + | c.ackDeferred = false | |
| 404 | 416 | c.pendingPreview = false | |
| 405 | 417 | c.generation++ | |
| 406 | 418 | c.armed = false | |
| @@ -417,6 +429,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) { | |||
| 417 | 429 | if c.attachment == old { | |
| 418 | 430 | c.attachment = replacement | |
| 419 | 431 | c.pending = false | |
| 432 | + | c.ackDeferred = false | |
| 420 | 433 | c.pendingPreview = false | |
| 421 | 434 | c.generation++ | |
| 422 | 435 | c.armed = false | |
| @@ -437,6 +450,7 @@ func (c *renderCoordinator) noteSessionTeardown() { | |||
| 437 | 450 | c.previewWake = nil | |
| 438 | 451 | c.previewWakes = nil | |
| 439 | 452 | c.pending = false | |
| 453 | + | c.ackDeferred = false | |
| 440 | 454 | c.pendingPreview = false | |
| 441 | 455 | c.generation++ | |
| 442 | 456 | c.armed = false | |
| @@ -487,7 +501,7 @@ func (c *renderCoordinator) fireCurrent(watchdog bool) { | |||
| 487 | 501 | c.mu.Lock() | |
| 488 | 502 | gen := c.generation | |
| 489 | 503 | c.mu.Unlock() | |
| 490 | - | c.fire(gen, watchdog) | |
| 504 | + | c.fire(gen, watchdog, watchdog) | |
| 491 | 505 | } | |
| 492 | 506 | ||
| 493 | 507 | // 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 | 539 | c.mu.Lock() | |
| 526 | 540 | if c.torndown || !c.pending || (!watchdog && gen != c.generation) { | |
| 527 | 541 | c.mu.Unlock() | |
| @@ -534,6 +548,9 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |||
| 534 | 548 | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog} | |
| 535 | 549 | preview, previews := c.takePendingPreviewsLocked() | |
| 536 | 550 | if c.opts.ackReady != nil && !c.opts.ackReady() { | |
| 551 | + | if deadline { | |
| 552 | + | c.ackDeferred = true | |
| 553 | + | } | |
| 537 | 554 | c.mu.Unlock() | |
| 538 | 555 | c.notifyPreviews(w, preview, previews) | |
| 539 | 556 | return | |
| @@ -551,7 +568,7 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |||
| 551 | 568 | } | |
| 552 | 569 | } | |
| 553 | 570 | 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 | |
| 555 | 572 | wake := c.opts.wake | |
| 556 | 573 | c.mu.Unlock() | |
| 557 | 574 | if wake != nil { | |
| @@ -113,7 +113,10 @@ type renderCoordinator struct { | |||||
| 113 | // viewers may consume a coalesced target snapshot while that target is | 113 | // viewers may consume a coalesced target snapshot while that target is | ||
| 114 | // still ACK-blocked. | 114 | // still ACK-blocked. | ||
| 115 | pendingPreview bool | 115 | 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 | |||
| 117 | // outputPressure carries recent bulk-burst pressure between completed | 120 | // outputPressure carries recent bulk-burst pressure between completed | ||
| 118 | // batches. It selects a bounded 8–16ms deadline without letting a busy | 121 | // 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.pendingPreview | 210 | wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview | ||
| 211 | + | if !wasPending { | |||
| 212 | + | c.ackDeferred = false | |||
| 213 | + | } | |||
| 208 | c.pending = true | 214 | c.pending = true | ||
| 209 | c.pendingReset = c.pendingReset || inv.reset | 215 | c.pendingReset = c.pendingReset || inv.reset | ||
| 210 | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | 216 | 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 | return | 248 | 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, releasing | 267 | // 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.generation | 276 | 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 and | 281 | // 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 = nil | 413 | c.attachment = nil | ||
| 403 | c.pending = false | 414 | c.pending = false | ||
| 415 | + | c.ackDeferred = false | |||
| 404 | c.pendingPreview = false | 416 | c.pendingPreview = false | ||
| 405 | c.generation++ | 417 | c.generation++ | ||
| 406 | c.armed = false | 418 | 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 = replacement | 430 | c.attachment = replacement | ||
| 419 | c.pending = false | 431 | c.pending = false | ||
| 432 | + | c.ackDeferred = false | |||
| 420 | c.pendingPreview = false | 433 | c.pendingPreview = false | ||
| 421 | c.generation++ | 434 | c.generation++ | ||
| 422 | c.armed = false | 435 | c.armed = false | ||
| @@ -437,6 +450,7 @@ func (c *renderCoordinator) noteSessionTeardown() { | |||||
| 437 | c.previewWake = nil | 450 | c.previewWake = nil | ||
| 438 | c.previewWakes = nil | 451 | c.previewWakes = nil | ||
| 439 | c.pending = false | 452 | c.pending = false | ||
| 453 | + | c.ackDeferred = false | |||
| 440 | c.pendingPreview = false | 454 | c.pendingPreview = false | ||
| 441 | c.generation++ | 455 | c.generation++ | ||
| 442 | c.armed = false | 456 | c.armed = false | ||
| @@ -487,7 +501,7 @@ func (c *renderCoordinator) fireCurrent(watchdog bool) { | |||||
| 487 | c.mu.Lock() | 501 | c.mu.Lock() | ||
| 488 | gen := c.generation | 502 | 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 transitional | 507 | // 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 | return | 556 | 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, false | 571 | + | c.pending, c.pendingReset, c.pendingUrgent, c.ackDeferred, c.coalesced, c.armed = false, false, false, false, 0, false |
| 555 | wake := c.opts.wake | 572 | wake := c.opts.wake | ||
| 556 | c.mu.Unlock() | 573 | c.mu.Unlock() | ||
| 557 | if wake != nil { | 574 | if wake != nil { | ||