Skip to content

Conversation

@hannesrudolph
Copy link
Collaborator

@hannesrudolph hannesrudolph commented Oct 10, 2025

fix(webview): throttle state/message updates to prevent grey screens during long tasks

Summary

  • Stabilizes the webview during long-running tasks by reducing postMessage burst traffic and avoiding hidden-panel churn.

Changes

  • Coalesce state updates in ClineProvider.postStateToWebview (short throttle window).
  • Skip non-essential "messageUpdated" deltas while the panel is hidden; push a full state sync when it becomes visible.
  • Throttle indexing progress update sends to ~10 Hz.
  • Batch chat-row "messageUpdated" deltas to ~30 fps in Task.updateClineMessage and skip when hidden.

Why

  • Prior grey screens correlated with renderer/GPU resets from large and rapid UI updates during streaming and task resumption. These mitigations reduce message volume and DOM churn without losing UX fidelity.

Verification

  • Provider tests pass locally:
    • core/webview/tests/ClineProvider.spec.ts
    • core/webview/tests/ClineProvider.sticky-mode.spec.ts

Risk/Notes

  • Changes are additive and scoped.
  • If residual cases remain, we can add payload-size caps/chunking for extreme markdown/diff outputs.

Important

Optimize webview performance by throttling state and message updates, reducing update frequency during long tasks.

  • Behavior:
    • Throttle ClineProvider.postStateToWebview() to coalesce state updates (~30 FPS).
    • Skip non-essential messageUpdated deltas when panel is hidden; push full state sync on visibility.
    • Throttle indexing progress updates to ~10 Hz in ClineProvider.
    • Batch messageUpdated deltas to ~30 FPS in Task.updateClineMessage(); skip when hidden.
  • Misc:
    • Add RelativePattern mock in vscode.js.
    • Update extension.ts to conditionally watch files in development.
    • Add isTestEnvironment() helper in McpHub.ts.

This description was created by Ellipsis for 62557dc. You can customize this summary. It will automatically update as commits are pushed.

…s; throttle indexing updates to prevent grey screens during long tasks
@hannesrudolph hannesrudolph requested review from Copilot and removed request for Copilot October 10, 2025 23:54
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Oct 10, 2025
Comment on lines 683 to 696
if (!this.messageUpdateTimer) {
this.messageUpdateTimer = setTimeout(async () => {
try {
const batch = Array.from(this.messageUpdateBuffer.values())
this.messageUpdateBuffer.clear()
this.messageUpdateTimer = undefined
for (const m of batch) {
await provider.postMessageToWebview({ type: "messageUpdated", clineMessage: m })
}
} catch (e) {
console.error("[Task#updateClineMessage] Failed to flush message updates:", e)
}
}, this.MESSAGE_UPDATE_THROTTLE_MS)
}
Copy link

Choose a reason for hiding this comment

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

The message update batching logic has a potential race condition. If updateClineMessage() is called rapidly with different messages, the timer could fire while new messages are still being added to the buffer, causing some messages to be sent in the next batch instead of the current one.

More critically, if the provider becomes unavailable or the panel becomes hidden between when messages are buffered and when the timer fires, those buffered messages will be lost since the early return check happens before the timer is set, not when it fires.

Consider moving the visibility check inside the timer callback to ensure buffered messages are either sent or explicitly dropped with logging.

@hannesrudolph hannesrudolph moved this from Triage to PR [Needs Prelim Review] in Roo Code Roadmap Oct 10, 2025
…, atomic batch swap, drop/log deltas when hidden
Copilot AI review requested due to automatic review settings October 11, 2025 00:33
Copy link

Copilot AI left a comment

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 issues in the webview by implementing throttling and batching mechanisms to prevent grey screens during long-running tasks. The changes reduce message volume and DOM churn while maintaining UX fidelity.

Key changes:

  • Throttled state updates with coalescing in ClineProvider (~30 FPS)
  • Dropped non-essential message updates when panel is hidden
  • Batched chat message updates to prevent webview flooding

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/core/webview/ClineProvider.ts Added throttling for state updates, indexing progress, and visibility-aware message filtering
src/core/task/Task.ts Implemented batched message updates with buffering to reduce webview message flood

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +151 to +155
// Throttling/backpressure for heavy webview messages
private stateFlushQueued = false
private stateQueueDirty = false
private stateFlushPromise?: Promise<void>
private readonly STATE_THROTTLE_MS = 33 // ~30 FPS coalescing
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider extracting these throttling constants to a configuration object or constants file to improve maintainability and make them easier to tune.

Copilot uses AI. Check for mistakes.
Comment on lines +1656 to +1659
// Allow any additional state requests queued during the await to be coalesced
if (this.stateQueueDirty) {
await delay(this.STATE_THROTTLE_MS)
}
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

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

The nested delay in the do-while loop could cause unnecessary delays. Consider checking if more requests arrived during the previous delay before adding another one.

Suggested change
// Allow any additional state requests queued during the await to be coalesced
if (this.stateQueueDirty) {
await delay(this.STATE_THROTTLE_MS)
}
// No further delay here; immediately process any new requests
// Loop will continue if this.stateQueueDirty was set during the above awaits

Copilot uses AI. Check for mistakes.
}

