Skip to content

chat: defer chat row resize dispatch to break ResizeObserver loop#314882

Open
bryanchen-d wants to merge 2 commits intomainfrom
brchen/chat-row-resize-loop
Open

chat: defer chat row resize dispatch to break ResizeObserver loop#314882
bryanchen-d wants to merge 2 commits intomainfrom
brchen/chat-row-resize-loop

Conversation

@bryanchen-d
Copy link
Copy Markdown
Contributor

The chat row ResizeObserver callback in ChatListItemRenderer dispatched fireItemHeightChange synchronously, which re-enters layout in the same animation frame and causes Chromium to fire the synthetic ResizeObserver loop completed with undelivered notifications error.

Telemetry attribution shipped in #314411 confirms this row observer (named ChatListItemRenderer.itemHeight) is the dominant source of the loop warning across our error stream.

Fix

Defer the dispatch onto the next task with setTimeout(0) so the observer callback returns cleanly before the layout mutation runs. Coalesce repeated observations into a single dispatch using the latest blockSize, and clear the pending handle on template disposal.

Validation

  • Built locally and reproduced via streaming chat responses against a long thread.
  • Before: ResizeObserver loop completed with undelivered notifications fires repeatedly during streaming and on row hover.
  • After: warning no longer fires; row heights still update visibly during streaming with no perceptible lag.

Notes

  • setTimeout(0) (not queueMicrotask) is required: V8 microtask checkpoints run after each ResizeObserver callback returns, but Chromium dispatches the loop error after the entire observation phase finishes — so deferring to the next task is what breaks the cycle. Same reasoning as dom: attribute ResizeObserver loop telemetry to DisposableResizeObserver name #314411.
  • Net cost is one setTimeout(0) per observed resize per row, coalesced. Negligible relative to the layout work that was already running.

The `ChatListItemRenderer` row ResizeObserver dispatched `fireItemHeightChange`
synchronously inside the observer callback. That re-enters layout in the same
animation frame, causing the observer to fire again, which Chromium reports as
`ResizeObserver loop completed with undelivered notifications`.

Telemetry attribution from #314411 (DisposableResizeObserver named slots) shows
this row observer is the dominant source of the warning across our 7-day error
stream.

Defer the dispatch onto the next task with `setTimeout(0)` so the observer
callback returns cleanly before the layout mutation runs. Coalesce repeated
observations into a single dispatch using the latest blockSize, and clear the
pending handle on template disposal.
Copilot AI review requested due to automatic review settings May 6, 2026 23:49
Copy link
Copy Markdown
Contributor

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 mitigates Chromium’s synthetic ResizeObserver loop completed with undelivered notifications warning in the chat list by preventing synchronous layout re-entry from the chat row ResizeObserver callback in ChatListItemRenderer.

Changes:

  • Defers fireItemHeightChange dispatch to the next macrotask via setTimeout(0) from the row ResizeObserver callback.
  • Coalesces multiple resize notifications into a single pending dispatch using the latest observed blockSize.
  • Clears any pending timeout when the template is disposed to avoid late callbacks.
Show a summary per file
File Description
src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts Defers/coalesces row height-change dispatches from ResizeObserver to break synchronous resize/layout feedback loops.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment on lines +659 to +662
const blockSize = entry.borderBoxSize.at(0)?.blockSize;
if (typeof blockSize !== 'number') {
return;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 01a64dc. The dispatch is now always scheduled; only the cached pendingBlockSize is gated on typeof blockSize === 'number', so when borderBoxSize is empty/missing we still call fireItemHeightChange(template, undefined) and fall back to getBoundingClientRect() as before.

if (pendingDispatchHandle !== undefined) {
return;
}
pendingDispatchHandle = setTimeout(() => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Deferring this is going to add the kinds of issues I was talking about. It's not expected that fireItemHeightChange would cause the size of this element to change, we should only change the parent list row to accomodate it, do you know what causes its height to change?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And since this is mine, I can investigate, do you know of repro steps?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's interesting because I don't remember seeing that warning, but this observer fires really often

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, for repro: Start a new chat session with some prompt like "research the repo"

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.

3 participants