fix(daemon): decouple preview wakes from target ack
- commit
- bfbfc090851f3e7308f4ff8c9f715aa37609e404
- author
- brice · · 2026-07-11 18:35 UTC
- parent
- f50c254
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 109 | 109 | pending bool | |
| 110 | 110 | pendingReset bool | |
| 111 | 111 | 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 | |
| 113 | 117 | // outputPressure carries recent bulk-burst pressure between completed | |
| 114 | 118 | // batches. It selects a bounded 8–16ms deadline without letting a busy | |
| 115 | 119 | // batch perpetually reset its own timer. | |
| @@ -187,13 +191,17 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 187 | 191 | c.opts.onInvalidate(inv) | |
| 188 | 192 | } | |
| 189 | 193 | c.metrics.invalidations.Add(1) | |
| 190 | - | wasPending, wasUrgent := c.pending, c.pendingUrgent | |
| 194 | + | wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview | |
| 191 | 195 | c.pending = true | |
| 192 | 196 | c.pendingReset = c.pendingReset || inv.reset | |
| 193 | 197 | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | |
| 198 | + | c.pendingPreview = c.pendingPreview || c.previewWake != nil || len(c.previewWakes) != 0 | |
| 194 | 199 | 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) | |
| 197 | 205 | if arm { | |
| 198 | 206 | c.generation++ | |
| 199 | 207 | c.cancelNormalTimerLocked() | |
| @@ -360,6 +368,7 @@ func (c *renderCoordinator) noteDetach(ac *attachedClient) { | |||
| 360 | 368 | if c.attachment == ac { | |
| 361 | 369 | c.attachment = nil | |
| 362 | 370 | c.pending = false | |
| 371 | + | c.pendingPreview = false | |
| 363 | 372 | c.generation++ | |
| 364 | 373 | c.armed = false | |
| 365 | 374 | c.cancelNormalTimerLocked() | |
| @@ -375,6 +384,7 @@ func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) { | |||
| 375 | 384 | if c.attachment == old { | |
| 376 | 385 | c.attachment = replacement | |
| 377 | 386 | c.pending = false | |
| 387 | + | c.pendingPreview = false | |
| 378 | 388 | c.generation++ | |
| 379 | 389 | c.armed = false | |
| 380 | 390 | c.cancelNormalTimerLocked() | |
| @@ -394,6 +404,7 @@ func (c *renderCoordinator) noteSessionTeardown() { | |||
| 394 | 404 | c.previewWake = nil | |
| 395 | 405 | c.previewWakes = nil | |
| 396 | 406 | c.pending = false | |
| 407 | + | c.pendingPreview = false | |
| 397 | 408 | c.generation++ | |
| 398 | 409 | c.armed = false | |
| 399 | 410 | c.cancelNormalTimerLocked() | |
| @@ -487,11 +498,13 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |||
| 487 | 498 | c.mu.Unlock() | |
| 488 | 499 | return | |
| 489 | 500 | } | |
| 501 | + | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog} | |
| 502 | + | preview, previews := c.takePendingPreviewsLocked() | |
| 490 | 503 | if c.opts.ackReady != nil && !c.opts.ackReady() { | |
| 491 | 504 | c.mu.Unlock() | |
| 505 | + | c.notifyPreviews(w, preview, previews) | |
| 492 | 506 | return | |
| 493 | 507 | } | |
| 494 | - | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog} | |
| 495 | 508 | c.metrics.wakes.Add(1) | |
| 496 | 509 | c.metrics.coalesced.Add(uint64(w.coalesced)) | |
| 497 | 510 | if !w.urgent { | |
| @@ -506,15 +519,29 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |||
| 506 | 519 | } | |
| 507 | 520 | c.cancelNormalTimerLocked() | |
| 508 | 521 | 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 | |
| 514 | 523 | c.mu.Unlock() | |
| 515 | 524 | if wake != nil { | |
| 516 | 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 | 545 | if preview != nil { | |
| 519 | 546 | preview(w) | |
| 520 | 547 | } | |
| @@ -109,7 +109,11 @@ type renderCoordinator struct { | |||||
| 109 | pending bool | 109 | pending bool | ||
| 110 | pendingReset bool | 110 | pendingReset bool | ||
| 111 | pendingUrgent bool | 111 | 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 | |||
| 113 | // outputPressure carries recent bulk-burst pressure between completed | 117 | // outputPressure carries recent bulk-burst pressure between completed | ||
| 114 | // batches. It selects a bounded 8–16ms deadline without letting a busy | 118 | // 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.pendingUrgent | 194 | + | wasPending, wasUrgent, wasPreviewPending := c.pending, c.pendingUrgent, c.pendingPreview |
| 191 | c.pending = true | 195 | c.pending = true | ||
| 192 | c.pendingReset = c.pendingReset || inv.reset | 196 | c.pendingReset = c.pendingReset || inv.reset | ||
| 193 | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | 197 | 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 = nil | 369 | c.attachment = nil | ||
| 362 | c.pending = false | 370 | c.pending = false | ||
| 371 | + | c.pendingPreview = false | |||
| 363 | c.generation++ | 372 | c.generation++ | ||
| 364 | c.armed = false | 373 | 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 = replacement | 385 | c.attachment = replacement | ||
| 377 | c.pending = false | 386 | c.pending = false | ||
| 387 | + | c.pendingPreview = false | |||
| 378 | c.generation++ | 388 | c.generation++ | ||
| 379 | c.armed = false | 389 | c.armed = false | ||
| 380 | c.cancelNormalTimerLocked() | 390 | c.cancelNormalTimerLocked() | ||
| @@ -394,6 +404,7 @@ func (c *renderCoordinator) noteSessionTeardown() { | |||||
| 394 | c.previewWake = nil | 404 | c.previewWake = nil | ||
| 395 | c.previewWakes = nil | 405 | c.previewWakes = nil | ||
| 396 | c.pending = false | 406 | c.pending = false | ||
| 407 | + | c.pendingPreview = false | |||
| 397 | c.generation++ | 408 | c.generation++ | ||
| 398 | c.armed = false | 409 | 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 | return | 499 | 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 | return | 506 | 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, false | 521 | c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false | ||
| 509 | - | wake, preview := c.opts.wake, c.previewWake | 522 | + | 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 | } | ||