Skip to content

perf(vscode): virtualize bounded transcript rows - #11094

Merged
marius-kilocode merged 3 commits into
mainfrom
perf-opencode-transcript-model
Jun 10, 2026
Merged

perf(vscode): virtualize bounded transcript rows#11094
marius-kilocode merged 3 commits into
mainfrom
perf-opencode-transcript-model

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Kilo uses the same chat transcript renderer in the sidebar, editor tabs, and Agent Manager. Very large sessions currently virtualize whole user turns while keeping an active tail outside Virtua. A single turn can contain hundreds of tools and thousands of elements, so navigating or switching sessions may still mount most of a transcript even when only one small area intersects the viewport.

This introduces a viewport-first transcript model for every Kilo VS Code chat surface, based on the proven OpenCode desktop architecture. Complete turns are flattened into stable, bounded rows for user messages, assistant part groups, diffs, and errors. Virtua mounts only visible rows plus two rows of overscan, while keepMounted is limited to the genuinely active streaming rows. Completed history no longer has an unbounded direct tail.

The implementation preserves lazy PartStash hydration and adds exact session-keyed measurement and scroll caches. Cached heights are accepted only when the complete row-key and layout fingerprints match, and scroll state independently preserves bottom-follow or a row anchor plus intra-row offset. The task timeline is rendered as bounded grouped SVG paths instead of one DOM bar per activity, while retaining hover, drag, wheel, keyboard navigation, colors, active state, and accessible labeling.

Measured impact

All comparisons used real Agent Manager sessions in a metadata-guarded shadow workspace, the exact Agent Manager fake.html?id=<uuid> frame, a 505 px transcript viewport, closed diff/review/terminal surfaces, stable source and target IDs, and multiple samples. The baseline is commit f180658b99.

Heavy transcript DOM

The baseline heavy target had only one viewport-intersecting turn but retained:

Signal Baseline Final representative session
Loaded transcript messages 240 240
Logical units 12 complete turns 245 bounded rows
Mounted transcript units 12 turns 3 rows
Direct unvirtualized units 10 turns 0 rows
Viewport-intersecting units 1 turn 1 row
Transcript pane elements Thousands, with 13,292 elements in the full Agent Manager document 179
Tool part wrappers 712 4
Tool collapsibles 449 Bounded to mounted rows
Timeline activities 791 791
Timeline DOM descendants About 1,582 9

The final viewport typically mounts 3 to 6 rows. A second session that previously exposed an unusually large offscreen row settled at 421 transcript elements and 15 tool wrappers after reducing overscan to two rows.

Comparable CPU traces

For the same warmed source-to-heavy-target switch, the first bounded-row stage changed the trace distribution as follows:

Metric Baseline Bounded rows Change
Longest switch task p50 379.8 ms 43.3 ms 88.6% lower
Longest switch task p95 418.0 ms 45.9 ms 89.0% lower
Longest switch task max 427.5 ms 46.2 ms 89.2% lower
Full-capture script p50 2,290.5 ms 934.6 ms 59.2% lower
Full-capture style p50 59.5 ms 28.5 ms 52.2% lower
Full-capture layout p50 64.0 ms 28.9 ms 54.9% lower
Full-capture paint p50 17.7 ms 15.9 ms 10.3% lower
Chromium transient node delta +13,818 +492 96.4% lower

The final strictly valid traced stage had no switch-overlapping main-thread task above 50 ms. Its three-stable-frame qualification was 95.0 ms p50, 99.1 ms p95, and 99.6 ms max, with work split across frames instead of one blocking task.

Final warm activation latency

After reducing overscan to two rows, 30 repeated heavy worktree activations measured:

Metric Latency
p50 25.7 ms
p95 33.8 ms
max 38.6 ms

All 30 samples activated the expected target. Compared with the previous three-row overscan stage, this is 26.1% lower at p50, 12.4% lower at p95, and 23.1% lower at max.

Sessions within one worktree

Thirty direct switches from one 240-message session to each of two other 240-message tabs in the same worktree measured:

Target p50 p95 max Mounted rows
Fork 1 20.9 ms 24.8 ms 28.8 ms 7
Fork 2 13.9 ms 16.0 ms 17.4 ms 5 to 6

Broad real-session traversal

A single traversal across 26 real worktrees representing 37 open session tabs produced:

Metric Result
Activation p50 9.6 ms
Activation p95 45.1 ms
Activation max 47.3 ms
Loaded messages per target 16 to 240
Logical rows per target 16 to 263
Mounted rows per target 2 to 9

A separate repeated traversal of nine large sessions for 20 cycles, 180 switches total, kept mounted rows between 2 and 9 and produced 25.9 ms p50, 61.2 ms p95, and 96.2 ms max across sessions with very different visible content. Per-session p95 ranged from 17.8 ms to 77.8 ms, with the slower cases corresponding to heavier visible rows rather than retained transcript size.

Memory signals

