mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
44e1fee31b
Three hot paths in the dashboard ran `layer.nodeIds.includes(node.id)`, which is O(K) per check. Combined with their enclosing loops they collectively spent quadratic time per render of the overview / per filter recompute / per node-selection event. On the 4.8 MB knowledge graph reported in #102, the overview render alone took ~470 ms of synchronous main-thread work before ELK / React Flow ran — long enough for the page to register as unresponsive. Fix: precompute two indexes once when a graph is loaded. - `nodesById: Map<string, GraphNode>` - `nodeIdToLayerId: Map<string, string>` (first layer wins, matching prior `findNodeLayer` semantics) Both live in `useDashboardStore` and are rebuilt by `setGraph`. The three call sites: 1. `useOverviewGraph` (GraphView.tsx) — per-layer complexity aggregation moved into a new `computeLayerStats(layer, nodesById)` helper that walks `layer.nodeIds` instead of filtering all `graph.nodes`. Search match counts now read straight from `nodeIdToLayerId` instead of rebuilding a layer index on every searchResults change. 2. `filterNodes` (utils/filters.ts) — takes `nodeIdToLayerId` instead of `Layer[]`; the layer-membership check is one Map.get() per node. Updated `ExportMenu.tsx` caller to pass the store-level index. 3. `findNodeLayer` (store.ts) — replaced with `nodeIdToLayerId.get()` at the four call sites. `navigateTourToLayer` helper updated to take the index rather than the whole graph. Behavior is preserved exactly: - "First layer wins" semantics for nodes that appear in multiple layers (#102 schema doesn't forbid this). - 30 % aggregate-complexity threshold pinned by tests. - Layer filter that excludes layer-less orphans, but ungated when no layers are selected. Verified locally: Bench (`scripts/benchmark-aggregations.mjs`, node 22): 100 layers × 200 nodes (#102 shape): 475 ms → 2 ms (232× faster) 50 layers × 200 nodes: 116 ms → 0.6 ms (190× faster) 30 layers × 100 nodes: 12 ms → 0.2 ms (63× faster) Tests: `pnpm --filter @understand-anything/dashboard test` 24 → 41 pass (+17 new tests across `layerStats.test.ts` and `filters.test.ts`, including a #102 perf-regression guard at 100 layers × 100 nodes < 50 ms). `pnpm --filter @understand-anything/core test` — 654 / 654 pass. `pnpm --filter @understand-anything/dashboard exec tsc -b` — clean. `pnpm --filter @understand-anything/dashboard build` — clean. Pre-existing on master and not from this branch: `pnpm lint` errors out with "eslint: command not found" — `eslint` isn't installed by any package and the root `lint` script is bare `eslint .`. Out of scope here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.3 KiB
JavaScript
100 lines
3.3 KiB
JavaScript
// Per-layer aggregation perf benchmark.
|
||
//
|
||
// Mirrors the BEFORE shape (graph.nodes.filter(n => layer.nodeIds.includes(n.id))
|
||
// per layer) and the AFTER shape (single nodesById Map + iterate layer.nodeIds)
|
||
// from `useOverviewGraph` in `src/components/GraphView.tsx`. Issue #102 reported
|
||
// a 4.8 MB knowledge graph that froze the dashboard on overview render — the
|
||
// quadratic Array.includes pass was the dominant synchronous cost.
|
||
//
|
||
// We can't import the dashboard helper directly (Vite-bundled, no
|
||
// per-module dist), so the new shape is reproduced here in lockstep with
|
||
// `src/utils/layerStats.ts::computeLayerStats`.
|
||
//
|
||
// Usage:
|
||
// node understand-anything-plugin/packages/dashboard/scripts/benchmark-aggregations.mjs
|
||
|
||
import { performance } from "node:perf_hooks";
|
||
|
||
function makeGraph(layerCount, nodesPerLayer) {
|
||
const nodes = [];
|
||
const layers = [];
|
||
for (let li = 0; li < layerCount; li++) {
|
||
const ids = [];
|
||
for (let ni = 0; ni < nodesPerLayer; ni++) {
|
||
const id = `n-${li}-${ni}`;
|
||
const complexity = ["simple", "moderate", "complex"][(li + ni) % 3];
|
||
nodes.push({ id, complexity });
|
||
ids.push(id);
|
||
}
|
||
layers.push({ id: `L${li}`, nodeIds: ids });
|
||
}
|
||
return { nodes, layers };
|
||
}
|
||
|
||
// --- BEFORE: O(N × K × L) per overview render ----------------------------
|
||
function aggregateBefore(graph) {
|
||
const out = [];
|
||
for (const layer of graph.layers) {
|
||
const memberNodes = graph.nodes.filter((n) => layer.nodeIds.includes(n.id));
|
||
const c = { simple: 0, moderate: 0, complex: 0 };
|
||
for (const n of memberNodes) c[n.complexity]++;
|
||
const aggregate =
|
||
c.complex > memberNodes.length * 0.3
|
||
? "complex"
|
||
: c.moderate > memberNodes.length * 0.3
|
||
? "moderate"
|
||
: "simple";
|
||
out.push({ id: layer.id, aggregateComplexity: aggregate });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// --- AFTER: O(N + Σ K_i) per overview render ----------------------------
|
||
function aggregateAfter(graph, nodesById) {
|
||
const out = [];
|
||
for (const layer of graph.layers) {
|
||
const c = { simple: 0, moderate: 0, complex: 0 };
|
||
let resolved = 0;
|
||
for (const nid of layer.nodeIds) {
|
||
const node = nodesById.get(nid);
|
||
if (!node) continue;
|
||
resolved++;
|
||
c[node.complexity]++;
|
||
}
|
||
const aggregate =
|
||
c.complex > resolved * 0.3
|
||
? "complex"
|
||
: c.moderate > resolved * 0.3
|
||
? "moderate"
|
||
: "simple";
|
||
out.push({ id: layer.id, aggregateComplexity: aggregate });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function bench(label, layerCount, nodesPerLayer) {
|
||
const graph = makeGraph(layerCount, nodesPerLayer);
|
||
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
||
|
||
const t0 = performance.now();
|
||
const before = aggregateBefore(graph);
|
||
const t1 = performance.now();
|
||
const after = aggregateAfter(graph, nodesById);
|
||
const t2 = performance.now();
|
||
|
||
const beforeMs = t1 - t0;
|
||
const afterMs = t2 - t1;
|
||
const speedup = afterMs > 0 ? beforeMs / afterMs : Infinity;
|
||
const parity = JSON.stringify(before) === JSON.stringify(after);
|
||
console.log(
|
||
`${label} (${layerCount} layers × ${nodesPerLayer} nodes = ${graph.nodes.length} total): ` +
|
||
`BEFORE ${beforeMs.toFixed(1)}ms | AFTER ${afterMs.toFixed(1)}ms | ` +
|
||
`${speedup.toFixed(1)}× faster | parity ${parity}`,
|
||
);
|
||
}
|
||
|
||
bench("small", 10, 50);
|
||
bench("medium", 30, 100);
|
||
bench("large", 50, 200);
|
||
bench("issue#102 shape", 100, 200);
|