perf: optimize stacked diff view and Pierre diff rendering
- Compute default expanded count for stacked view to reduce DOM load - Move diff processing to worker pool to improve UI responsiveness - Add content cache key to avoid recomputing large diffs on render
This commit is contained in:
@@ -29,8 +29,14 @@ import { useDeviceInfo } from '@/lib/device';
|
|||||||
const SIDE_BY_SIDE_MIN_WIDTH = 1100;
|
const SIDE_BY_SIDE_MIN_WIDTH = 1100;
|
||||||
const DIFF_REQUEST_TIMEOUT_MS = 15000;
|
const DIFF_REQUEST_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
// Memory optimization: limit concurrent expanded diffs in stacked view
|
// Perf: limit concurrent expanded diffs in stacked view.
|
||||||
const STACKED_VIEW_MAX_EXPANDED_DIFFS = 10;
|
// Expanding many diffs mounts many Pierre instances + lots of DOM.
|
||||||
|
const getStackedViewDefaultExpandedCount = (fileCount: number): number => {
|
||||||
|
if (fileCount <= 6) return fileCount;
|
||||||
|
if (fileCount <= 12) return 6;
|
||||||
|
if (fileCount <= 25) return 4;
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
|
||||||
type FileEntry = GitStatus['files'][number] & {
|
type FileEntry = GitStatus['files'][number] & {
|
||||||
insertions: number;
|
insertions: number;
|
||||||
@@ -771,7 +777,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
Loading diff…
|
Loading diff…
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{diffData ? (
|
{isExpanded && diffData ? (
|
||||||
<InlineDiffViewer
|
<InlineDiffViewer
|
||||||
filePath={file.path}
|
filePath={file.path}
|
||||||
diff={diffData}
|
diff={diffData}
|
||||||
@@ -1054,6 +1060,8 @@ export const DiffView: React.FC = () => {
|
|||||||
const renderStackedDiffView = () => {
|
const renderStackedDiffView = () => {
|
||||||
if (!effectiveDirectory) return null;
|
if (!effectiveDirectory) return null;
|
||||||
|
|
||||||
|
const defaultExpandedCount = getStackedViewDefaultExpandedCount(changedFiles.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 min-h-0 h-full gap-3 px-3 pb-3 pt-2">
|
<div className="flex flex-1 min-h-0 h-full gap-3 px-3 pb-3 pt-2">
|
||||||
{showFileSidebar && (
|
{showFileSidebar && (
|
||||||
@@ -1087,7 +1095,7 @@ export const DiffView: React.FC = () => {
|
|||||||
isSelected={file.path === selectedFile}
|
isSelected={file.path === selectedFile}
|
||||||
onSelect={handleSelectFile}
|
onSelect={handleSelectFile}
|
||||||
registerSectionRef={registerSectionRef}
|
registerSectionRef={registerSectionRef}
|
||||||
defaultCollapsed={index >= STACKED_VIEW_MAX_EXPANDED_DIFFS}
|
defaultCollapsed={index >= defaultExpandedCount}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { FileDiff } from '@pierre/diffs/react';
|
import { FileDiff as PierreFileDiff, type FileContents, type FileDiffOptions, type SelectedLineRange } from '@pierre/diffs';
|
||||||
import { parseDiffFromFile, type FileContents, type FileDiffMetadata, type SelectedLineRange } from '@pierre/diffs';
|
|
||||||
import { RiSendPlane2Line } from '@remixicon/react';
|
import { RiSendPlane2Line } from '@remixicon/react';
|
||||||
|
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
|
import { useWorkerPool } from '@/contexts/DiffWorkerProvider';
|
||||||
import { ensurePierreThemeRegistered, getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
|
import { ensurePierreThemeRegistered, getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
|
||||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
|
|
||||||
@@ -96,15 +96,23 @@ const WEBKIT_SCROLL_FIX_CSS = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Fast cache key - use length + samples instead of full hash
|
// Fast cache key - use length + samples instead of full hash
|
||||||
function getCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
|
function fnv1a32(input: string): string {
|
||||||
// Sample a few characters instead of hashing entire content
|
// Fast + stable across runtimes; good enough for cache keys.
|
||||||
const sampleOriginal = original.length > 100
|
let hash = 0x811c9dc5;
|
||||||
? `${original.slice(0, 50)}${original.slice(-50)}`
|
for (let i = 0; i < input.length; i += 1) {
|
||||||
: original;
|
hash ^= input.charCodeAt(i);
|
||||||
const sampleModified = modified.length > 100
|
// hash *= 16777619 (but keep 32-bit)
|
||||||
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;
|
||||||
: modified;
|
}
|
||||||
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
return hash.toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeContentCacheKey(contents: string): string {
|
||||||
|
// Avoid hashing full file; sample head+tail.
|
||||||
|
const sample = contents.length > 400
|
||||||
|
? `${contents.slice(0, 200)}${contents.slice(-200)}`
|
||||||
|
: contents;
|
||||||
|
return `${contents.length}:${fnv1a32(sample)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractSelectedCode = (original: string, modified: string, range: SelectedLineRange): string => {
|
const extractSelectedCode = (original: string, modified: string, range: SelectedLineRange): string => {
|
||||||
@@ -315,6 +323,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
const diffThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
const diffThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
||||||
|
|
||||||
const diffRootRef = useRef<HTMLDivElement | null>(null);
|
const diffRootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const diffContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const diffInstanceRef = useRef<PierreFileDiff<unknown> | null>(null);
|
||||||
|
const workerPool = useWorkerPool();
|
||||||
|
|
||||||
const lightResolvedTheme = useMemo(() => getResolvedShikiTheme(lightTheme), [lightTheme]);
|
const lightResolvedTheme = useMemo(() => getResolvedShikiTheme(lightTheme), [lightTheme]);
|
||||||
const darkResolvedTheme = useMemo(() => getResolvedShikiTheme(darkTheme), [darkTheme]);
|
const darkResolvedTheme = useMemo(() => getResolvedShikiTheme(darkTheme), [darkTheme]);
|
||||||
@@ -393,43 +404,6 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
}
|
}
|
||||||
}, [darkResolvedTheme, diffThemeKey, isDark, lightResolvedTheme]);
|
}, [darkResolvedTheme, diffThemeKey, isDark, lightResolvedTheme]);
|
||||||
|
|
||||||
// Cache the last computed diff to avoid recomputing on every render
|
|
||||||
const diffCacheRef = useRef<{
|
|
||||||
key: string;
|
|
||||||
fileDiff: FileDiffMetadata;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
// Pre-parse the diff with cacheKey for worker pool caching
|
|
||||||
const fileDiff = useMemo(() => {
|
|
||||||
const cacheKey = getCacheKey(fileName, original, modified, diffThemeKey);
|
|
||||||
|
|
||||||
// Return cached diff if inputs haven't changed
|
|
||||||
if (diffCacheRef.current?.key === cacheKey) {
|
|
||||||
return diffCacheRef.current.fileDiff;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldFile: FileContents = {
|
|
||||||
name: fileName,
|
|
||||||
contents: original,
|
|
||||||
lang: language as FileContents['lang'],
|
|
||||||
cacheKey: `old-${cacheKey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newFile: FileContents = {
|
|
||||||
name: fileName,
|
|
||||||
contents: modified,
|
|
||||||
lang: language as FileContents['lang'],
|
|
||||||
cacheKey: `new-${cacheKey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const diff = parseDiffFromFile(oldFile, newFile);
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
|
||||||
|
|
||||||
return diff;
|
|
||||||
}, [diffThemeKey, fileName, original, modified, language]);
|
|
||||||
|
|
||||||
const options = useMemo(() => ({
|
const options = useMemo(() => ({
|
||||||
theme: {
|
theme: {
|
||||||
dark: darkTheme.metadata.id,
|
dark: darkTheme.metadata.id,
|
||||||
@@ -439,7 +413,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
|
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
|
||||||
diffIndicators: 'none' as const,
|
diffIndicators: 'none' as const,
|
||||||
hunkSeparators: 'line-info' as const,
|
hunkSeparators: 'line-info' as const,
|
||||||
lineDiffType: 'word-alt' as const,
|
// Perf: disable intra-line diff (word-level) globally.
|
||||||
|
lineDiffType: 'none' as const,
|
||||||
overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
|
overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
|
||||||
disableFileHeader: true,
|
disableFileHeader: true,
|
||||||
enableLineSelection: true,
|
enableLineSelection: true,
|
||||||
@@ -448,6 +423,61 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
||||||
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id, renderSideBySide, wrapLines, handleSelectionChange]);
|
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id, renderSideBySide, wrapLines, handleSelectionChange]);
|
||||||
|
|
||||||
|
// Imperative render (like upstream OpenCode): avoids `parseDiffFromFile` on main thread.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const container = diffContainerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
if (!workerPool) return;
|
||||||
|
|
||||||
|
// Dispose previous instance
|
||||||
|
diffInstanceRef.current?.cleanUp();
|
||||||
|
diffInstanceRef.current = null;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
const instance = new PierreFileDiff(options as unknown as FileDiffOptions<unknown>, workerPool);
|
||||||
|
diffInstanceRef.current = instance;
|
||||||
|
|
||||||
|
const oldFile: FileContents = {
|
||||||
|
name: fileName,
|
||||||
|
contents: original,
|
||||||
|
lang: language as FileContents['lang'],
|
||||||
|
cacheKey: `old:${diffThemeKey}:${fileName}:${makeContentCacheKey(original)}`,
|
||||||
|
};
|
||||||
|
const newFile: FileContents = {
|
||||||
|
name: fileName,
|
||||||
|
contents: modified,
|
||||||
|
lang: language as FileContents['lang'],
|
||||||
|
cacheKey: `new:${diffThemeKey}:${fileName}:${makeContentCacheKey(modified)}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.render({
|
||||||
|
oldFile,
|
||||||
|
newFile,
|
||||||
|
lineAnnotations: [],
|
||||||
|
containerWrapper: container,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
instance.cleanUp();
|
||||||
|
if (diffInstanceRef.current === instance) {
|
||||||
|
diffInstanceRef.current = null;
|
||||||
|
}
|
||||||
|
container.innerHTML = '';
|
||||||
|
};
|
||||||
|
}, [diffThemeKey, fileName, language, modified, options, original, workerPool]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const instance = diffInstanceRef.current;
|
||||||
|
if (!instance) return;
|
||||||
|
try {
|
||||||
|
instance.setSelectedLines(selection);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [selection]);
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -557,12 +587,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
fillContainer={true}
|
fillContainer={true}
|
||||||
>
|
>
|
||||||
<div ref={diffRootRef} className="size-full">
|
<div ref={diffRootRef} className="size-full">
|
||||||
<FileDiff
|
<div ref={diffContainerRef} className="size-full" />
|
||||||
key={diffThemeKey}
|
|
||||||
fileDiff={fileDiff}
|
|
||||||
options={options}
|
|
||||||
selectedLines={selection}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollableOverlay>
|
</ScrollableOverlay>
|
||||||
</div>
|
</div>
|
||||||
@@ -595,12 +620,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("relative", "w-full")}>
|
<div className={cn("relative", "w-full")}>
|
||||||
<div ref={diffRootRef} className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
|
<div ref={diffRootRef} className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
|
||||||
<FileDiff
|
<div ref={diffContainerRef} className="w-full" />
|
||||||
key={diffThemeKey}
|
|
||||||
fileDiff={fileDiff}
|
|
||||||
options={options}
|
|
||||||
selectedLines={selection}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selection && createPortal(
|
{selection && createPortal(
|
||||||
|
|||||||
@@ -1,84 +1,34 @@
|
|||||||
import React, { useMemo, useEffect, useRef } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { WorkerPoolContextProvider, useWorkerPool } from '@pierre/diffs/react';
|
import { WorkerPoolContextProvider, useWorkerPool } from '@pierre/diffs/react';
|
||||||
import { parseDiffFromFile, type FileContents } from '@pierre/diffs';
|
|
||||||
import type { SupportedLanguages } from '@pierre/diffs';
|
import type { SupportedLanguages } from '@pierre/diffs';
|
||||||
|
|
||||||
import { useOptionalThemeSystem } from './useThemeSystem';
|
import { useOptionalThemeSystem } from './useThemeSystem';
|
||||||
import { workerFactory } from '@/lib/diff/workerFactory';
|
import { workerFactory } from '@/lib/diff/workerFactory';
|
||||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
import { useGitStore } from '@/stores/useGitStore';
|
// NOTE: keep provider lightweight; avoid main-thread diff parsing here.
|
||||||
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
|
||||||
|
|
||||||
// Preload common languages for faster initial diff rendering
|
// Preload common languages for faster initial diff rendering
|
||||||
const PRELOAD_LANGS: SupportedLanguages[] = [
|
const PRELOAD_LANGS: SupportedLanguages[] = [
|
||||||
|
// Keep small; workers load others on-demand.
|
||||||
'typescript',
|
'typescript',
|
||||||
'javascript',
|
'javascript',
|
||||||
'tsx',
|
'tsx',
|
||||||
'jsx',
|
'jsx',
|
||||||
'json',
|
'json',
|
||||||
'html',
|
|
||||||
'css',
|
|
||||||
'markdown',
|
'markdown',
|
||||||
'yaml',
|
|
||||||
'python',
|
|
||||||
'rust',
|
|
||||||
'go',
|
|
||||||
'bash',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Limit warmup to prevent memory bloat with many modified files
|
|
||||||
const WARMUP_MAX_FILES = 10;
|
|
||||||
|
|
||||||
// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx`
|
|
||||||
function getPierreCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
|
|
||||||
const sampleOriginal = original.length > 100
|
|
||||||
? `${original.slice(0, 50)}${original.slice(-50)}`
|
|
||||||
: original;
|
|
||||||
const sampleModified = modified.length > 100
|
|
||||||
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
|
||||||
: modified;
|
|
||||||
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DiffWorkerProviderProps {
|
interface DiffWorkerProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdleDeadlineLike = { timeRemaining(): number; didTimeout: boolean };
|
|
||||||
|
|
||||||
function scheduleWarmupWork(cb: (deadline?: IdleDeadlineLike) => void): () => void {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = window.setTimeout(() => cb(undefined), 0);
|
|
||||||
return () => window.clearTimeout(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component that warms up the worker pool and precomputes diff ASTs
|
// Component that warms up the worker pool and precomputes diff ASTs
|
||||||
const WorkerPoolWarmup: React.FC<{
|
const WorkerPoolWarmup: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
themeKey: string;
|
|
||||||
renderTheme: { light: string; dark: string };
|
renderTheme: { light: string; dark: string };
|
||||||
}> = ({ children, themeKey, renderTheme }) => {
|
}> = ({ children, renderTheme }) => {
|
||||||
const workerPool = useWorkerPool();
|
const workerPool = useWorkerPool();
|
||||||
const activeDirectory = useGitStore((state) => state.activeDirectory);
|
|
||||||
const lastStatusChange = useGitStore((state) => {
|
|
||||||
if (!activeDirectory) return 0;
|
|
||||||
return state.directories.get(activeDirectory)?.lastStatusChange ?? 0;
|
|
||||||
});
|
|
||||||
const diffCacheSize = useGitStore((state) => {
|
|
||||||
if (!activeDirectory) return 0;
|
|
||||||
return state.directories.get(activeDirectory)?.diffCache.size ?? 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const didDummyWarmupRef = useRef(false);
|
|
||||||
const warmedStatusRef = useRef(new Map<string, number>());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
warmedStatusRef.current.clear();
|
|
||||||
}, [themeKey]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workerPool) {
|
if (!workerPool) {
|
||||||
@@ -87,110 +37,10 @@ const WorkerPoolWarmup: React.FC<{
|
|||||||
|
|
||||||
// Important: WorkerPoolContextProvider uses a singleton and does not react to
|
// Important: WorkerPoolContextProvider uses a singleton and does not react to
|
||||||
// prop changes. Update the worker pool render options explicitly.
|
// prop changes. Update the worker pool render options explicitly.
|
||||||
void workerPool.setRenderOptions({ theme: renderTheme });
|
// Force-disable intra-line diff globally (word-level/char-level).
|
||||||
|
void workerPool.setRenderOptions({ theme: renderTheme, lineDiffType: 'none' });
|
||||||
}, [renderTheme, workerPool]);
|
}, [renderTheme, workerPool]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workerPool || didDummyWarmupRef.current) return;
|
|
||||||
|
|
||||||
didDummyWarmupRef.current = true;
|
|
||||||
|
|
||||||
const dummyFile: FileContents = {
|
|
||||||
name: 'warmup.ts',
|
|
||||||
contents: 'const x = 1;',
|
|
||||||
lang: 'typescript',
|
|
||||||
cacheKey: 'warmup-file',
|
|
||||||
};
|
|
||||||
|
|
||||||
const dummyDiff = parseDiffFromFile(dummyFile, { ...dummyFile, contents: 'const x = 2;', cacheKey: 'warmup-file-2' });
|
|
||||||
|
|
||||||
const dummyInstance = {
|
|
||||||
onHighlightSuccess: () => {},
|
|
||||||
onHighlightError: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
workerPool.highlightDiffAST(dummyInstance, dummyDiff);
|
|
||||||
}, [workerPool]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workerPool || !activeDirectory) return;
|
|
||||||
if (!lastStatusChange || diffCacheSize === 0) return;
|
|
||||||
|
|
||||||
const dirState = useGitStore.getState().directories.get(activeDirectory);
|
|
||||||
if (!dirState || dirState.diffCache.size === 0) return;
|
|
||||||
|
|
||||||
const alreadyWarmedAt = warmedStatusRef.current.get(activeDirectory) ?? 0;
|
|
||||||
if (alreadyWarmedAt >= dirState.lastStatusChange) return;
|
|
||||||
|
|
||||||
// Only warm once per status-change tick
|
|
||||||
warmedStatusRef.current.set(activeDirectory, dirState.lastStatusChange);
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
let cancelScheduled: (() => void) | null = null;
|
|
||||||
|
|
||||||
// Limit entries to prevent memory bloat with many modified files
|
|
||||||
const allEntries = Array.from(dirState.diffCache.entries());
|
|
||||||
const entries = allEntries.slice(0, WARMUP_MAX_FILES);
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
const processChunk = (deadline?: IdleDeadlineLike) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
const chunkStart = Date.now();
|
|
||||||
while (index < entries.length) {
|
|
||||||
if (deadline && deadline.timeRemaining() < 8) break;
|
|
||||||
if (!deadline && Date.now() - chunkStart > 8) break;
|
|
||||||
|
|
||||||
const [filePath, diff] = entries[index];
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
const language = getLanguageFromExtension(filePath) || 'text';
|
|
||||||
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified, themeKey);
|
|
||||||
|
|
||||||
const oldFile: FileContents = {
|
|
||||||
name: filePath,
|
|
||||||
contents: diff.original,
|
|
||||||
lang: language as FileContents['lang'],
|
|
||||||
cacheKey: `old-${cacheKey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newFile: FileContents = {
|
|
||||||
name: filePath,
|
|
||||||
contents: diff.modified,
|
|
||||||
lang: language as FileContents['lang'],
|
|
||||||
cacheKey: `new-${cacheKey}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileDiff = parseDiffFromFile(oldFile, newFile);
|
|
||||||
|
|
||||||
// Use a unique instance per request so Pierre doesn't ignore earlier results.
|
|
||||||
const instance = {
|
|
||||||
onHighlightSuccess: () => {},
|
|
||||||
onHighlightError: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
workerPool.highlightDiffAST(instance, fileDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < entries.length) {
|
|
||||||
cancelScheduled = scheduleWarmupWork(processChunk);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kick off immediately, then continue in chunks.
|
|
||||||
processChunk(undefined);
|
|
||||||
|
|
||||||
if (index < entries.length) {
|
|
||||||
cancelScheduled = scheduleWarmupWork(processChunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
cancelScheduled?.();
|
|
||||||
};
|
|
||||||
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize, themeKey]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,8 +73,6 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
|||||||
langs: PRELOAD_LANGS,
|
langs: PRELOAD_LANGS,
|
||||||
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id]);
|
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id]);
|
||||||
|
|
||||||
const workerThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
|
||||||
|
|
||||||
const renderTheme = useMemo(
|
const renderTheme = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
light: lightTheme.metadata.id,
|
light: lightTheme.metadata.id,
|
||||||
@@ -243,7 +91,6 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
|||||||
highlighterOptions={highlighterOptions}
|
highlighterOptions={highlighterOptions}
|
||||||
>
|
>
|
||||||
<WorkerPoolWarmup
|
<WorkerPoolWarmup
|
||||||
themeKey={workerThemeKey}
|
|
||||||
renderTheme={renderTheme}
|
renderTheme={renderTheme}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user