67 lines
4.4 KiB
Markdown
67 lines
4.4 KiB
Markdown
|
|
# Cycle 5: Load-Aware Actor Placement + Work Stealing Research — Development History
|
||
|
|
|
||
|
|
> Commit: `7d00e65` · 6 files · 184 insertions, 13 deletions
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Motivation
|
||
|
|
|
||
|
|
With fairness (Cycle 1), thread parking (Cycle 3), and shutdown (Cycle 4) resolved, the next bottleneck was actor placement. Swactor used blind round-robin to assign actors to workers — ignoring current load. If actors have unequal workloads, round-robin produces persistent imbalance. This cycle also included deep research into work stealing to decide whether full actor migration was worthwhile.
|
||
|
|
|
||
|
|
## Competitor Analysis: Work Stealing Deep Dive
|
||
|
|
|
||
|
|
| Aspect | Tokio | Go | BEAM | ForkJoinPool |
|
||
|
|
|--------|-------|-----|------|-------------|
|
||
|
|
| Queue | Fixed 256-slot ring | 256-slot ring + runnext | Per-priority linked | Growable array deque |
|
||
|
|
| Steal granularity | Half victim's queue | Half victim's runq | Individual processes | One task at a time |
|
||
|
|
| LIFO fast-path | Dedicated slot (3-use cap) | runnext (stealable 4th try) | None | Owner pops from top |
|
||
|
|
| Global queue | Mutex intrusive list | Checked 1/61 ticks | Per-priority migration | Even-indexed queues |
|
||
|
|
| Searcher limit | N/2 workers | GOMAXPROCS/2 | N/A (proactive migration) | Idle stack in ctl |
|
||
|
|
| Balance strategy | Reactive steal | Reactive steal | **Proactive migration** + reactive | Reactive scan |
|
||
|
|
|
||
|
|
### Key Patterns Discovered
|
||
|
|
|
||
|
|
1. **LIFO slot** — every runtime has one; improves cache locality by running the recipient immediately after the sender. Tokio caps at 3 consecutive uses to prevent starvation.
|
||
|
|
2. **Steal-half** — Tokio and Go both steal half the victim's queue, amortizing cross-thread coordination overhead.
|
||
|
|
3. **N/2 searcher limit** — both Tokio and Go cap concurrent searchers to prevent thundering herd (O(N²) cache-line bouncing).
|
||
|
|
4. **BEAM's migration** — unique dual approach: reactive stealing when idle + proactive migration via periodic `check_balance()`.
|
||
|
|
|
||
|
|
### Feasibility for Swactor
|
||
|
|
|
||
|
|
- **Full actor migration**: Mechanically possible (ActorSlot is `Send`), but has a 1-tick message loss window during migration and requires push-based donation (`ActorPool` is not `Sync` → no pull stealing)
|
||
|
|
- **Message stealing without actors**: Impossible — the actor IS the state; messages without the actor are meaningless
|
||
|
|
- **Decision: Load-aware placement over work stealing** — zero correctness risk, handles the primary imbalance source (uneven spawn distribution), full work stealing deferred
|
||
|
|
|
||
|
|
## Implementation
|
||
|
|
|
||
|
|
- `Placement::next_worker()` now reads per-worker stats (`num_actors` + `mailbox_depth`)
|
||
|
|
- Selects the worker with lowest combined load
|
||
|
|
- Scan starts from a rotating position → round-robin fallback when all stats are equal (initial burst, before first tick publishes stats)
|
||
|
|
- O(N) relaxed atomic loads per spawn — trivial for N ≤ 8 workers
|
||
|
|
|
||
|
|
**Key files modified:** `src/delivery.rs`, `tests/runtime_api.rs`, `benches/runtime_benchmarks.rs`
|
||
|
|
|
||
|
|
## Design Decisions
|
||
|
|
|
||
|
|
- **Load-aware placement instead of work stealing** — zero message loss risk, no ordering changes, trivial implementation cost. Handles the #1 source of imbalance: uneven spawn distribution.
|
||
|
|
- **Combined metric (actors + depth)** — neither actor count alone nor mailbox depth alone captures load accurately. Combined metric approximates total pending work per worker.
|
||
|
|
- **Relaxed atomics for stat reads** — stats are advisory (best-effort), so relaxed ordering is sufficient. No need for acquire/release which would add synchronization cost.
|
||
|
|
- **Round-robin fallback** — before the first tick, all workers report zero stats. Falling back to round-robin ensures even initial distribution rather than always picking worker 0.
|
||
|
|
- **Full work stealing deferred** — would require migration channels, address map coordination, forwarding tombstones, and a message loss window. Benefit uncertain for N ≤ 8 workers.
|
||
|
|
|
||
|
|
## Tests Added
|
||
|
|
|
||
|
|
3 new tests (57 → 60 total):
|
||
|
|
|
||
|
|
- `load_aware_placement_prefers_lighter_worker` — imbalanced load biases spawn toward the lighter worker
|
||
|
|
- `load_aware_placement_single_worker_degrades_gracefully` — single-thread mode works correctly
|
||
|
|
- `load_aware_placement_falls_back_to_round_robin_on_fresh_runtime` — even distribution before ticks produce stats
|
||
|
|
|
||
|
|
**Benchmark added:** `placement/spawn_under_load` (2-thread and 4-thread variants)
|
||
|
|
|
||
|
|
## Result
|
||
|
|
|
||
|
|
- 60 tests pass
|
||
|
|
- All workspace crates compile
|
||
|
|
- Comprehensive work-stealing research documented for future reference
|