Skip to content

fix(ui): prepare diffs off the render thread#31309

Open
Hona wants to merge 1 commit into
anomalyco:devfrom
Hona:fix/diff-preparation-worker
Open

fix(ui): prepare diffs off the render thread#31309
Hona wants to merge 1 commit into
anomalyco:devfrom
Hona:fix/diff-preparation-worker

Conversation

@Hona
Copy link
Copy Markdown
Member

@Hona Hona commented Jun 8, 2026

TLDR;

  • Large session-review diffs are prepared in a Web Worker instead of blocking the UI thread.
  • Edit, apply-patch, turn-summary, and review components now render stable prepared state rather than running diff algorithms during reactive rendering.
  • Pierre continues to own diff rendering, syntax highlighting, virtualization, and height measurement.

Problem

Two Windows user debug exports repeatedly sampled the renderer inside this synchronous path:

SessionReview
normalize
resolveFileDiff
parseDiffFromFile
structuredPatch
diffLines
execEditLength

Session Review eagerly parsed every file while building reactive component state, before expansion or Pierre virtualization. A sufficiently difficult line diff could therefore block the renderer for seconds while messages and parts continued streaming.

Changelog

Fixed

  • Opening Session Review with a difficult large diff no longer freezes the renderer while the diff is calculated.
  • Streaming message and tool updates no longer run diff parsing as part of component reconciliation.
  • Multi-file apply-patch results no longer parse every file before its diff is opened.
  • Edit and turn-summary diffs no longer calculate synchronously on the UI thread.

Improved

  • Raw patches and before/after content are prepared by one lazy renderer Web Worker and reused through a bounded cache.
  • Expanded review files now rely on Pierre's virtualizer and measured heights instead of a second OpenCode viewport scanner and fixed-height placeholders.
  • Components receive pending or prepared state and remain pure renderers; browser diff parser imports are isolated to one preparation module.

Performance

A Playwright smoke test reproduces the user stack with a full-context replacement containing 1,536 unique old lines and 1,536 unique new lines. It opens the real Review panel, expands the file, samples requestAnimationFrame, and waits for the actual Pierre viewer.

Five isolated Chromium runs per implementation:

Metric upstream/dev median This PR median Change
Maximum frame gap 233.3 ms 66.7 ms 71% lower
p95 frame gap 33.4 ms 16.8 ms 50% lower
End-to-end viewer mount 1,330 ms 1,208 ms 9% lower median

Baseline maximum frame gaps:

233.2, 233.3, 250.0, 233.3, 266.7 ms

This PR:

83.3, 33.4, 116.6, 66.7, 66.6 ms

The committed 200 ms responsiveness budget fails 5/5 baseline measurements and passes 5/5 worker measurements. A final repeated worker run measured 66.6, 66.6, 66.6, 66.7, 66.7 ms.

Verification

  • UI tests: 29 passed
  • App tests: 377 passed
  • Desktop tests: 8 passed
  • UI, app, desktop, and enterprise typechecks passed
  • Desktop production build passed and emitted a separate 34 KB parser worker
  • Performance smoke passed 5/5 repeated runs

@Hona Hona requested a review from Brendonovich as a code owner June 8, 2026 04:42
Copilot AI review requested due to automatic review settings June 8, 2026 04:42
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 moves expensive diff parsing/preparation off the UI render thread by introducing a dedicated diff-preparation Web Worker + client-side resource wrapper, and updates multiple UI surfaces (session review, turn diffs, edit/apply-patch tools, timeline) to render only already-prepared diff state.

Changes:

  • Added a diff preparation worker/client with request/response protocol, caching, and Solid resource helpers.
  • Refactored diff-rendering call sites to depend on createPreparedDiff() output instead of running diff algorithms during reactive rendering.
  • Added tests to enforce parser isolation and a Playwright smoke/perf test that validates renderer responsiveness on a pathological diff.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/ui/src/diff/worker.ts Worker entrypoint that prepares diffs and posts results back to the main thread.
packages/ui/src/diff/resource.ts Solid helpers to snapshot diff inputs and fetch prepared diffs (via worker).
packages/ui/src/diff/render-purity.test.ts Test to ensure browser diff parsers aren’t imported by render components.
packages/ui/src/diff/protocol.ts Types for worker request/response messages.
packages/ui/src/diff/client.ts Lazily-instantiated worker client wrapper.
packages/ui/src/diff/client.test.ts Unit tests for diff preparation dedupe, snapshotting, and failure handling.
packages/ui/src/diff/client-core.ts Core diff preparation client: caching/dedup + worker message lifecycle.
packages/ui/src/components/session-turn.tsx Session turn diff rendering updated to use prepared diffs.
packages/ui/src/components/session-review.tsx Session review updated to prepare diffs lazily and render only prepared state.
packages/ui/src/components/session-diff.ts Extracted prepareDiff result shape and removed patch diff cache from render path.
packages/ui/src/components/message-part.tsx Edit/apply-patch tool diffs updated to render prepared diffs instead of parsing during render.
packages/ui/src/components/apply-patch-file.ts Apply-patch file model now carries a DiffSource instead of a precomputed view diff.
packages/ui/src/components/apply-patch-file.test.ts Tests updated to assert metadata adaptation without preparing diffs.
packages/ui/package.json Export map extended to expose diff/* entrypoints.
packages/app/src/pages/session/message-timeline.tsx Timeline diff view updated to use prepared diffs.
packages/app/e2e/utils/mock-server.ts Mock server enhanced to serve custom /vcs/diff payloads for tests.
packages/app/e2e/smoke/session-review-performance.spec.ts New Playwright perf test measuring frame gaps while preparing a pathological review diff.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +75 to +79
function sourceKey(source: DiffSource) {
if (source.patch !== undefined) return `${source.file.length}:${source.file}p${source.patch.length}:${source.patch}`
const before = source.before ?? ""
const after = source.after ?? ""
return `${source.file.length}:${source.file}c${before.length}:${before}${after.length}:${after}`
Comment on lines +4 to +12
self.addEventListener("message", (event: MessageEvent<DiffWorkerRequest>) => {
try {
self.postMessage({ id: event.data.id, result: prepareDiff(event.data.source) } satisfies DiffWorkerResponse)
} catch (error) {
self.postMessage({
id: event.data.id,
error: error instanceof Error ? error.message : String(error),
} satisfies DiffWorkerResponse)
}
Comment on lines +1 to +15
import { createMemo, createResource, type Accessor } from "solid-js"
import type { DiffSource } from "../components/session-diff"
import { prepareDiff } from "./client"
import { snapshotSource } from "./client-core"

export function createPreparedDiff(source: Accessor<DiffSource | undefined>) {
const snapshot = createDiffSource(source)
if (typeof Worker === "undefined") return [() => undefined] as const
return createResource(snapshot, (value) =>
prepareDiff(value).catch((error) => {
console.error("failed to prepare diff", error)
return undefined
}),
)
}
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