Skip to content

fix - reduce streaming render cost with incremental layout and event batching#321

Open
xinyi-gong wants to merge 3 commits into
mainfrom
tori/slow-render
Open

fix - reduce streaming render cost with incremental layout and event batching#321
xinyi-gong wants to merge 3 commits into
mainfrom
tori/slow-render

Conversation

@xinyi-gong

@xinyi-gong xinyi-gong commented Jun 25, 2026

Copy link
Copy Markdown
Member

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-measured
every historical turn.

Fixes

1. Event-drain batching (drainPendingEvents)

Streaming events are now queued into a ConcurrentLinkedQueue and drained in a single scheduled
UI-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)

computeSize is now called with the actual clientArea.width instead of SWT.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 ThinkingBlock streaming

The thinking streaming path no longer calls refreshEnclosingScroller() on every chunk. Long
thinking content previously forced a full enclosing ChatContentViewer re-layout per chunk
(hundreds of full O(n) passes per turn). The enclosing scroller is now refreshed by the batched
drainPendingEventsdoRefreshScrollerLayout path along with all other streaming events; the
local updateScrollerDuringStreaming() only manages the thinking block's own inner scroller. The
expand/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-end event, ensuring
minHeight is accurate before scrollToBottom().

Performance results

Benchmark: deterministic repeated-Ask prompt, 5 turns (n = widget count, increments by 2 per turn).
Compared against baseline run on unfixed HEAD.

turn refresh avg (ms) base → fix refresh total (ms) base → fix render % of wall-clock base → fix
n2 20.04 → 5.49 13 650 → 3 168 33% → 10%
n4 34.38 → 5.34 23 960 → 2 628 55% → 9%
n6 46.79 → 9.01 38 414 → 5 787 68% → 15%
n8 52.19 → 6.58 42 011 → 4 329 70% → 11%
n10 62.47 → 9.56 49 910 → 6 464 77% → 17%

~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 — parametrised refreshEnclosingScroller(boolean incremental)
  • BaseTurnWidget.java — minor cleanup
  • ChatMarkupViewer.java — minor cleanup

@xinyi-gong xinyi-gong requested a review from jdneo as a code owner June 25, 2026 09:53
Copilot AI review requested due to automatic review settings June 25, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ThinkingBlock streaming 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] After updating to 0.18.0 Agents are extremely slow

3 participants