Files
d ๐Ÿ”น 44e1fee31b fix(dashboard): O(N+K) per-layer aggregations, kill quadratic Array.includes (#102)
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>
44e1fee31b ยท 2026-05-04 11:49:20 +08:00
History
..