// Batch UI updates within a short window to avoid overwhelming the webview
const ts = (message as any)?.ts as number | undefined
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

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

The type assertion (message as any) bypasses TypeScript's type safety. Consider adding a proper type guard or extending the ClineMessage interface to include the ts property if it's expected.

Copilot uses AI. Check for mistakes.

const providerNow = this.providerRef.deref()
if (!providerNow) {
console.warn(
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider using the existing logging infrastructure (this.log or outputChannel) instead of console.warn for consistency with the rest of the codebase.

Suggested change
console.warn(
this.log.warn(

Copilot uses AI. Check for mistakes.
// Throttled chat row update batching to reduce webview message flood
private messageUpdateBuffer: Map<number, ClineMessage> = new Map()
private messageUpdateTimer?: NodeJS.Timeout
private readonly MESSAGE_UPDATE_THROTTLE_MS = 33 // ~30 FPS
Copy link

Choose a reason for hiding this comment

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

The messageUpdateTimer should be cleared in the dispose() method to prevent the timer from firing after the task is disposed. Currently, if a task is disposed while a timer is pending, the timer callback will execute and attempt to access this.providerRef.deref() on a disposed task.

Consider adding this cleanup in the existing dispose() method (around line 1581):

if (this.messageUpdateTimer) {
    clearTimeout(this.messageUpdateTimer)
    this.messageUpdateTimer = undefined
}

// Throttle noisy indexing status updates
private indexStatusThrottleTimer?: NodeJS.Timeout
private pendingIndexStatus?: IndexProgressUpdate
private readonly INDEX_STATUS_THROTTLE_MS = 100
Copy link

Choose a reason for hiding this comment

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

Similar to the messageUpdateTimer in Task.ts, the indexStatusThrottleTimer should be cleared when the provider is disposed to prevent the timer from firing after disposal.

Consider adding cleanup in the dispose() method or clearWebviewResources() method:

if (this.indexStatusThrottleTimer) {
    clearTimeout(this.indexStatusThrottleTimer)
    this.indexStatusThrottleTimer = undefined
}

Task: sort batched messageUpdated by ts; clear messageUpdateTimer in dispose; use provider.log; remove any-cast for ts

ClineProvider: void postMessage in throttle callbacks; clear indexStatusThrottleTimer in dispose
Comment on lines 689 to 705
const providerNow = this.providerRef.deref()
if (!providerNow) {
this.providerRef
.deref()
?.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas: provider unavailable`,
)
return
}
if (!providerNow.isVisible()) {
// Drop deltas while hidden; UI will receive a full state sync on visibility
this.providerRef
.deref()
?.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas while hidden`,
)
return
Copy link

Choose a reason for hiding this comment

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

The logging calls on lines 691-695 and 700-705 will never execute successfully. When providerNow is undefined (line 690) or when checking visibility (line 698), the code calls this.providerRef.deref()?.log() which will return undefined again and skip the log via optional chaining.

For better observability, use the cached providerNow reference or fallback to console.log():

Suggested change
const providerNow = this.providerRef.deref()
if (!providerNow) {
this.providerRef
.deref()
?.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas: provider unavailable`,
)
return
}
if (!providerNow.isVisible()) {
// Drop deltas while hidden; UI will receive a full state sync on visibility
this.providerRef
.deref()
?.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas while hidden`,
)
return
const providerNow = this.providerRef.deref()
if (!providerNow) {
console.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas: provider unavailable`,
)
return
}
if (!providerNow.isVisible()) {
providerNow.log(
`[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas while hidden`,
)
return
}

Copy link
Collaborator Author

@hannesrudolph hannesrudolph left a comment

Choose a reason for hiding this comment

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

Implemented deterministic chat update ordering and safer throttles. Remaining suggestions are minor and tuning-oriented.

// Throttled chat row update batching to reduce webview message flood
private messageUpdateBuffer: Map<number, ClineMessage> = new Map()
private messageUpdateTimer?: NodeJS.Timeout
private readonly MESSAGE_UPDATE_THROTTLE_MS = 33 // ~30 FPS
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[P3] Consider centralizing throttle constants (MESSAGE_UPDATE_THROTTLE_MS, STATE_THROTTLE_MS, INDEX_STATUS_THROTTLE_MS) into a shared config to simplify tuning and keep defaults consistent across Task and ClineProvider.

await this.postMessageToWebview({ type: "state", state })

// Check MDM compliance and send user to account tab if not compliant
// Only redirect if there's an actual MDM policy requiring authentication
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[P3] The extra delay inside flushStateQueue's loop adds latency under sustained load. Consider removing the inner delay and relying on the loop’s coalescing to maintain responsiveness without materially increasing message volume.

…x: guard dev FS watchers in activate() and MCP watchers in tests; core(task): safe provider logging in batched webview updates
@github-project-automation github-project-automation bot moved this from PR [Needs Prelim Review] to Done in Roo Code Roadmap Oct 16, 2025
@github-project-automation github-project-automation bot moved this from New to Done in Roo Code Roadmap Oct 16, 2025
@hannesrudolph
Copy link
Collaborator Author

Does not fix the issue in the way I have reproed it.

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

Labels

bug Something isn't working PR - Needs Preliminary Review size:L This PR changes 100-499 lines, ignoring generated files.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants