From 5cfab38f315945b81e2225b7edca01fdff622bb8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 18 Feb 2026 15:58:54 -0800 Subject: [PATCH] Move diff parsing to worker pool with stable patch cache keys - Wrap `DiffPanel` in `WorkerPoolContextProvider` and add a dedicated diffs worker - Use content-based patch cache keys to stabilize `parsePatchFiles` caching - Add tests for cache key stability, whitespace normalization, and content changes --- apps/web/src/components/DiffPanel.tsx | 247 +++++++++++++------------ apps/web/src/lib/diffRendering.test.ts | 23 +++ apps/web/src/lib/diffRendering.ts | 24 +++ apps/web/src/workers/diffs.worker.ts | 1 + 4 files changed, 180 insertions(+), 115 deletions(-) create mode 100644 apps/web/src/lib/diffRendering.test.ts create mode 100644 apps/web/src/lib/diffRendering.ts create mode 100644 apps/web/src/workers/diffs.worker.ts diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 37a4369a56e18ff3fa58071c3684531a71e15c20..c1bdc862aa8fb4599ad3af794f3cf4bd12cfea00 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -1,5 +1,5 @@ import { parsePatchFiles } from "@pierre/diffs"; -import { FileDiff, type FileDiffMetadata } from "@pierre/diffs/react"; +import { FileDiff, type FileDiffMetadata, WorkerPoolContextProvider } from "@pierre/diffs/react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Columns2Icon, Rows3Icon } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -8,6 +8,7 @@ import { cn } from "~/lib/utils"; import { isElectron } from "../env"; import { useNativeApi } from "../hooks/useNativeApi"; import { useTheme } from "../hooks/useTheme"; +import { buildPatchCacheKey } from "../lib/diffRendering"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { formatTimestamp } from "../session-logic"; import { useStore } from "../store"; @@ -32,7 +33,7 @@ function getRenderablePatch(patch: string | undefined): RenderablePatch | null { if (normalizedPatch.length === 0) return null; try { - const parsedPatches = parsePatchFiles(normalizedPatch, "diff-panel"); + const parsedPatches = parsePatchFiles(normalizedPatch, buildPatchCacheKey(normalizedPatch)); const files = parsedPatches.flatMap((parsedPatch) => parsedPatch.files); if (files.length > 0) { return { kind: "files", files }; @@ -288,135 +289,151 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { }; const shouldUseDragRegion = isElectron && mode === "inline"; + const workerPoolSize = useMemo(() => { + const cores = typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4); + return Math.max(2, Math.min(6, Math.floor(cores / 2))); + }, []); return ( - + { + const next = value[0]; + if (next === "stacked" || next === "split") { + setDiffRenderMode(next); + } + }} + > + + + + + + + + + + {!activeThread ? ( +
+ Select a thread to inspect turn diffs. +
+ ) : turnDiffSummaries.length === 0 ? ( +
+ No completed turns yet. +
+ ) : ( + <> +
+ {!canApplyStoredTarget && state.diffThreadId && ( +

+ Showing diffs for the active thread. +

+ )} + {checkpointDiffError && !renderablePatch && ( +

{checkpointDiffError}

+ )} + {!renderablePatch ? ( +
+ {isLoadingCheckpointDiff + ? "Loading checkpoint diff..." + : "No patch available for this selection."} +
+ ) : renderablePatch.kind === "files" ? ( +
+ {renderableFiles.map((fileDiff) => ( +
+ +
+ ))} +
+ ) : ( +
+

{renderablePatch.reason}

+
+                    {renderablePatch.text}
+                  
+
+ )} +
+ + )} + + ); } diff --git a/apps/web/src/lib/diffRendering.test.ts b/apps/web/src/lib/diffRendering.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..08bc25b690f56d0c2bd5e91014d0c677245eaae4 --- /dev/null +++ b/apps/web/src/lib/diffRendering.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { buildPatchCacheKey } from "./diffRendering"; + +describe("buildPatchCacheKey", () => { + it("returns a stable cache key for identical content", () => { + const patch = "diff --git a/a.ts b/a.ts\n+console.log('hello')"; + + expect(buildPatchCacheKey(patch)).toBe(buildPatchCacheKey(patch)); + }); + + it("normalizes outer whitespace before hashing", () => { + const patch = "diff --git a/a.ts b/a.ts\n+console.log('hello')"; + + expect(buildPatchCacheKey(`\n${patch}\n`)).toBe(buildPatchCacheKey(patch)); + }); + + it("changes when diff content changes", () => { + const before = "diff --git a/a.ts b/a.ts\n+console.log('hello')"; + const after = "diff --git a/a.ts b/a.ts\n+console.log('hello world')"; + + expect(buildPatchCacheKey(before)).not.toBe(buildPatchCacheKey(after)); + }); +}); diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5631ef8583b0c578c7250bbeb3dcb7760cf4f59 --- /dev/null +++ b/apps/web/src/lib/diffRendering.ts @@ -0,0 +1,24 @@ +const FNV_OFFSET_BASIS_32 = 0x811c9dc5; +const FNV_PRIME_32 = 0x01000193; +const SECONDARY_HASH_SEED = 0x9e3779b9; +const SECONDARY_HASH_MULTIPLIER = 0x85ebca6b; + +function fnv1a32(input: string, seed: number, multiplier: number): number { + let hash = seed >>> 0; + for (let index = 0; index < input.length; index += 1) { + hash ^= input.charCodeAt(index); + hash = Math.imul(hash, multiplier) >>> 0; + } + return hash >>> 0; +} + +export function buildPatchCacheKey(patch: string, scope = "diff-panel"): string { + const normalizedPatch = patch.trim(); + const primary = fnv1a32(normalizedPatch, FNV_OFFSET_BASIS_32, FNV_PRIME_32).toString(36); + const secondary = fnv1a32( + normalizedPatch, + SECONDARY_HASH_SEED, + SECONDARY_HASH_MULTIPLIER, + ).toString(36); + return `${scope}:${normalizedPatch.length}:${primary}:${secondary}`; +} diff --git a/apps/web/src/workers/diffs.worker.ts b/apps/web/src/workers/diffs.worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cbb6a35087fbe93bc5a044f0747754410952441 --- /dev/null +++ b/apps/web/src/workers/diffs.worker.ts @@ -0,0 +1 @@ +import "@pierre/diffs/worker/worker.js"; -- 2.54.0