Follow-up to #337. The framework-share seed lands the big perf win (3.7s/sketch → ~200ms/sketch on uno cold), but the stage-2 workers themselves finish in a strictly serial pattern despite `sketch_jobs=4` and 16 cores available locally.
Three consecutive cold-cache uno runs all show the same pattern:
| Run | s1 firmware mtime | s2 +Δ | s3 +Δ | s4 +Δ |
|---|
| 11:54 | .588 | +207ms | +194ms | +200ms |
| 12:02 | .806 | +196ms | +195ms | +190ms |
| 12:36 | .216 | +211ms | +158ms | +164ms |
If all four workers actually ran in parallel for the same duration, their firmware.elf mtimes would land within a few ms of each other. Instead they consistently spread out at near-uniform 160–210ms intervals — workers are running in parallel for SOME of the time but blocking on something shared near the end.
Candidates I'd start poking at:
- The shared zccache daemon (single sqlite WAL writer, even with WAL it serializes on commit)
- avr-ld link step — single linker process per worker, possibly contending on a temp file or framework lib path
- The fbuild perf-log mutex (probably not — even if it served all stage-2 workers, it's just a Mutex append)
- NTFS filesystem-level locks on the shared framework cache dir (`~/.fbuild/prod/cache/platforms/arduino-ArduinoCore-avr/`)
The PERF_LOG isn't useful here because each stage-2 worker emits its own perf-log frame and they don't share a timeline. A quick way to triangulate would be to emit `tracing::info!` at known phase boundaries inside each worker (seed-start, seed-end, orchestrator-start, link-start, link-end) with a worker ID, then sort the resulting log lines on wall-clock to see which span is the long one. If link-end is what's spreading out across workers and link-start is bunched, it's contention in the linker phase; if seed-end is spread out, the seed is the bottleneck (unlikely given how small the hardlink set is).
Pragmatically, the current 200ms-per-sketch baseline is already 19× better than pre-#337, so this isn't urgent. But the same contention will scale linearly: 100 sketches × 200ms ≈ 20s of serialization that should be ~5s if the workers actually fanned out.
Follow-up to #337. The framework-share seed lands the big perf win (3.7s/sketch → ~200ms/sketch on uno cold), but the stage-2 workers themselves finish in a strictly serial pattern despite `sketch_jobs=4` and 16 cores available locally.
Three consecutive cold-cache uno runs all show the same pattern:
If all four workers actually ran in parallel for the same duration, their firmware.elf mtimes would land within a few ms of each other. Instead they consistently spread out at near-uniform 160–210ms intervals — workers are running in parallel for SOME of the time but blocking on something shared near the end.
Candidates I'd start poking at:
The PERF_LOG isn't useful here because each stage-2 worker emits its own perf-log frame and they don't share a timeline. A quick way to triangulate would be to emit `tracing::info!` at known phase boundaries inside each worker (seed-start, seed-end, orchestrator-start, link-start, link-end) with a worker ID, then sort the resulting log lines on wall-clock to see which span is the long one. If link-end is what's spreading out across workers and link-start is bunched, it's contention in the linker phase; if seed-end is spread out, the seed is the bottleneck (unlikely given how small the hardlink set is).
Pragmatically, the current 200ms-per-sketch baseline is already 19× better than pre-#337, so this isn't urgent. But the same contention will scale linearly: 100 sketches × 200ms ≈ 20s of serialization that should be ~5s if the workers actually fanned out.