Repository navigation
Batch chat table scrollbar measurements to avoid a layout per table - #342234
Draft
Karthik Nadig (karthiknadig) wants to merge 1 commit into
Draft
Karthik Nadig (karthiknadig) wants to merge 1 commit into
Karthik Nadig (karthiknadig) wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Pending scans must follow tables into auxiliary windows so scrollbar initialization does not depend on the original window's animation frames.
1 open issue
What changed in this PR
Reduces repeated layout work when displaying table-heavy chat responses, addressing part of the slow session navigation reported in #341282.
Changes:
- Batches table measurements per window before updating scrollbars.
- Adds tests for scrollbar sizing and read-before-write ordering.
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownTableScrolling.test.ts | Tests scrollbar sizing and batched measurements. |
| src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownTableScrolling.ts | Schedules per-window measurement batches with disposal handling. |
Review effort: Balanced
Comment on lines
+29
to
+33
| if (table.pending || table.disposed) { | ||
| return; | ||
| } | ||
| const targetWindow = dom.getWindow(table.container); | ||
| let pending = pendingTableScans.get(targetWindow); |
Each markdown table in a chat response re-measured its horizontal scrollbar with scanDomNode(), which reads the table's layout and then updates the scrollbar DOM. Running these back to back forced one synchronous layout of the transcript per table (44 forced layouts, ~280 ms, when switching to a session with 44 tables). Schedule the tables instead: all pending tables in a window are read in one dom.measure callback and their scrollbars are updated in a later dom.modify callback in the same animation frame. Refs #341282 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Karthik Nadig (karthiknadig)
force-pushed
the
karthiknadig/bug-swarm/c2-f1-table-scroll-layout
branch
from
October 18, 2026 05:08
1814fb6 to
e7e97b2
Compare
This branch has not been deployed
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.

Refs #341282
Problem
Opening a chat session whose response contains many markdown tables forces the renderer to lay out the whole transcript once per table. In the Agents window, switching to a session with one ~100 KB response containing 44 tables triggers 44 forced layouts (about 280 ms) on every switch.
Median switch time: 2.60 s → 2.34 s (−0.26 s, −10%), 12 switches per build (3 runs × 4 switches). The time is measured from clicking the session to the end of its response being on screen. This removes one part of the slow session switching reported in #341282. Most of the remaining ~2.3 s goes to re-rendering the transcript, which this PR does not change.
Root cause
wrapTablesWithScrollablewraps each rendered table in aDomScrollableElementand registersscrollable.scanDomNode()as a layout participant (chatMarkdownTableScrolling.ts#L51-L52).scanDomNode()reads the table container'sclientWidth,scrollWidth,clientHeight,scrollHeight,scrollLeftandscrollTop, then immediately updates the scrollbar DOM (scrollableElement.ts#L747-L759).When a transcript is shown, every markdown part runs its layout participants from
ChatMarkdownContentPart.layout()and from its resize observer (chatMarkdownContentPart.ts#L616-L633, #L403-L407). The tables are therefore scanned back to back: read, write, read, write. Each write invalidates layout, so the next table's read makes Chromium synchronously lay out the transcript again. This pattern was added with the table scrollbars in d63b8bd (#298604).Fix
Layout participants now only schedule their table. All tables scheduled in a window are measured together in a single
dom.measurecallback at the next animation frame. Their scrollbars are then updated in adom.modifycallback later in that frame. All reads happen before any write, so they share one layout.scanDomNode()and applies them with the samesetScrollDimensions/setScrollPositioncalls, so scrollbar behavior is unchanged.ScrollbarVisibility.Auto, the slider stays hidden until hover or scroll, so the delay is not visible.ChatMarkdownContentPartuse the same per-elementscanDomNode()pattern, but they are left unchanged here.Measurements
mainVSCODE_DEV_DISABLE_DISPOSABLE_TRACKING=1. The Agents window is connected to a local scripted mock agent host with two completed sessions. Each session contains one ~100 KB response that renders 44 tables (3 columns × 5 rows) and 45 code blocks.mainis at 1e81fc7. Between the two builds, only the compiledchatMarkdownTableScrolling.jsdiffers.longtaskPerformanceObserver entries from the click until 1.2 s after the response is visible.mainand 2.40 s with this PR.Before / after
Both recordings show four switches between the two sessions, each containing 44 tables. After each switch, the overlay shows how long the response took to appear and how many forced layouts the table scrollbars caused.
Before
After
In these two recordings,
mainswitches in 2.70–3.50 s, with a median of 2.79 s and 44 forced layouts on every switch. This PR switches in 2.22–2.51 s, with a median of 2.29 s, and the table scrollbars force no layouts. Each recording is a single run of 4 switches, so use the 12-switch medians under Measurements for the comparison.How to test
main, the profile shows a forced reflow underscanDomNode←wrapTablesWithScrollablefor each table. With this PR, the tables are measured once in an animation frame callback and no forced reflows come from them.Console snippet that counts forced layouts from table scrollbars
Paste this into the renderer Developer Tools console, switch to a session with tables, then evaluate
__tableLayouts. Onmain,forcedis about one per table. With this PR, it is0.Validation
main@1e81fc7(source change stashed, tests kept)scripts\test.bat --grep "wrapTablesWithScrollable"scripts\test.bat --runGlob "**/contrib/chat/test/browser/widget/**/*.test.js"mainwith this change stashed:ChatResourceGroupWidget"the glyph band stays painted…" at 320 px and 760 px, andChatToolProgressSubPart"pauses clipped progress shimmer…". The other two failures are in theResourcePoolafter-each hook. They report disposables leaked by the timed-outChatToolProgressSubParttest (itswaitForMotionretry loop is still running), plus that test's console output. Run alone with this PR,ResourcePoolpasses 18/18 (--runGlob "**/chatContentParts/resourcePool.test.js").npm run typecheck-clientnpm run eslint -- <the two changed files>main: 44 per switch, 278 ms median. This PR: 0 per switch.main: 2.60 s median. This PR: 2.34 s.mainand this PRFound and fixed with automated exploratory testing. Draft until reviewed by a human.