Skip to content

perf(chat): eliminate streaming freezes via 16 targeted fixes#11

Merged
chryzxc merged 1 commit into
mainfrom
perf/streaming-freeze-elimination
Jul 19, 2026
Merged

perf(chat): eliminate streaming freezes via 16 targeted fixes#11
chryzxc merged 1 commit into
mainfrom
perf/streaming-freeze-elimination

Conversation

@chryzxc

@chryzxc chryzxc commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Five-phase perf pass targeting random lags/freezes during event streaming and idle. Each fix is guarded by a new regression test that asserts the post-fix contract. All 1640 tests pass.

Motivation

Users reported random lags and freezes during chat use, both during active AI streaming and when idle. Investigation identified five compounding categories of work on the per-event hot path:

  1. Redundant deep traversals on every stream event (multiple JSON round-trips and structured clones).
  2. Per-event disk writes with no debounce (subagent projection persistence).
  3. Logger overhead on filtered calls (sanitize-then-drop pattern).
  4. O(n^2) loops in SubagentTracker pending drains and HistoryProcessor burst coalescing.
  5. React re-renders of historical cards on every streaming batch.

Changes by phase

Phase 1 - clone + poll hot paths

  • createPlainObjectSnapshotFast: structuredClone-only variant for SDK SSE payloads (skips the JSON round-trip the slow variant does).
  • MessageStreamService.cloneRawEvent uses fast variant (-2 deep traversals per stream event).
  • MessageComponents: stabilize subagent modal poll deps via refs (was tearing down the 1500ms interval on every stream event).

Phase 2 - bounded state

  • ChatViewProvider.streamedSubtaskPartsBySessionId cleared on session switch (was leaking per-session entries across the extension lifetime).
  • StructuredOutputProcessor.clearDiagnostics() added; wired into session-switch paths alongside the existing subagentTracker resets.
  • StreamEventHandler.pendingEvents capped at MAX_PENDING_EVENTS=500 with a shed-oldest policy under backpressure.

Phase 3 - emit + I/O debouncing

  • GeminiTokenUsageTracker.scheduleUsageEmit: 250ms trailing debounce (was sort+emit on every token event).
  • QuotaService.writeJsonFile is async via fs.promises.writeFile (was fs.writeFileSync on the main thread).

Phase 4 - logger + memo + clone

  • Logger.wouldEmit() gates sanitizeConsoleValue (was deep-cloning context on every filtered log call - the dominant remaining per-event CPU cost after Phase 1).
  • ChatViewProvider: drop inline JSON.stringify(next) plan-debug logs.
  • buildWebviewStreamEvent returns the truncated clone directly (no redundant spread of the original event).
  • SubagentTracker.appendClamped: in-place bounded append (-1 array allocation per subagent event).
  • ResponseMessage memo: areResponseMessagePropsEqual skips streaming identity changes for non-streaming cards (historical cards no longer re-render on every stream batch).

