design 005 piece 2 (S2): parallelise the sweep — 2.19x measured, but two clone-scoped paths collide first nf-r1b ← Beads

open priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/746 by tools/beads/import_github.py's reconcile pass.

Piece 2 of design 005's work breakdown (§8), under #686. Covers S2. Stacks on #739 (pieces 1+3) — same file.

The win is real, and bigger than the doc says

Measured on nf-dev-sg 2026-08-07, 8 probes (tonapse-home focus-ring fit-notes cold-start cujs-gaps focus-glide guide-cursor chit-budget), warm cache, idle box:

| | wall clock | |---|---| | serial | 70 s | | 8-way parallel, one xvfb-run -a each | 32 s |

2.19×, against the doc's 1.63× at 4-way. The difference is the box: §2.2 measured 4-way because nproc read 4, and this box now reports 8 (piece 3 / S16 covers that — notes/machines.md had 2, the doc measured 4, it is 8, so the pool must read nproc at runtime, never a written-down number).

⚠️ This is now Phase 1's actual cost win. S1's 1.8× import-cache speedup is retracted (#738, and the comment on #686) — it is a correctness fix, not a cost one. Parallelism is what is left.

⚠️ Two clone-scoped paths collide, and both are independent of outDir

probe_all already rewrites outDir per probe, so screenshots are safe. These two are not, and neither follows outDir:

| path | source | |---|---| | <clone>/probe_out/userdata — the whole user:// redirect | shared/VisualProbe.cs:101 | | <clone>/probe_out/playtest — the playtest journal | shared/PlaytestJournal.cs:217 |

Both are wiped/seeded at startup, so N workers in one clone stamp on each other. Design 003 Open Q2 called this *"a race that happened not to fire"*. It fires at 8-way, and both failure modes showed up:

  1. With outDir also shared (running probe.py directly), cold-start failed: no focusable control matching '✳ New song using Shablam Demo Song'. It is the probe that sets ColdStartRun and depends on the fresh-install user:// state, so it is the most exposed.
  2. With per-probe outDir — i.e. exactly what probe_all does — round 2 was clean, and in round 3 cold-start hung. Not failed: stalled after [playtest] journal armed and was still stuck ~9 minutes later, when I killed it.

cold-start passes serially every time (3/3 immediately afterwards), so this is parallelism, not a broken probe.

⚠️ The hang is the dangerous half — and nothing times out

Filed separately as a finding, because it is not only a parallelism problem. budget_ms warns and never fails (VisualProbe.cs:301), and there is no step deadline anywhere in the harness — so a probe that stalls waits forever. Under merge_pr.py that is a merge gate that never returns rather than one that goes red, and probe_gate cannot report what never finished.

A parallel sweep must not ship without a deadline, or one stuck worker converts a 32-second sweep into an unbounded hang holding a merge open.

Scope
  1. Worker pool in probe_all.py, sized from nproc at runtime, one xvfb-run -a per worker. ⚠️ The sweep currently re-execs itself once under xvfb-run so all 34 children share one display (nflib.godot.ensure_display); a pool needs one server per worker instead, so that handshake changes.
  2. Per-probe user://. UserDataRoot is a static derived from res://. Deriving it from the probe's existing outDirprobe_out/<name>/userdata — needs no new probe-JSON key, which matters because VisualProbe.cs enforces a strict root-key allow-list (§2.7). shared/UserPathGuardTests.cs and UserPathsTests.cs guard this area and must stay green.
  3. Per-probe playtest journal, same treatment.
  4. A per-probe deadline, so a stuck worker fails rather than hangs.
  5. State in the tool's output that the ceiling is cores — probes are CPU-bound under llvmpipe (§2.2), so this does not scale past nproc, and on a shared box adding workers cannot recover what contention takes.

⚠️ Ordering: the user:// and journal isolation must land before or with the pool. Shipping the pool first makes the gate flaky in a way that looks like #514 and gets diagnosed as flake.

Refs #686 · Refs #619 · Refs #620 · Refs #514

Dependencies

None.

Comments

No comments.

Add a comment