Skip to content

Knowledge-graph view still blocks the main thread: synchronous d3-force layout (~4.3s at 3k nodes); layout.worker.ts is unused #491

Description

@DashBot-0001

Summary

The knowledge-graph view (/understand-knowledgeKnowledgeGraphView) still lays out the whole graph with a synchronous d3-force simulation on the main thread, inside a useMemo. On larger graphs this blocks the UI for several seconds on every layout/filter/search recompute. This looks like the remaining instance of #14 — the structural views moved to async ELK, but the force-directed knowledge-graph path didn't.

Evidence (benchmark)

Ran applyForceLayout (the function computeLayout calls) standalone over synthetic graphs — pure wall-clock of the synchronous call that runs on the main thread:

nodes ticks blocking time
200 200 ~230 ms
500 300 ~570 ms
1000 300 ~1.2 s
2000 300 ~2.8 s
3000 300 ~4.3 s

ticks = Math.min(300, Math.max(100, nodes.length)), so cost grows roughly linearly in ticks × Barnes-Hut forceManyBody per tick. (Happy to contribute the benchmark script — it mirrors the existing scripts/benchmark-layout.mjs / benchmark-aggregations.mjs.)

Root cause

  • src/components/KnowledgeGraphView.tsx:140-142useMemo(() => computeLayout(filteredGraph), …)
  • computeLayout (:51) → applyForceLayout (src/utils/layout.ts:94)
  • sim.tick(Math.min(300, Math.max(100, n))) (layout.ts:168-169) runs synchronously on the main thread.

There's no large-graph guard in KnowledgeGraphView (no node-count threshold / progressive disclosure), so the full simulation runs eagerly.

Two related observations

  1. src/utils/layout.worker.ts is dead codegrep -rn "layout.worker\|new Worker" src/ returns nothing; it's imported nowhere. It's a dagre worker, so it can't host the force sim as-is, but it shows the worker scaffolding was started and never wired up.
  2. Non-deterministic layoutapplyForceLayout seeds initial positions with Math.random() (layout.ts:105-106), so the knowledge graph re-shuffles to a different arrangement on every recompute (selection, filter, tour). d3-force's default phyllotaxis seeding (or a fixed seed) would make layouts stable across renders — and would also make a worker version byte-for-byte verifiable.

Suggested fix

Mirror the async pattern the structural views already use (applyElkLayout via useOverviewGraph / useLayerDetailTopology round-trip positions through setState): move applyForceLayout into a web worker, post {nodes, edges, communityMap} in, post positions back, and render a lightweight placeholder until they arrive. Optionally pair with the deterministic-seed change above so the worker output is verifiable against the main-thread output.

Environment for the numbers: Node-class runtime, d3-force@^3, the repo's own layout.ts. I can open a PR for the deterministic-seed change (small, self-contained) if that's a welcome first step while the worker-ization is scoped.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions