Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Actor stack-switching probe evidence

Companion to Actor budgets and retained stacks. Status: Test-only native probe; compiler insertion and stage integration are not implemented. Source: experiments/actor-stacks/probe.rs, included inside the runtime’s actor heap module only in x86-64 GNU/Linux test builds.

What executes

Two callbacks run on separate fixed 64 KiB stacks, with an inaccessible page at each end. A small round-robin harness alternates them whenever an explicit checkpoint exhausts a budget of 17. Checkpoints model intended compiler output; no compiler pass inserts them. Each callback enters the actual plenty_actor_run, so its production recovery record lives on its own stack. The probe uses the real routing allocator, heap limits and unwrap trap helper.

The harness saves/restores allocation routing, recovery, caller-location and last-trap TLS, plus the production drop queue. It refuses checkpoints during arena calls or active destruction. The drop queue remains empty in these fixtures; nonempty queue switching is not tested. Caller-location identities are distinct and checked after resumption. Retained heap allocations are checked for ownership and contents, and an oversized allocation returns null within each actor’s limit. A borrowed stack local survives nested helper suspension.

The harness uses stable raw-pointer records rather than keeping an exclusive Rust reference to scheduler storage across a switch. It does not model a sound replacement for Plenty’s shared Context, compiler adapter storage, mailbox delivery, readiness, timers, supervision or restart. It cannot establish the borrow checker’s acceptance of a new context design.

Reproduce

From the task checkout:

cargo test --locked -p plenty-runtime actor_stack_switching_probe -- --ignored --nocapture --test-threads=1

The probe is ignored during ordinary suites because it owns the process-wide actor heap table; run it alone. An external timeout is appropriate for detecting switch failures. Its scheduler also asserts a finite maximum turn count. The default fixture has finite loops; it does not execute an infinite loop.

Checks and interpretation

CaseAssertion
Budget disabledPeer first runs after the first actor completes all 10,000 iterations
Budget enabledPeer first runs after 17 iterations; each actor completes 10,000 iterations with 588 yields
Trap after resumptionFirst actor traps after three yields, returns Unwrap, runs no local destructor; peer completes and runs its destructor once
Cooperative cancellationScheduler requests cancellation after eight dispatches; both callbacks resume, observe it and return through cleanup before completing their loops
Allocation routing and limitsSmall allocations belong to the running actor before/after switches; oversized allocation is refused
ReclamationEach case discards both heaps and unmaps both stacks; live heap count, used bytes and committed heap bytes return to zero

These are deterministic work/progress checks, not throughput or latency benchmarks. The cancellation flag is an experiment-only mechanism, not Mail.Stop or a new public cancellation API. The trap fixture deliberately leaves its allocation and destructor behind; existing actor heap discard reclaims that allocation without unwinding the stack. Runtime panics are not recovered.

Observed on x86-64 GNU/Linux on 2026-11-02: all four cases passed in debug, release and debug with allocation-checks. The budgeted pair performed 588 yields each; the trap case returned Unwrap at iteration 52 while its peer completed 10,000 iterations. Cancellation returned both callbacks at iteration 68 with one destructor each. The oversized-allocation check calls Router directly because an unused standard Rust allocation can be optimized away.

Validation also passed the ordinary runtime suite (187 passed, 5 ignored), workspace/all-target Clippy with warnings denied, workspace formatting and an explicit rustfmt check of the included probe. Three networking tests initially failed under sandbox restrictions; the runtime suite passed when rerun with local networking available. Documentation validation passed source links, 151 script tests (3 skipped), and the complete site using mdBook 0.5.4 (1,444 default and 722 development HTML pages). No compiler integration suite was run: production code generation and executable Plenty examples are unchanged.

No stack admission fault injection, guard-page overflow, real socket workload, platform register conformance, signal delivery, blocked-native-call latency or complete sanitizer coverage is established. The stackful runtime uses glibc contexts, so it does not benchmark the cost of a future minimal register switch. Only a compiler prototype can validate coverage of optimized loops, recursion, tail calls, callbacks, generators and cached objects.