Skip to content

perf(layout): keep Smart Layout's candidate scoring off the main thread - #105

Merged
tomasz-zajac-oss merged 1 commit into
mainfrom
feature/smart-layout-off-main-thread
Sep 28, 2026
Merged

tomasz-zajac-oss merged 1 commit into
mainfrom
feature/smart-layout-off-main-thread

Conversation

@tomasz-zajac-oss

Copy link
Copy Markdown
Collaborator

Follow-up to #104.

Summary

On the whole bundled sample model, Smart Layout's ELK candidate phase blocked the renderer for ~3.4 s. Profiling showed almost all of that was minimizeCrossings (300–400 ms per candidate × 10), not ELK itself.

  • Faster minimizeCrossings. Each swap or shift is scored by only the pairs it can change (localCost), instead of re-scoring the whole group before and after. Costs are exact integer counts, so every accept/reject decision is the same as before.
    • Output was checked against the previous implementation on 55 inputs: sample-model views × 5 engines, synthetic landscapes and 20 random scatters. It was identical on all of them, at 5.9× the speed.
    • This also speeds up Radical Layout, which calls minimizeCrossings on the main thread in the store.
  • Scoring in the worker. The main thread now only runs the layout engines (RawCandidate). Crossing minimisation, finalising and scoring of every candidate moved into the Smart Layout worker. runSmartLayoutSAPhase became runSmartLayoutWorkerPhase, and the button shows a new "Scoring layouts… N/10" stage.
  • Engines one at a time. Bundled ELK lays out synchronously, so ten queued layouts used to run as one uninterrupted ~220–380 ms block. They now run one at a time with a yield in between, so "N/10 algorithms tried" advances live.

Results

Measured against #104 (ac1a105). "Longest block" is the longest gap between 1 ms event-loop ticks.

Input Main thread Longest block Total
Sample: whole model 3442 → 398 ms 323 → 79 ms 5.1 → 2.6 s
Synthetic, 95 nodes 4570 → 428 ms 337 → 89 ms 6.7 → 3.3 s
Sample: Governance 247 → 224 ms 141 → 52 ms 0.81 → 0.81 s

Follow-up (not in this PR)

ELK itself still runs on the main thread, which accounts for the remaining ~80 ms cold block. Running it in its own worker (elk-api + elk-worker.min.js) would remove that, but it depends on how Vite/Electron serve the worker script and needs checking in the app.

Test plan

  • npm run typecheck
  • npm test on top of current main: 432 passed, 2 of them new. One checks that minimizeCrossings never raises the cost; the other checks that the ELK phase only runs the engines, one at a time, reporting each.
  • Old vs new minimizeCrossings output identical on 55 inputs.
  • Main-thread blocking and result equality measured against fix(layout): make Smart Layout refinement deterministic and never worse #104 on 5 inputs.
  • Run Smart Layout in the app on the whole sample model and confirm the UI stays responsive (not done: no app automation available).

🤖 Generated with Claude Code

On the bundled sample model the ELK candidate phase blocked the renderer
for ~3.4 s. Almost all of it was minimizeCrossings (300–400 ms per
candidate × 10), not ELK itself.

- minimizeCrossings scores each swap/shift by the pairs it can change
  instead of re-scoring the whole group. Costs are exact integer counts, so
  decisions are identical; ~6x faster (also speeds up Radical Layout, which
  calls it on the main thread).
- The main thread now only runs the layout engines. Crossing minimisation,
  finalising and scoring of every candidate move into the Smart Layout
  worker (runSmartLayoutWorkerPhase), with a "Scoring layouts… N/10"
  progress stage.
- Engines run one at a time with a yield in between: bundled ELK lays out
  synchronously, and ten queued layouts ran as one uninterrupted block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tomasz-zajac-oss
tomasz-zajac-oss merged commit 4d39a55 into main Sep 28, 2026
2 checks passed
@tomasz-zajac-oss
tomasz-zajac-oss deleted the feature/smart-layout-off-main-thread branch September 28, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant