perf(explorer): avoid full refresh after replayed events (#1827)
Skip the expensive expanded-tree refresh after deferred file explorer watcher payloads when the replayed events can be reconciled with targeted directory refreshes. Keep the fallback for unreplayable rename payloads.
This commit is contained in:
parent
105d0a50f8
commit
ee2b8b861c
|
|
@ -1,5 +1,9 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getExternalFileChangeRelativePath } from './useFileExplorerWatch'
|
||||
import type { FsChangedPayload } from '../../../../shared/types'
|
||||
import {
|
||||
getExternalFileChangeRelativePath,
|
||||
payloadRequiresDeferredTreeRefresh
|
||||
} from './useFileExplorerWatch'
|
||||
|
||||
describe('getExternalFileChangeRelativePath', () => {
|
||||
it('returns a worktree-relative file path for external file updates', () => {
|
||||
|
|
@ -57,3 +61,36 @@ describe('getExternalFileChangeRelativePath', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('payloadRequiresDeferredTreeRefresh', () => {
|
||||
function payload(events: FsChangedPayload['events'], worktreePath = '/repo'): FsChangedPayload {
|
||||
return { worktreePath, events }
|
||||
}
|
||||
|
||||
it('does not require a full tree refresh for replayable deferred changes', () => {
|
||||
const changes = payload([
|
||||
{ kind: 'create', absolutePath: '/repo/src/new.ts', isDirectory: false },
|
||||
{ kind: 'update', absolutePath: '/repo/src', isDirectory: true },
|
||||
{ kind: 'delete', absolutePath: '/repo/src/old.ts' }
|
||||
])
|
||||
|
||||
expect(payloadRequiresDeferredTreeRefresh(changes, '/repo')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires a full tree refresh for unreplayable rename payloads in the current worktree', () => {
|
||||
const changes = payload([
|
||||
{ kind: 'rename', absolutePath: '/repo/src/old.ts', isDirectory: false }
|
||||
])
|
||||
|
||||
expect(payloadRequiresDeferredTreeRefresh(changes, '/repo')).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores stale deferred rename payloads from a previous worktree', () => {
|
||||
const changes = payload(
|
||||
[{ kind: 'rename', absolutePath: '/other/src/old.ts', isDirectory: false }],
|
||||
'/other'
|
||||
)
|
||||
|
||||
expect(payloadRequiresDeferredTreeRefresh(changes, '/repo')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -57,6 +57,17 @@ export function getExternalFileChangeRelativePath(
|
|||
return normalizeRelativePath(normalizedAbsolutePath.slice(worktreePrefix.length))
|
||||
}
|
||||
|
||||
export function payloadRequiresDeferredTreeRefresh(
|
||||
payload: FsChangedPayload,
|
||||
currentWorktreePath: string
|
||||
): boolean {
|
||||
if (normalizeAbsolutePath(payload.worktreePath) !== normalizeAbsolutePath(currentWorktreePath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return payload.events.some((evt) => evt.kind === 'rename')
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciles File Explorer state on filesystem events for the active worktree.
|
||||
*
|
||||
|
|
@ -278,6 +289,9 @@ export function useFileExplorerWatch({
|
|||
deferredRef.current.length > 0
|
||||
) {
|
||||
const deferred = deferredRef.current.splice(0)
|
||||
const requiresFullRefresh = worktreePath
|
||||
? deferred.some((payload) => payloadRequiresDeferredTreeRefresh(payload, worktreePath))
|
||||
: false
|
||||
// Why: replay every deferred payload through `processPayload` so the
|
||||
// tree cache reconciles to disk state after inline input or drag ends
|
||||
// (design §6.2). Editor-tab reloads are handled independently by
|
||||
|
|
@ -288,11 +302,10 @@ export function useFileExplorerWatch({
|
|||
processPayloadRef.current(payload)
|
||||
}
|
||||
}
|
||||
// Why: also trigger a tree refresh as a safety net. Deferred events may
|
||||
// have been coalesced by the watcher or become stale during the defer
|
||||
// window, and a full refresh guarantees the explorer state converges on
|
||||
// disk reality even if individual event replay misses a subtree.
|
||||
if (worktreePath) {
|
||||
// Why: create/delete/update payloads replay into targeted refreshDir
|
||||
// calls above. Only event kinds this reconciler cannot apply safely
|
||||
// should pay the full expanded-tree refresh cost after a deferred flush.
|
||||
if (requiresFullRefresh) {
|
||||
void refreshTreeRef.current()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue