From c66d873ff59720e82b24bf0eb309fabd80dedc0e Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 28 Oct 2026 14:29:41 -0700 Subject: [PATCH] Reuse Explorer watcher child indexes across unchanged payloads (#35319) * perf(explorer): reuse watcher child indexes by listing identity * test(explorer): trim child index reuse tests to reuse and invalidation --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../file-explorer-watch-child-index.test.ts | 73 +++++++++++++++++++ .../file-explorer-watch-reconcile.ts | 23 +++--- 2 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/file-explorer-watch-child-index.test.ts diff --git a/src/renderer/src/components/right-sidebar/file-explorer-watch-child-index.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-watch-child-index.test.ts new file mode 100644 index 00000000000..ef6eea7ab72 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/file-explorer-watch-child-index.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from 'vitest' +import type { DirCache, TreeNode } from './file-explorer-types' +import { processFileExplorerFsPayload } from './file-explorer-watch-reconcile' + +function reconcile( + cache: Record, + absolutePath: string +): { dir: ReturnType; tree: ReturnType } { + const dir = vi.fn() + const tree = vi.fn() + processFileExplorerFsPayload({ + payload: { + worktreePath: '/repo', + events: [{ kind: 'update', absolutePath, isDirectory: false }] + }, + currentWorktreePath: '/repo', + worktreeId: 'wt-1', + cache, + expanded: new Set(), + setDirCache: vi.fn(), + setSelectedPath: vi.fn(), + refreshDir: dir, + refreshTree: tree + }) + return { dir, tree } +} + +function node(path: string): TreeNode { + return { + path, + name: path, + relativePath: path.slice('/repo/'.length), + isDirectory: false, + isSymlink: false, + depth: 0, + operationOwner: { kind: 'local' } + } +} + +describe('watcher sibling index reuse', () => { + it('indexes unchanged siblings once across update payloads', () => { + let reads = 0 + const children = Array.from({ length: 1_000 }, (_, index) => { + const path = `/repo/file-${index}.ts` + const child = node(path) + Object.defineProperty(child, 'path', { + get: () => { + reads++ + return path + } + }) + return child + }) + const cache = { '/repo': { children } } + for (let payload = 0; payload < 10; payload++) { + const refresh = reconcile(cache, '/repo/file-0.ts') + expect(refresh.dir).not.toHaveBeenCalled() + expect(refresh.tree).not.toHaveBeenCalled() + } + expect(reads).toBe(1_000) + }) + + it('reindexes replaced listings while reusing an unchanged array through cache copies', () => { + const children = [node('/repo/old.ts')] + const before = { '/repo': { children } } + expect(reconcile(before, '/repo/old.ts').dir).not.toHaveBeenCalled() + expect(reconcile({ ...before }, '/repo/old.ts').dir).not.toHaveBeenCalled() + + const after = { '/repo': { children: [node('/repo/new.ts')] } } + expect(reconcile(after, '/repo/new.ts').dir).not.toHaveBeenCalled() + expect(reconcile(after, '/repo/old.ts').dir).toHaveBeenCalledWith('/repo') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts index b82b38bf76d..2da75cc7d01 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts @@ -31,21 +31,23 @@ export type ProcessFileExplorerFsPayloadArgs = { refreshTree: () => void } +const cachedChildPathIndexes = new WeakMap>() + function cachedDirectoryChild( cache: Record, cachedDirPath: string, - childPath: string, - childPathIndexes: Map> + childPath: string ): TreeNode | undefined { - let childPaths = childPathIndexes.get(cachedDirPath) + const children = cache[cachedDirPath]?.children + if (!children) { + return undefined + } + let childPaths = cachedChildPathIndexes.get(children) if (!childPaths) { childPaths = new Map( - cache[cachedDirPath]?.children.map((child) => [ - normalizeRuntimePathForComparison(child.path), - child - ]) ?? [] + children.map((child) => [normalizeRuntimePathForComparison(child.path), child]) ) - childPathIndexes.set(cachedDirPath, childPaths) + cachedChildPathIndexes.set(children, childPaths) } return childPaths.get(normalizeRuntimePathForComparison(childPath)) } @@ -83,7 +85,6 @@ export function processFileExplorerFsPayload(args: ProcessFileExplorerFsPayloadA } const dirsToRefresh = new Set() - const childPathIndexes = new Map>() let hasLinkedCache: boolean | undefined let cachedDirKeys: string[] | undefined let cachedDirPathIndex: ReadonlyMap | undefined @@ -114,7 +115,7 @@ export function processFileExplorerFsPayload(args: ProcessFileExplorerFsPayloadA const cachedParent = resolveCachedDirPath(cache, parent, currentWorktreePath, cachePathIndex) const updatedChild = evt.kind === 'update' && cachedParent - ? cachedDirectoryChild(cache, cachedParent, normalizedPath, childPathIndexes) + ? cachedDirectoryChild(cache, cachedParent, normalizedPath) : undefined const knownFileUpdate = evt.kind === 'update' && @@ -227,7 +228,7 @@ export function processFileExplorerFsPayload(args: ProcessFileExplorerFsPayloadA if ( cachedParent && !dirsToRefresh.has(cachedParent) && - !cachedDirectoryChild(cache, cachedParent, normalizedPath, childPathIndexes) + !cachedDirectoryChild(cache, cachedParent, normalizedPath) ) { dirsToRefresh.add(cachedParent) }