Skip to content
Open
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
Expand Up @@ -77,3 +77,6 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct
## 2024-07-13 - [Optimize Export Dictionary FK lookups]
**Learning:** Found O(N * C * E) performance bottleneck in ERD export dictionaries due to repeated array searching with `edges.some()` inside a nested loop over nodes and columns.
**Action:** Replace repeated linear array scans for edges by precomputing O(1) Set lookups of foreign key column handles per node before looping.
## 2026-07-16 - Avoid React.memo breakage in React Flow high-frequency updates
**Learning:** During high-frequency updates in React Flow (like node dragging at 60fps), `nodes` array mapping that creates a new `data` object reference (e.g., to inject `isDimmed`/`isHighlighted`) breaks the fast-path `prev.data === next.data` in `TableNode`'s `React.memo`, triggering deep re-renders across the entire graph. Additionally, O(N*C) computations (like text search over all nodes' columns) attached to the `nodes` dependency array will be redundantly re-evaluated on every frame.
**Action:** Since React Flow creates new object references for nodes when dragged but keeps the `node.data` reference stable, use a `WeakMap` keyed by `node.data` to cache derived or decorated data and search results. This preserves object identity, avoiding full re-renders and repeated heavy computations during dragging operations.
26 changes: 21 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,33 @@ export default function App() {
const searchMatchedNodeIds = useMemo(() => {
return findSearchMatchedNodeIds(nodes, normalizedNodeSearch);
}, [nodes, normalizedNodeSearch]);

const visibleNodesDataCache = useRef(new WeakMap<TableNodeData, Map<boolean, TableNodeData>>());

const visibleNodes = useMemo(() => {
if (!normalizedNodeSearch) return nodes;
return nodes.map((node) => {
const isHighlighted = searchMatchedNodeIds.has(node.id);
return {
...node,
data: {

let cacheMap = visibleNodesDataCache.current.get(node.data);
if (!cacheMap) {
cacheMap = new Map();
visibleNodesDataCache.current.set(node.data, cacheMap);
}

let decoratedData = cacheMap.get(isHighlighted);
if (!decoratedData) {
decoratedData = {
...node.data,
isDimmed: !isHighlighted,
isHighlighted,
},
};
cacheMap.set(isHighlighted, decoratedData);
}

return {
...node,
data: decoratedData,
};
});
}, [nodes, normalizedNodeSearch, searchMatchedNodeIds]);
Expand Down Expand Up @@ -382,7 +398,7 @@ export default function App() {
}, [
cardinalityColumnSelections,
cardinalityDistinctCounts,
cardinalityNode,
cardinalityNode?.data,
]);
const cardinalityRecommendations = useMemo(
() =>
Expand Down
25 changes: 23 additions & 2 deletions frontend/src/erd/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,41 @@ export function tableNodeMatchesSearch(
return terms.every((term) => nodeIncludesTerm(node, term));
}

// ⚡ Bolt: WeakMap to cache search matches by node.data.
// Since `node.data` remains stable during drags, this avoids re-evaluating the O(N*C) search function
// across all nodes at 60fps when dragging while a search filter is active.
const searchCache = new WeakMap<TableNodeData, Map<string, boolean>>();

export function findSearchMatchedNodeIds(
nodes: Array<Node<TableNodeData>>,
search: string,
): Set<string> {
const matches = new Set<string>();
// ⚡ Bolt: Parse search terms ONCE outside the loop (O(1)) instead of inside tableNodeMatchesSearch for every node (O(N)),
// eliminating redundant string allocations, regex splits, and Sets per node.
const trimmedSearch = search.trim().toLocaleLowerCase();
const terms = Array.from(
new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)),
new Set(trimmedSearch.split(/\s+/).filter(Boolean)),
);
if (terms.length === 0) return matches;

// Use a predictable cache key for this search.
const searchKey = terms.join(" ");

for (const node of nodes) {
if (tableNodeMatchesSearch(node, terms)) {
let nodeCache = searchCache.get(node.data);
if (!nodeCache) {
nodeCache = new Map();
searchCache.set(node.data, nodeCache);
}

let isMatch = nodeCache.get(searchKey);
if (isMatch === undefined) {
isMatch = tableNodeMatchesSearch(node, terms);
nodeCache.set(searchKey, isMatch);
}

if (isMatch) {
matches.add(node.id);
}
}
Expand Down
Loading