Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-06-06 - VaultSidebar.tsx Tree Building and Search O(N*V) Bottleneck
**Learning:** React `useMemo` blocks handling dual responsibilities (e.g. static tree grouping + dynamic search filtering) force expensive tree-rebuilding on every keystroke. Furthermore, using `array.find()` inside a search loop creates an O(N * V) complexity trap that drastically degrades typing performance in large vaults.
**Action:** Always split large `useMemo` blocks by caching frequency (e.g., separate tree-shaping from search-filtering). Ensure lookups inside search loops utilize pre-computed hash maps (like `vaultById`) instead of linear array scans to maintain O(1) time complexity per node.
30 changes: 13 additions & 17 deletions ui/components/VaultSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,15 +665,8 @@ function VaultSidebar({
// ---------------------------------------------------------------------------
// Build structured tree data with search-match metadata
// ---------------------------------------------------------------------------
const {
topLevelVaults,
childrenByParent,
nodesByVaultId,
matchingVaultIds,
matchingSubVaultIds,
matchingNodeIds,
resultCount,
} = useMemo(() => {
// ⚡ Bolt: Split tree building from search filtering to avoid rebuilding the tree on every keystroke
const { topLevelVaults, childrenByParent, nodesByVaultId } = useMemo(() => {
const childMap = new Map<string, Vault[]>();
for (const vault of vaults) {
if (!vault.parentVaultId) {
Expand Down Expand Up @@ -701,12 +694,18 @@ function VaultSidebar({
nodeMap.set(parentKey, existing);
}

return {
topLevelVaults: roots,
childrenByParent: childMap,
nodesByVaultId: nodeMap,
};
}, [allNodes, vaults]);

// ⚡ Bolt: Isolated search filtering into its own useMemo and replaced O(N) array.find() with O(1) hash map lookup
const { matchingVaultIds, matchingSubVaultIds, matchingNodeIds, resultCount } = useMemo(() => {
// Without a search query, show everything normally
if (!normalizedQuery) {
return {
topLevelVaults: roots,
childrenByParent: childMap,
nodesByVaultId: nodeMap,
matchingVaultIds: new Set<string>(),
matchingSubVaultIds: new Set<string>(),
matchingNodeIds: new Set<string>(),
Expand All @@ -731,7 +730,7 @@ function VaultSidebar({
if (node.subVaultId) {
matchSubVaults.add(node.subVaultId);
// Also find the root vault for this sub-vault
const subVault = vaults.find((v) => v.id === node.subVaultId);
const subVault = vaultById[node.subVaultId];
if (subVault?.parentVaultId) {
matchVaults.add(subVault.parentVaultId);
}
Comment on lines 730 to 736

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The lookup of subVault from vaultById to find parentVaultId is redundant.

In a two-level vault hierarchy, node.vaultId is already the top-level (parent) vault ID. Therefore, subVault.parentVaultId is always equal to node.vaultId. Since matchVaults.add(node.vaultId) is already called unconditionally on line 738, we can completely eliminate this lookup and simplify the block.

        if (node.subVaultId) {
          matchSubVaults.add(node.subVaultId);
        }

Expand All @@ -754,15 +753,12 @@ function VaultSidebar({
}

return {
topLevelVaults: roots,
childrenByParent: childMap,
nodesByVaultId: nodeMap,
matchingVaultIds: matchVaults,
matchingSubVaultIds: matchSubVaults,
matchingNodeIds: matchNodes,
resultCount: count,
};
}, [allNodes, normalizedQuery, vaults]);
}, [allNodes, normalizedQuery, vaults, vaultById]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since vaultById is no longer used in this useMemo block after removing the redundant lookup, we can remove it from the dependency array. This prevents unnecessary dependency tracking and potential re-runs.

Suggested change
}, [allNodes, normalizedQuery, vaults, vaultById]);
}, [allNodes, normalizedQuery, vaults]);


// ---------------------------------------------------------------------------
// Helpers for determining match/dim state
Expand Down