The 20-cycle traversal showed a cycle-end heap slope of approximately +0.04 MiB per cycle. Comparable cycle-end samples ranged from 144.5 to 310.3 MiB and ended at 176.0 MiB after starting at 228.0 MiB. A one-pass 26-worktree traversal ranged from 145.8 to 393.2 MiB and ended at 283.1 MiB. These process-wide figures are GC-sensitive, but the repeated traversal did not show unbounded cycle-over-cycle growth.

Architecture details

  • Stable row keys survive append, prepend, reconcile, queue handoff, compaction, and revert boundaries.
  • Unchanged row object identities are reused to avoid unnecessary Solid and Virtua work.
  • Assistant part rows are capped at eight parts, preventing historical rows from collecting hundreds of tool wrappers.
  • Active streaming rows remain inside Virtua and use bounded keepMounted indexes rather than a direct tail.
  • Measurement cache capacity is 16 sessions and requires exact row-key and layout fingerprints.
  • Scroll cache capacity is 50 sessions and stores bottom-follow or stable row-anchor state independently of measurements.
  • Timeline paths are grouped by color, with delegated hit testing and one bounded keyboard-accessible active representation.
  • No session prefetch or data LRU is added. Current switching is already below the user-visible target, and adding eviction/prefetch would trade warm speed for memory complexity without measured need.

Measurement limits

CPU profiler startup work was excluded from switch-task attribution. Captures with frame or document teardown were rejected. Direct activation measurements cover synchronous selection and render dispatch, while the stable-frame metric additionally waits for three unchanged animation frames. Streaming was not exercised with paid prompts; active-row pinning and queue handoff are covered deterministically, while real-session profiling used existing settled sessions.

@marius-kilocode marius-kilocode changed the title perf(agent-manager): virtualize bounded transcript rows perf(vscode): virtualize bounded transcript rows Jun 10, 2026
Comment thread packages/kilo-vscode/tests/unit/timeline-geometry.test.ts
Comment thread packages/kilo-vscode/webview-ui/src/utils/timeline/geometry.ts
@kilo-code-bot

kilo-code-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Suggestions | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/kilo-vscode/tests/unit/timeline-geometry.test.ts 40 navigate(key = "Escape") is never called from onKeyDown — Escape is filtered before navigate. Test exercises dead code.
packages/kilo-vscode/webview-ui/src/utils/timeline/geometry.ts 41 width: x includes one trailing gap after the last bar, sizing SVG/content 1px wider than bar area. Intentional but undocumented.
Other Observations (not in diff)
  • transcript-cache.ts — module-level singleton maps: measurements and scrolls are module-level Map instances that persist for the entire webview lifetime. They are never cleared in production (only resetTranscriptCaches() for tests). The 16-entry LRU for measurements and 50-entry LRU for scrolls provide natural eviction, so this is unlikely to cause unbounded growth, but stale entries for deleted sessions will linger until evicted. This is consistent with the PR description's design intent (no session eviction/prefetch added).

  • TranscriptRow.tsx:30 — single-message hydration: session.hydrateParts([props.row.message.id]) hydrates only the message for this row, compared with the previous VscodeSessionTurn which eagerly hydrated all messages in a turn. This is the correct lazy hydration pattern for bounded rows. For user rows, user parts are pre-fetched at row creation time in transcript-rows.ts:129, so the hydrateParts call on a user row is a no-op if parts are already in the stash — this is fine.

  • getMeasurement is destructive on mismatch: When called with a mismatched key or layout fingerprint, it deletes the stored entry for that session ID (transcript-cache.ts:57). This means a stale fingerprint check permanently evicts a potentially reusable cache entry. The test at line 125-126 confirms this is intentional — it avoids serving a measurement for a different row set — but it does mean layout changes mid-session (e.g., panel resize) clear the stored measurement for the current session, not just mark it stale.

Files Reviewed (13 files)
  • .changeset/fast-agent-transcripts.md
  • packages/kilo-vscode/tests/unit/task-timeline-tooltip.test.ts — 0 issues
  • packages/kilo-vscode/tests/unit/timeline-geometry.test.ts — 1 suggestion
  • packages/kilo-vscode/tests/unit/transcript-cache.test.ts — 0 issues
  • packages/kilo-vscode/tests/unit/transcript-rows.test.ts — 0 issues
  • packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx — 0 issues
  • packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx — 0 issues
  • packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx — 0 issues
  • packages/kilo-vscode/webview-ui/src/components/chat/TranscriptRow.tsx — 0 issues
  • packages/kilo-vscode/webview-ui/src/components/chat/transcript-cache.ts — 0 issues
  • packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts — 0 issues
  • packages/kilo-vscode/webview-ui/src/styles/task-header.css — 0 issues
  • packages/kilo-vscode/webview-ui/src/utils/timeline/geometry.ts — 1 suggestion

Fix these issues in Kilo Cloud


Reviewed by claude-4.6-sonnet-20260217 · 2,596,060 tokens

Review guidance: REVIEW.md from base branch main

@marius-kilocode
marius-kilocode merged commit 77ad0bd into main Jun 10, 2026
23 of 25 checks passed
@marius-kilocode
marius-kilocode deleted the perf-opencode-transcript-model branch June 10, 2026 20:46
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…ipt-model

perf(vscode): virtualize bounded transcript rows
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.

2 participants