detached 3398032

fix(daemon): adapt coordinator render deadlines

commit
3398032cc9f96fca5221911cac4e49605cba6616
author
brice · · 2026-07-11 17:33 UTC
committer
brice · · 2026-07-11 17:33 UTC
parent
c1108a4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +33 -3
M internal/usecase/daemon/render.go +16 -1
@@ -2,6 +2,7 @@
22 package daemon
33
44 import (
5+ "bytes"
56 "errors"
67 "strconv"
78
@@ -14,6 +15,14 @@ import (
1415 "github.com/bnema/vev/pkg/vt"
1516 )
1617
18+// completedSynchronizedUpdate reports a full DEC 2026 batch contained in one
19+// PTY read. The VT's final state is inactive in this case, so it cannot be
20+// inferred from before/after SyncUpdateActive alone.
21+func completedSynchronizedUpdate(data []byte) bool {
22+ _, tail, found := bytes.Cut(data, []byte(renderer.SyncStartCSI))
23+ return found && bytes.Contains(tail, []byte(renderer.SyncEndCSI))
24+}
25+
1726 func signal(ch chan struct{}) {
1827 select {
1928 case ch <- struct{}{}:
@@ -46,6 +55,7 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) {
4655 p.screen.Write(data)
4756 p.refreshTerminalTitleLocked()
4857 isSyncing := p.screen.SyncUpdateActive()
58+ completeSyncRead := !wasSyncing && !isSyncing && completedSynchronizedUpdate(data)
4959 var syncGen uint64
5060 if wasSyncing != isSyncing {
5161 if isSyncing {
@@ -91,7 +101,12 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) {
91101 rc.noteSyncEnd(gen)
92102 }
93103 }
94- if !isSyncing && wasSyncing == isSyncing {
104+ if completeSyncRead {
105+ // The entire batch completed in this read. Its final screen state
106+ // is ready now, so bypass the bulk debounce without arming a
107+ // watchdog for a batch that no longer exists.
108+ rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "render.go"})
109+ } else if !isSyncing && wasSyncing == isSyncing {
95110 rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "render.go"})
96111 }
97112 }
@@ -2,6 +2,7 @@
2 package daemon2 package daemon
3 3
4 import (4 import (
5+ "bytes"
5 "errors"6 "errors"
6 "strconv"7 "strconv"
7 8
@@ -14,6 +15,14 @@ import (
14 "github.com/bnema/vev/pkg/vt"15 "github.com/bnema/vev/pkg/vt"
15 )16 )
16 17
18+// completedSynchronizedUpdate reports a full DEC 2026 batch contained in one
19+// PTY read. The VT's final state is inactive in this case, so it cannot be
20+// inferred from before/after SyncUpdateActive alone.
21+func completedSynchronizedUpdate(data []byte) bool {
22+ _, tail, found := bytes.Cut(data, []byte(renderer.SyncStartCSI))
23+ return found && bytes.Contains(tail, []byte(renderer.SyncEndCSI))
24+}
25+
17 func signal(ch chan struct{}) {26 func signal(ch chan struct{}) {
18 select {27 select {
19 case ch <- struct{}{}:28 case ch <- struct{}{}:
@@ -46,6 +55,7 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) {
46 p.screen.Write(data)55 p.screen.Write(data)
47 p.refreshTerminalTitleLocked()56 p.refreshTerminalTitleLocked()
48 isSyncing := p.screen.SyncUpdateActive()57 isSyncing := p.screen.SyncUpdateActive()
58+ completeSyncRead := !wasSyncing && !isSyncing && completedSynchronizedUpdate(data)
49 var syncGen uint6459 var syncGen uint64
50 if wasSyncing != isSyncing {60 if wasSyncing != isSyncing {
51 if isSyncing {61 if isSyncing {
@@ -91,7 +101,12 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) {
91 rc.noteSyncEnd(gen)101 rc.noteSyncEnd(gen)
92 }102 }
93 }103 }
94- if !isSyncing && wasSyncing == isSyncing {104+ if completeSyncRead {
105+ // The entire batch completed in this read. Its final screen state
106+ // is ready now, so bypass the bulk debounce without arming a
107+ // watchdog for a batch that no longer exists.
108+ rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "render.go"})
109+ } else if !isSyncing && wasSyncing == isSyncing {
95 rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "render.go"})110 rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "render.go"})
96 }111 }
97 }112 }
M internal/usecase/daemon/render_coordinator.go +16 -1
@@ -102,6 +102,10 @@ type renderCoordinator struct {
102102 pendingReset bool
103103 pendingUrgent bool
104104 coalesced int
105+ // outputPressure carries recent bulk-burst pressure between completed
106+ // batches. It selects a bounded 8–16ms deadline without letting a busy
107+ // batch perpetually reset its own timer.
108+ outputPressure int
105109
106110 // previewWake receives the same coalesced wakes for the legacy single
107111 // subscriber. previewWakes tracks picker subscriptions by viewer, so one
@@ -187,8 +191,9 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
187191 c.cancelNormalTimerLocked()
188192 }
189193 gen := c.generation
190- delay := minOutputRenderDeadline
194+ delay := minOutputRenderDeadline + time.Duration(c.outputPressure)*time.Millisecond
191195 if c.pendingUrgent {
196+ // Urgent state always supersedes bulk pressure and is never extended.
192197 delay = urgentRenderDeadline
193198 }
194199 clock := c.opts.clock
@@ -469,6 +474,16 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
469474 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
470475 c.metrics.wakes.Add(1)
471476 c.metrics.coalesced.Add(uint64(w.coalesced))
477+ if !w.urgent {
478+ if w.coalesced > 1 {
479+ c.outputPressure += w.coalesced - 1
480+ if c.outputPressure > int((maxOutputRenderDeadline-minOutputRenderDeadline)/time.Millisecond) {
481+ c.outputPressure = int((maxOutputRenderDeadline - minOutputRenderDeadline) / time.Millisecond)
482+ }
483+ } else if c.outputPressure > 0 {
484+ c.outputPressure--
485+ }
486+ }
472487 c.cancelNormalTimerLocked()
473488 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false
474489 wake, preview := c.opts.wake, c.previewWake
@@ -102,6 +102,10 @@ type renderCoordinator struct {
102 pendingReset bool102 pendingReset bool
103 pendingUrgent bool103 pendingUrgent bool
104 coalesced int104 coalesced int
105+ // outputPressure carries recent bulk-burst pressure between completed
106+ // batches. It selects a bounded 8–16ms deadline without letting a busy
107+ // batch perpetually reset its own timer.
108+ outputPressure int
105 109
106 // previewWake receives the same coalesced wakes for the legacy single110 // previewWake receives the same coalesced wakes for the legacy single
107 // subscriber. previewWakes tracks picker subscriptions by viewer, so one111 // subscriber. previewWakes tracks picker subscriptions by viewer, so one
@@ -187,8 +191,9 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) {
187 c.cancelNormalTimerLocked()191 c.cancelNormalTimerLocked()
188 }192 }
189 gen := c.generation193 gen := c.generation
190- delay := minOutputRenderDeadline194+ delay := minOutputRenderDeadline + time.Duration(c.outputPressure)*time.Millisecond
191 if c.pendingUrgent {195 if c.pendingUrgent {
196+ // Urgent state always supersedes bulk pressure and is never extended.
192 delay = urgentRenderDeadline197 delay = urgentRenderDeadline
193 }198 }
194 clock := c.opts.clock199 clock := c.opts.clock
@@ -469,6 +474,16 @@ func (c *renderCoordinator) fire(gen uint64, watchdog bool) {
469 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}474 w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog}
470 c.metrics.wakes.Add(1)475 c.metrics.wakes.Add(1)
471 c.metrics.coalesced.Add(uint64(w.coalesced))476 c.metrics.coalesced.Add(uint64(w.coalesced))
477+ if !w.urgent {
478+ if w.coalesced > 1 {
479+ c.outputPressure += w.coalesced - 1
480+ if c.outputPressure > int((maxOutputRenderDeadline-minOutputRenderDeadline)/time.Millisecond) {
481+ c.outputPressure = int((maxOutputRenderDeadline - minOutputRenderDeadline) / time.Millisecond)
482+ }
483+ } else if c.outputPressure > 0 {
484+ c.outputPressure--
485+ }
486+ }
472 c.cancelNormalTimerLocked()487 c.cancelNormalTimerLocked()
473 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false488 c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false
474 wake, preview := c.opts.wake, c.previewWake489 wake, preview := c.opts.wake, c.previewWake
M internal/usecase/daemon/snapshot_test.go +1 -1
@@ -223,7 +223,7 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
223223 invs := make(chan renderInvalidation, 1)
224224 clock := &signalClock{timers: make(chan *signalTimer, 2)}
225225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
226- clock: clock,
226+ clock: clock,
227227 onInvalidate: func(inv renderInvalidation) { invs <- inv },
228228 }))
229229 d.sessions[sess.id] = sess
@@ -223,7 +223,7 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
223 invs := make(chan renderInvalidation, 1)223 invs := make(chan renderInvalidation, 1)
224 clock := &signalClock{timers: make(chan *signalTimer, 2)}224 clock := &signalClock{timers: make(chan *signalTimer, 2)}
225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
226- clock: clock,226+ clock: clock,
227 onInvalidate: func(inv renderInvalidation) { invs <- inv },227 onInvalidate: func(inv renderInvalidation) { invs <- inv },
228 }))228 }))
229 d.sessions[sess.id] = sess229 d.sessions[sess.id] = sess

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