Skip to content

factory: make the cockpit fast - #228

Merged
LittleChimera merged 2 commits into
stagingfrom
factory/ui-perf
Nov 6, 2026
Merged

LittleChimera merged 2 commits into
stagingfrom
factory/ui-perf

Conversation

@LittleChimera

Copy link
Copy Markdown

Related issue

N/A (perf: the live cockpit takes 3-22 s per snapshot and 15 s per node page when its cache runs out)

Summary

The live factory server runs one python process. Each cockpit read rebuilt the snapshot from about 360 to 450 SQL statements: permission checks one member at a time, sub-agents listed per member, paused labels and agent rows read once per project or child, and the roster listing children a second time. On Postgres each statement is a round trip, so a snapshot cost seconds. Every agent event also sent a factory_changed frame (debounced 0.5 s), and each frame made every open tab refetch about 10 queries.

Server:

  • Batched reads. readable_set resolves admin, sub-agent parents and grants with a fixed number of reads (get_conversations + list_for_sessions). The new ConversationStore.list_sub_agents_by_parent reads every member's sub-agents with one id query plus get_conversations; the base class keeps the per-parent loop. The roster reuses those children and reads message previews with one batched call. FactoryData prefetches root, PL and worker rows once per load. Agent rows are read once per build (contextvar memo, agent_rows_memo). Member costs are read in parallel when cold, in the request's context.
  • Stale-while-revalidate. A snapshot older than the 2 s TTL is served for up to 60 s while a single background rebuild runs. The node projection works the same way (5 s TTL / 60 s). forget(), nodes.invalidate() and recorded events still drop the cache hard, so the next read waits for a fresh build. A generation and epoch counter keeps a build that started before a drop from being stored.
  • Per-viewer cut cache. filtered_view caches filter_snapshot and its ETag digest once per snapshot and readable set, so it no longer deep-copies 200-450 KB and hashes it on every request. The 304 path does no work. The 200 body is rendered off the event loop.

Web:

  • factory_changed frames are coalesced per factory: the leading edge applies at once and the trailing edge applies once per 2 s with merged scopes. A person's own mutations still invalidate straight away.

Benchmark (dev/benchmarks/factory_cockpit.py, live size: 20 projects, ~50 members, 5k events, 600 decisions and proposals, 30 demos, 2k chat messages; SQLite with 2 ms added per SQL statement to stand in for Postgres round trips; 2 tabs polling while agents write 2 events/s):

endpoint queries cold before → after cold ms before → after warm p50 before → after under load p95 before → after
snapshot GET /factories/{fid} 453 → 186 1789 → 599 65 → 44 1155 → 79
node page n/product 376 → 113 1437 → 370 78 → 58 1302 → 164
needs-you 369 → 106 1464 → 338 38 → 31 1567 → 471 (cold first read)
chat 361 → 98 1369 → 292 56 → 47 1453 → 89
status-summary 367 → 100 1445 → 299 72 → 56 n/a
events 362 → 99 1383 → 288 42 → 34 1183 → 82
factories 362 → 99 1367 → 288 65 → 48 1488 → 425 (cold first read)
decisions 65 → 27 311 → 100 264 → 98 341 → 117
preflight 2 → 2 9 → 8 10 → 8 n/a
/ping while loaded (event loop free?) p95 15 → 5 ms, max 36 → 14 ms

The 186 cold snapshot statements include 90 one-time handle writes on a factory's first build. A steady-state rebuild is about 95. The worst cases left under load are each page's first read after a cold start, all under 0.5 s.

Test Plan

  • uv run pytest -n 8 tests/server/factory tests/stores: 5531 passed. test_databricks_volumes_artifact_store needs the databricks extra and was skipped locally.
  • New tests/server/factory/test_cockpit_perf.py seeds a live-shaped factory and checks per-page cold SQL statement budgets, cold < 3 s, warm p95 < 300 ms and the 304.
  • npx vitest run src/hooks src/components/factory: 2760 passed. New test for the frame throttle.
  • pyrefly: no new errors. ruff is clean.
  • OMNIGENT_SKIP_WEB_UI=true uv run python -m dev.benchmarks.factory_cockpit --db-latency-ms 2 reproduces the table.

Demo

N/A (server and push-handling change; numbers above)

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

The perf test budgets SQL statements, which is the deterministic signal, and keeps loose time budgets for CI. No new env or config.

🤖 Generated with Claude Code

Batch the snapshot's N+1 reads (viewer permissions, sub-agents per member,
paused labels, agent rows, roster children), serve a stale snapshot and node
projection while one rebuild runs, cache each viewer's cut with its ETag digest,
render the snapshot off the event loop, read member costs side by side, and
coalesce factory_changed frames in the web to one refresh per factory per 2 s.
Adds a live-size benchmark (dev/benchmarks/factory_cockpit.py) and a query and
latency budget test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XL Pull request size: XL label Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LittleChimera
LittleChimera merged commit df2ac02 into staging Nov 6, 2026
20 of 21 checks passed
@LittleChimera
LittleChimera deleted the factory/ui-perf branch November 6, 2026 09:35
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

❌ UI Snapshot doesn't match the committed baseline.

If this UI change is intentional, update the baseline — each path renders in the same pinned image, so the result matches this gate:

  • Label the PR (recommended): add the update-ui-snapshot label — the bot regenerates the baseline in the pinned image, pushes it back here, and re-runs the checks.
  • Locally with Docker: run tests/e2e_ui/visual/regen_baseline_docker.sh, review the PNG, then commit + push.

Diff PNGs (expected_=baseline, actual_=your render, diff_) are in the run artifact. Full guide: tests/e2e_ui/visual/README.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant