fix - reduce streaming render cost with incremental layout and event batching#321
Open
xinyi-gong wants to merge 3 commits into
Open
fix - reduce streaming render cost with incremental layout and event batching#321xinyi-gong wants to merge 3 commits into
xinyi-gong wants to merge 3 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses performance regressions in the chat UI’s streaming render path by batching incoming progress events and reducing layout work during streaming, aiming to restore responsive agent interactions (issue #259).
Changes:
- Batch LSP streaming/progress events via a pending queue drained on the UI thread to avoid per-chunk full relayouts.
- Introduce incremental (trailing-turn-only) layout/measurement and use constrained-width sizing to reduce flicker and layout cost.
- Remove per-chunk enclosing-scroller refresh from
ThinkingBlockstreaming updates, relying on the batched refresh path instead.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java | Adds queued/batched event draining and incremental scroller layout to reduce streaming render cost and flicker. |
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ThinkingBlock.java | Avoids refreshing the enclosing scroller on every streaming chunk. |
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java | Updates refresh scheduling call site to the renamed refresh scheduler. |
jdneo
reviewed
Jun 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix #259
Root cause
Every streaming chunk (text fragment, thinking update, tool call delta) triggered a full
layout(true, true)+cmpContent.computeSize(SWT.DEFAULT, SWT.DEFAULT)pass that re-measuredevery historical turn.
Fixes
1. Event-drain batching (
drainPendingEvents)Streaming events are now queued into a
ConcurrentLinkedQueueand drained in a single scheduledUI-thread callback instead of dispatching one layout pass per event. A re-entrancy guard prevents
cascading drains from
controlResized.2. Incremental / O(1) layout
During streaming only the trailing turns (latest user turn + active Copilot turn) are
re-measured. Historical sealed turns keep their cached sizes (
computeSize(w, DEFAULT, false)).This replaces the previous
layout(true, true)call that invalidated the entire composite.3. Constrained-width size measurement (flicker fix)
computeSizeis now called with the actualclientArea.widthinstead ofSWT.DEFAULT.Previously the unconstrained width produced a phantom over-estimated height, causing the scroll
position to oscillate during streaming and producing visible flicker.
4. Drop per-chunk enclosing-scroller refresh in
ThinkingBlockstreamingThe thinking streaming path no longer calls
refreshEnclosingScroller()on every chunk. Longthinking content previously forced a full enclosing
ChatContentViewerre-layout per chunk(hundreds of full O(n) passes per turn). The enclosing scroller is now refreshed by the batched
drainPendingEvents→doRefreshScrollerLayoutpath along with all other streaming events; thelocal
updateScrollerDuringStreaming()only manages the thinking block's own inner scroller. Theexpand/collapse path still refreshes the enclosing scroller directly.
5. Incremental re-measure on turn-end only
The drain batch triggers a incremental re-measure only when it contains a
turn-endevent, ensuringminHeightis accurate beforescrollToBottom().Performance results
Benchmark: deterministic repeated-Ask prompt, 5 turns (
n= widget count, increments by 2 per turn).Compared against baseline run on unfixed HEAD.
~7.7× reduction in layout time at turn 5. The per-pass average is now essentially flat
(5.5–9.6 ms) across n2–n10 — effectively O(1) in turn count. Per turn only 2–4 full-measure
passes run; the rest take the O(1) incremental path. Render is no longer the dominant cost.
Perf Doc: perf-baseline-259.md
Files changed
ChatContentViewer.java— drain queue, incremental layout, re-entrancy guard, flicker fix,full-measure-on-turn-end, auto-scroll logic
ThinkingBlock.java— parametrisedrefreshEnclosingScroller(boolean incremental)BaseTurnWidget.java— minor cleanupChatMarkupViewer.java— minor cleanup