perf(worktree): skip unchanged store updates and content-aware persist (#1992)
* perf(worktree): skip unchanged store updates and content-aware persist - Add content-aware equality check before setState in all three discovery loops (SessionSidebar, ElectronMiniChatApp, MobileApp). Compares Map size and per-entry length + element references — avoids triggering 16+ subscriber re-renders when discovery finds the same worktrees. - Add content-hash guard to persistWorktreeMap subscription with try-catch. Avoids redundant localStorage writes when the Map reference changed but the content is identical. Serialization errors are caught and skipped. Contributes to #1990 * perf(worktree): extract shared worktreeMapsEqual, fix comparison, avoid double serialization - Extract worktreeMapsEqual() into worktreeManager.ts as a shared utility comparing worktree maps by path (not reference identity). This replaces the inline reference-comparison logic in all three discovery loops (SessionSidebar, ElectronMiniChatApp, MobileApp) that was ineffective because readStableProjectWorktrees creates new object instances on each call after cache expiry, making item !== value[i] always true. - Pass pre-serialized JSON to persistWorktreeMap to avoid double JSON.stringify on every persist. The subscriber already computes the serialized string for the content-hash check; pass it through instead of re-serializing inside persistWorktreeMap. - Deduplicate 3 copies of the same comparison logic into the shared util. * refactor(worktree): make worktreeMapsEqual generic over path-bearing type The helper's equality contract is element-wise path comparison, not anything specific to WorktreeMetadata. Generifying on `T extends { path: string }` documents the contract at the type level and keeps it reusable for any future map-of-arrays shape that has a path field. Call sites stay compatible since WorktreeMetadata has a required `path: string`. No runtime change. * refactor(worktree-store): clarify persist hash name and signature Drop the optional preSerialized parameter from persistWorktreeMap — its only caller (the subscriber) already builds the serialized string for the content-compare, so the dual-path body is dead code. persistWorktreeMap now takes the serialized string directly. Rename lastPersistedWorktreeHash → lastPersistedWorktreeSerialized (the variable holds the full JSON string, not a hash) and drop the try/catch around JSON.stringify: it cannot realistically throw on Map.entries() of WorktreeMetadata (no circular refs, no BigInt, no custom toJSON). The try/catch around setItem stays — it can throw on quota errors. No behavior change in the success path. * docs(worktree): trim repeated call-site comments Replace the 5-line explanation block (copy-pasted in all three discovery loops) with a one-liner that points at the worktreeMapsEqual JSDoc. The '16+ subscribers' framing is also dropped — the helper itself is general-purpose and the precise number was fuzzy. * fix(worktree): compare branch in worktreeMapsEqual to avoid stale sidebar label The helper compared entries by path only. An external git checkout between discoveries changes branch (and the derived label / headState) while path stays the same, so the helper returned true and the store update was skipped — leaving a stale branch label in the sidebar until the next worktree create/remove or project switch, since there is no periodic worktree-list refresh. Compare branch in the inner loop alongside path. Tighten the generic constraint to T extends { path: string; branch: string } so the contract is documented at the type level. worktreeStatus is intentionally NOT compared: status transitions go through setStoredWorktreeStatus, which writes a fresh Map reference that the persist subscriber picks up directly. Adding worktreeStatus to the contract would also force the sidebar to detect status changes that the persist path already handles, and would couple this helper to a field whose semantics differ from the discovery path. Fixes the staleness concern raised by openchamber-bot in PR #1992. * test(worktree): cover worktreeMapsEqual edge cases Documents the helper's equality contract and guards against regressions in the path+branch comparison. Eight cases: - two empty maps - identical entries (path and branch match in order) - same path, different branch — the F1 regression case - different paths at the same index - per-project array length mismatch - project-key count mismatch - positional reorder (helper is order-sensitive) - non-first-entry branch difference (subset detection) All 10 tests in the file pass (2 existing + 8 new). * ci: retrigger checks * test(worktree): add benchmark for worktreeMapsEqual and persist path Documents the actual cost of the PR #1992 optimizations on representative sizes (1-1000 worktrees per project, 1-50 projects), so future contributors can reproduce the numbers and detect regressions in the equality helper or the persist subscriber. Run with: `bun run packages/ui/src/lib/worktrees/worktreeManager.bench.ts` Measured on V8 (one example run): - worktreeMapsEqual early-exit (50×20 with first project differing): 412 ns/op vs 33,034 ns/op full sweep — ~80x speedup when any project actually changed. - F1 path+branch overhead vs path-only (10×50): +2.3 µs (+15.8%) on a full sweep; on the early-exit path the F1 cost is irrelevant. - Stringify dedup in persistWorktreeMap subscriber: 67% saved (552 µs per persist on 10×50). This is the main absolute win of the PR. - Content-compare guard: 19-29 ns/op, free relative to the stringify it gates. Bench file is standalone (import.meta.main guard) — does not run as part of `bun test`, does not import React, does not touch localStorage. --------- Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
bashrusakh
parent
76c3bb5fd9
commit
6d7ea82d86
@@ -538,9 +538,9 @@ const loadPersistedWorktreeMap = (): Map<string, WorktreeMetadata[]> => {
|
||||
}
|
||||
}
|
||||
|
||||
const persistWorktreeMap = (map: Map<string, WorktreeMetadata[]>): void => {
|
||||
const persistWorktreeMap = (serialized: string): void => {
|
||||
try {
|
||||
getDeferredSafeStorage().setItem(WORKTREE_MAP_STORAGE_KEY, JSON.stringify([...map.entries()]))
|
||||
getDeferredSafeStorage().setItem(WORKTREE_MAP_STORAGE_KEY, serialized)
|
||||
} catch {
|
||||
// quota / serialization error — ignore; discovery still refreshes at runtime
|
||||
}
|
||||
@@ -1567,10 +1567,17 @@ setSessionOpener((sessionID, directory) => {
|
||||
})
|
||||
|
||||
// Write-through persist of the worktree map whenever discovery refreshes it.
|
||||
// Cheap reference-equality guard — this fires only when the map actually
|
||||
// changes (discovery / worktree create/remove), not on hot session updates.
|
||||
// Reference-equality guard filters hot session updates; the serialized
|
||||
// comparison avoids redundant localStorage writes when the Map reference
|
||||
// changed but the content is identical (e.g., re-discovery that found the
|
||||
// same worktrees).
|
||||
let lastPersistedWorktreeSerialized = ''
|
||||
useSessionUIStore.subscribe((state, prev) => {
|
||||
if (state.availableWorktreesByProject !== prev.availableWorktreesByProject) {
|
||||
persistWorktreeMap(state.availableWorktreesByProject)
|
||||
const serialized = JSON.stringify([...state.availableWorktreesByProject.entries()])
|
||||
if (serialized !== lastPersistedWorktreeSerialized) {
|
||||
lastPersistedWorktreeSerialized = serialized
|
||||
persistWorktreeMap(serialized)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user