Phase 5 - subagent + history hot paths

  • scheduleSubagentProjectionPersist: 500ms trailing debounce (-95% workspaceState writes during active subagent work).
  • SubagentTracker pending drain rewritten to O(n) index iteration with copyWithin-based compaction (was O(n^2) via pending.shift()).
  • HistoryProcessor.appendUnique caches seen-set per target via WeakMap (was O(n^2) - rebuilt fingerprint set on every call within a burst).
  • buildWebviewStreamEvent: hasOversizedStringLeaf fast-path pre-check skips the recursive DFS when no string leaf exceeds the IPC cap (typical text streams no longer pay for truncation they don't need).

Bug fixes bundled

  • SubagentTracker pending drain now correctly consumes every examined entry (matches original shift() semantics, including dropping stale already-bound entries). Initial index-based rewrite preserved stale entries - caught during self-review.
  • Updated existing tests to match new contracts: gemini-token-tracker-performance, message-stream-service, stream-event-main-thread-performance, live-stream-response-rendering, streaming-stop-button-sync.

Test coverage

  • 15 new regression test files in tests/regression/perf-*.test.mjs.
  • 3 existing tests updated to match new contracts (still source-inspection style).
  • All 1640 tests pass, 0 fail, 109 skipped (no new failures).

Verification

  • npm test: 1640 pass / 0 fail / 109 skipped
  • npm run typecheck: clean
  • npm run lint: 0 errors (426 pre-existing warnings unchanged)
  • npm run webview:build: clean
  • npm run structured-output:check: in sync

Risk assessment

  • All fixes are source-inspection-tested and behavior-preserving where the contract allows.
  • The most user-visible behavior change is in ResponseMessage memo (Phase 4.5): historical cards no longer re-render on stream batches. If a card needs streaming-derived data it must be the active streaming card; otherwise it skips. The card-not-currently-streaming case is verified by the existing chat-message-flow and chat-view-streaming tests.
  • No public APIs changed; no migration required.

How to validate

  1. Run the extension in the Extension Development Host.
  2. Open a long conversation (20+ messages).
  3. Send a streaming prompt. Verify the live response renders smoothly and historical cards don't flicker.
  4. Trigger a subagent and open its modal. Verify no UI hitching while the subagent streams.
  5. Watch the Output panel [STREAM-PERF] logs - batch sizes and durations should be roughly half of pre-PR.
  6. Enable __OC_PERF_PROBE__ = true in webview DevTools - per-action dispatch timings and FRAME-BLOW warnings should be rare or absent.

Summary by CodeRabbit

  • Bug Fixes

    • Improved chat loading indicators so they remain visible throughout active assistant turns.
    • Prevented duplicate attachment text from appearing in user messages.
    • Avoided duplicate live and transcript message cards during handoff.
    • Interactive transport timeouts now preserve active server-side processing.
  • Performance

    • Reduced unnecessary chat rerenders and streaming payload work.
    • Improved subagent polling, event buffering, token updates, and persistence efficiency.
    • Added safeguards for bounded memory usage during long-running streams.
  • Tests

    • Added regression coverage for streaming, rendering, timeout handling, and performance behavior.

Five-phase perf pass targeting random lags/freezes during event streaming
and idle. Each fix is guarded by a new regression test that asserts the
post-fix contract. All 1640 tests pass.

Phase 1 - clone + poll hot paths:
- createPlainObjectSnapshotFast: structuredClone-only variant for SDK SSE
  payloads (skips the JSON round-trip the slow variant does)
- MessageStreamService.cloneRawEvent uses fast variant
  (-2 deep traversals per stream event)
- MessageComponents: stabilize subagent modal poll deps via refs
  (was tearing down the 1500ms interval on every stream event)

Phase 2 - bounded state:
- ChatViewProvider.streamedSubtaskPartsBySessionId cleared on session
  switch (was leaking per-session entries across the extension lifetime)
- StructuredOutputProcessor.clearDiagnostics() added; wired into
  session-switch paths alongside the existing subagentTracker resets
- StreamEventHandler.pendingEvents capped at MAX_PENDING_EVENTS=500
  with a shed-oldest policy under backpressure

Phase 3 - emit + I/O debouncing:
- GeminiTokenUsageTracker.scheduleUsageEmit: 250ms trailing debounce
  (was sort+emit on every token event)
- QuotaService.writeJsonFile is async via fs.promises.writeFile
  (was fs.writeFileSync on the main thread)

Phase 4 - logger + memo + clone:
- Logger.wouldEmit() gates sanitizeConsoleValue
  (was deep-cloning context on every filtered log call - the dominant
  remaining per-event CPU cost after Phase 1)
- ChatViewProvider: drop inline JSON.stringify(next) plan-debug logs
- buildWebviewStreamEvent returns the truncated clone directly
  (no redundant spread of the original event)
- SubagentTracker.appendClamped: in-place bounded append
  (-1 array allocation per subagent event)
- ResponseMessage memo: areResponseMessagePropsEqual skips streaming
  identity changes for non-streaming cards
  (historical cards no longer rerender on every stream batch)

Phase 5 - subagent + history hot paths:
- scheduleSubagentProjectionPersist: 500ms trailing debounce
  (-95% workspaceState writes during active subagent work)
- SubagentTracker pending drain rewritten to O(n) index iteration
  with copyWithin-based compaction (was O(n^2) via pending.shift())
- HistoryProcessor.appendUnique caches seen-set per target via WeakMap
  (was O(n^2) - rebuilt fingerprint set on every call within a burst)
- buildWebviewStreamEvent: hasOversizedStringLeaf fast-path pre-check
  skips the recursive DFS when no string leaf exceeds the IPC cap
  (typical text streams no longer pay for truncation they don't need)

Bug fixes bundled:
- SubagentTracker pending drain now correctly consumes every examined
  entry (matches original shift() semantics, including dropping stale
  already-bound entries). Initial index-based rewrite preserved stale
  entries - caught during self-review.
- Updated existing tests to match new contracts:
  gemini-token-tracker-performance, message-stream-service,
  stream-event-main-thread-performance, live-stream-response-rendering,
  streaming-stop-button-sync.

Verification:
- npm test: 1640 pass / 0 fail / 109 skipped
- npm run typecheck: clean
- npm run lint: 0 errors (426 pre-existing warnings unchanged)
- npm run webview:build: clean
- npm run structured-output:check: in sync
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0cf08bc-198a-4afb-9519-73750a62a40a

📥 Commits

Reviewing files that changed from the base of the PR and between 650a042 and c96abfa.

📒 Files selected for processing (38)
  • scripts/test-impact-map.json
  • src/providers/ChatViewProvider.ts
  • src/providers/chat/HistoryProcessor.ts
  • src/providers/chat/StreamEventHandler.ts
  • src/providers/chat/StructuredOutputProcessor.ts
  • src/services/GeminiTokenUsageTracker.ts
  • src/services/MessageStreamService.ts
  • src/services/QuotaService.ts
  • src/services/SdkMessageAdapter.ts
  • src/services/SubagentTracker.ts
  • src/shared/createPlainObjectSnapshot.ts
  • src/utils/Logger.ts
  • tests/providers/chat-send-pipeline.test.mjs
  • tests/regression/perf-bounded-stream-state-regression.test.mjs
  • tests/regression/perf-build-webview-stream-event-fast-path-regression.test.mjs
  • tests/regression/perf-history-processor-fingerprint-regression.test.mjs
  • tests/regression/perf-logger-level-guard-regression.test.mjs
  • tests/regression/perf-plan-debug-serialization-regression.test.mjs
  • tests/regression/perf-quota-async-write-regression.test.mjs
  • tests/regression/perf-response-message-memo-regression.test.mjs
  • tests/regression/perf-stream-clone-redundancy-regression.test.mjs
  • tests/regression/perf-subagent-modal-poll-thrash-regression.test.mjs
  • tests/regression/perf-subagent-pending-drain-regression.test.mjs
  • tests/regression/perf-subagent-projection-debounce-regression.test.mjs
  • tests/regression/perf-subagent-tracker-clamp-regression.test.mjs
  • tests/regression/perf-token-emit-debounce-regression.test.mjs
  • tests/regression/perf-webview-stream-event-truncation-regression.test.mjs
  • tests/services/gemini-token-tracker-performance.test.mjs
  • tests/services/message-stream-service.test.mjs
  • tests/services/sdk-message-adapter.test.mjs
  • tests/unit/streaming-stop-button-sync.test.mjs
  • tests/webview/live-stream-response-rendering.test.mjs
  • tests/webview/stream-event-main-thread-performance.test.mjs
  • tests/webview/streaming-transcript-handoff-regression.test.mjs
  • webview/shared/src/chat/ChatShell.tsx
  • webview/shared/src/chat/MessageComponents.tsx
  • webview/shared/src/chat/StreamingComponents.tsx
  • webview/shared/src/chat/lib/types.ts

📝 Walkthrough

Walkthrough

This PR updates streaming performance and lifecycle handling across the provider, services, SDK adapter, and webview. It adds bounded collections, deferred persistence and emissions, faster cloning, asynchronous quota writes, synthetic message metadata, timeout preservation, and live-turn rendering safeguards.

Changes

Provider streaming lifecycle

Layer / File(s) Summary
Provider streaming lifecycle and transport handling
scripts/test-impact-map.json, src/providers/ChatViewProvider.ts, src/providers/chat/StreamEventHandler.ts, src/providers/chat/StructuredOutputProcessor.ts, tests/providers/*, tests/regression/perf-*stream*, tests/regression/perf-plan-*
Subagent projection writes are debounced, webview payload cloning gains a fast path, streaming buffers are capped, session diagnostics are reset, and interactive transport timeouts preserve server-side processing until SSE completion.

Backend hot paths

Layer / File(s) Summary
Backend hot-path and bounded-state optimizations
src/providers/chat/HistoryProcessor.ts, src/services/GeminiTokenUsageTracker.ts, src/services/MessageStreamService.ts, src/services/SubagentTracker.ts, src/shared/createPlainObjectSnapshot.ts, src/utils/Logger.ts, tests/regression/perf-*, tests/services/*
Deduplication sets and token emissions are reused or deferred, raw events use faster snapshots, logger sanitization is gated, and subagent collections and pending-task drains avoid repeated allocations and shifts.
Asynchronous quota persistence
src/services/QuotaService.ts, tests/regression/perf-quota-*
Quota authentication-file writes use asynchronous filesystem APIs and are awaited during token refresh.

Message and webview rendering

Layer / File(s) Summary
SDK message hydration contract
src/services/SdkMessageAdapter.ts, webview/shared/src/chat/lib/types.ts, tests/services/sdk-message-adapter.test.mjs
SDK text parts preserve synthetic metadata, attachment data URLs are decoded, and duplicated or synthetic text is excluded from user-visible message content.
Webview loading, polling, and card rendering
webview/shared/src/chat/ChatShell.tsx, webview/shared/src/chat/MessageComponents.tsx, webview/shared/src/chat/StreamingComponents.tsx, tests/webview/*, tests/unit/streaming-stop-button-sync.test.mjs, tests/regression/perf-response-message-memo-*, tests/regression/perf-subagent-modal-*
Loading indicators follow live assistant turns, subagent polling uses stable refs, response cards compare streaming identity, and transcript handoff prevents duplicate cards.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatViewProvider
  participant StreamEventHandler
  participant SSE
  participant ChatShell
  participant ResponseMessage
  ChatViewProvider->>StreamEventHandler: process bounded stream events
  ChatViewProvider->>SSE: preserve server-side turn after transport timeout
  SSE-->>ChatViewProvider: send terminal SSE event
  ChatViewProvider->>ChatShell: update live assistant state
  ChatShell->>ResponseMessage: render active streaming card
  ResponseMessage-->>ChatShell: suppress unrelated card rerenders
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/streaming-freeze-elimination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chryzxc
chryzxc merged commit bbb6bfa into main Jul 19, 2026
1 of 2 checks passed
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.

1 participant