Skip to content

perf(desktop): coalesce streaming text/reasoning deltas per animation frame (#3114)#3501

Merged
esengine merged 3 commits into
main-v2from
land/3114-raf-batch
Jun 8, 2026
Merged

perf(desktop): coalesce streaming text/reasoning deltas per animation frame (#3114)#3501
esengine merged 3 commits into
main-v2from
land/3114-raf-batch

Conversation

@esengine

@esengine esengine commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Lands the rAF streaming coalescer from #3114 (by @HUQIANTAO) on top of the current main-v2.

The original PR was written against the old single global dispatch; main-v2 has since moved the controller to per-tab dispatch (dispatchTo(targetTabId, …)). Re-applied the batching onto the new model:

  • the rAF batch now carries each event's resolved tabId and flushes via dispatchTo, so coalesced deltas route to the correct tab;
  • non-text events drain() first to preserve causal ordering (unchanged intent);
  • merged the unmount cleanup into the single effect return so it no longer dead-codes the new onReady/syncActiveTabFromBackend setup.

Also trimmed rafBatch.ts's comments to repo style (2-line header, 1-line whys) — logic unchanged.

Original author attribution preserved.

Closes #3114

HUQIANTAO and others added 3 commits June 5, 2026 09:38
… frame

The agent stream pushes a text/reasoning delta per Webview event; at 200
tok/s that is one state update every ~5ms, while rAF is 16ms — so the
reducer was running 3-4 times per visible frame and the React tree was
re-rendering the entire Message list each time.

Add lib/rafBatch.ts: a tiny rAF-coalescing queue with a synchronous
drain() so non-text events (tool_dispatch, usage, notice, turn_started/
done, message) flush any pending deltas first to keep causal ordering
intact. Drain is also called on unmount so a turn-end that races the
teardown doesn't strand the last few tokens in the buffer.

tsc --noEmit passes; Go build ./desktop/... is clean.
… frame

The agent stream pushes a text/reasoning delta per Webview event; at 200
tok/s that is one state update every ~5ms, while rAF is 16ms — so the
reducer was running 3-4 times per visible frame and the React tree was
re-rendering the entire Message list each time.

Add lib/rafBatch.ts: a tiny rAF-coalescing queue with a synchronous
drain() so non-text events (tool_dispatch, usage, notice, turn_started/
done, message) flush any pending deltas first to keep causal ordering
intact. Drain is also called on unmount so a turn-end that races the
teardown doesn't strand the last few tokens in the buffer.

tsc --noEmit passes; Go build ./desktop/... is clean.
# Conflicts:
#	desktop/frontend/src/lib/useController.ts
@esengine esengine requested a review from SivanCola as a code owner June 8, 2026 03:19
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) and removed v2 Go rewrite (1.x) — main-v2 branch, active development labels Jun 8, 2026
@esengine esengine merged commit c816d2d into main-v2 Jun 8, 2026
10 checks passed
@esengine esengine deleted the land/3114-raf-batch branch June 8, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants