feat(scheduler,gen_server,gen_statem,introspect): SpawnOpts — per-actor stack shape on every spawn surface (RFC 019 §1)
SpawnOpts { stack_reserve, guard_size } with Option<usize> fields, None
resolving to the Config defaults at spawn time — a deliberate deviation
from the RFC's plain-usize struct so struct-update syntax works without
a runtime handle in scope. Threaded across the five surfaces:
spawn_with, spawn_under_with, spawn_addr_with,
GenServerBuilder::stack_opts (mirrored on NamedGenServerBuilder), and
gen_statem::spawn_with (gen_statem has no builder, so the opts ride a
_with variant — Claude-solo surface call, flagged for review). Existing
spawns forward defaults; no call-site churn.
introspect::stack_shape(pid) pulled forward (agreed) as the first slice
of the RFC 019 introspection surface, giving tests an observable.
Tests (tests/spawn_opts.rs): override/partial-override/rounding on each
surface; obligation 4 from the outside — a dead custom stack is never
handed to the next default spawn (LIFO pool would expose it), and the
reverse (default stacks ARE recycled); 8 MiB reserve behaviorally
permits ~1 MiB recursion. Also: silence unused-Result in the c1
runtime test (join now unwrapped).
This commit is contained in:
+31
-5
@@ -182,7 +182,7 @@ use crate::channel::{channel, select, select_timeout, Receiver, RecvTimeoutError
|
|||||||
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
||||||
use crate::scheduler::{cancel_timer, request_stop, send_after_to, spawn, spawn_under};
|
use crate::scheduler::{cancel_timer, request_stop, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -643,11 +643,17 @@ pub struct GenServerBuilder<G: GenServer> {
|
|||||||
state: G,
|
state: G,
|
||||||
infos: Vec<Receiver<G::Info>>,
|
infos: Vec<Receiver<G::Info>>,
|
||||||
supervisor: Option<Pid>,
|
supervisor: Option<Pid>,
|
||||||
|
stack_opts: crate::scheduler::SpawnOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> GenServerBuilder<G> {
|
impl<G: GenServer> GenServerBuilder<G> {
|
||||||
pub fn new(state: G) -> Self {
|
pub fn new(state: G) -> Self {
|
||||||
GenServerBuilder { state, infos: Vec::new(), supervisor: None }
|
GenServerBuilder {
|
||||||
|
state,
|
||||||
|
infos: Vec::new(),
|
||||||
|
supervisor: None,
|
||||||
|
stack_opts: crate::scheduler::SpawnOpts::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
||||||
@@ -665,6 +671,14 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stack shape for the server actor (RFC 019) — see
|
||||||
|
/// [`SpawnOpts`](crate::SpawnOpts). Useful for servers that recurse
|
||||||
|
/// deeply or call into FFI with large C frames.
|
||||||
|
pub fn stack_opts(mut self, opts: crate::scheduler::SpawnOpts) -> Self {
|
||||||
|
self.stack_opts = opts;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
||||||
/// lifetime is governed by its refs, not by joining, so the backing join
|
/// lifetime is governed by its refs, not by joining, so the backing join
|
||||||
/// handle is dropped.
|
/// handle is dropped.
|
||||||
@@ -686,10 +700,16 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
/// under the name before returning.
|
/// under the name before returning.
|
||||||
fn spawn_server(self) -> GenServerRef<G> {
|
fn spawn_server(self) -> GenServerRef<G> {
|
||||||
let (tx, rx) = channel::<Envelope<G>>();
|
let (tx, rx) = channel::<Envelope<G>>();
|
||||||
let GenServerBuilder { state, infos, supervisor } = self;
|
let GenServerBuilder { state, infos, supervisor, stack_opts } = self;
|
||||||
let handle = match supervisor {
|
let handle = match supervisor {
|
||||||
Some(sup) => spawn_under(sup, move || server_loop::<G>(rx, state, infos)),
|
Some(sup) => {
|
||||||
None => spawn(move || server_loop::<G>(rx, state, infos)),
|
crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||||
|
server_loop::<G>(rx, state, infos)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => crate::scheduler::spawn_with(stack_opts, move || {
|
||||||
|
server_loop::<G>(rx, state, infos)
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
GenServerRef { tx, pid: handle.pid() }
|
GenServerRef { tx, pid: handle.pid() }
|
||||||
}
|
}
|
||||||
@@ -758,6 +778,12 @@ impl<G: GenServer> NamedGenServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stack shape for the server actor (see [`GenServerBuilder::stack_opts`]).
|
||||||
|
pub fn stack_opts(mut self, opts: crate::scheduler::SpawnOpts) -> Self {
|
||||||
|
self.builder = self.builder.stack_opts(opts);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the server and bind its name in one step. Fallible: returns
|
/// Spawn the server and bind its name in one step. Fallible: returns
|
||||||
/// [`RegisterError::NameTaken`] if the name is already held by a different
|
/// [`RegisterError::NameTaken`] if the name is already held by a different
|
||||||
/// live server.
|
/// live server.
|
||||||
|
|||||||
+15
-2
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
use crate::channel::{channel, select, Receiver, Sender};
|
use crate::channel::{channel, select, Receiver, Sender};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::scheduler::{cancel_timer, send_after_to, spawn as spawn_actor};
|
use crate::scheduler::{cancel_timer, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -434,8 +434,21 @@ impl<M: Machine> GenStatemRef<M> {
|
|||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||||
|
spawn_with(crate::scheduler::SpawnOpts::default(), machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`spawn`] with per-actor stack shape overrides (RFC 019) for the machine's
|
||||||
|
/// actor — see [`SpawnOpts`](crate::SpawnOpts). gen_statem has no builder
|
||||||
|
/// (its one-shot `spawn(machine)` shape predates RFC 019), so the opts ride
|
||||||
|
/// a `_with` variant like the scheduler's own spawns.
|
||||||
|
///
|
||||||
|
/// Panics if called outside `Runtime::run()`.
|
||||||
|
pub fn spawn_with<M: Machine>(
|
||||||
|
opts: crate::scheduler::SpawnOpts,
|
||||||
|
machine: M,
|
||||||
|
) -> GenStatemRef<M> {
|
||||||
let (tx, rx) = channel::<M::Ev>();
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
let handle = spawn_actor(move || statem_loop(rx, machine));
|
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine));
|
||||||
GenStatemRef { tx, pid: handle.pid() }
|
GenStatemRef { tx, pid: handle.pid() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,22 @@ pub fn snapshot() -> RuntimeSnapshot {
|
|||||||
/// slot was reused by another), out of range, or was never a real pid at
|
/// slot was reused by another), out of range, or was never a real pid at
|
||||||
/// all. Unlike [`snapshot`], every field of the result describes the same
|
/// all. Unlike [`snapshot`], every field of the result describes the same
|
||||||
/// instant, since there is only one actor to read.
|
/// instant, since there is only one actor to read.
|
||||||
|
/// The stack shape `(reserve, guard)` of a live actor, page-rounded — the
|
||||||
|
/// RFC 019 introspection surface's first field (depth sampling and shrink
|
||||||
|
/// counters land with the shrink machinery). `None` if `pid` no longer names
|
||||||
|
/// a live actor. Takes the actor's cold lock briefly; debugging/assertion
|
||||||
|
/// use, not a hot-path call.
|
||||||
|
pub fn stack_shape(pid: Pid) -> Option<(usize, usize)> {
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let slot = inner.slot_at(pid)?;
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
if slot.generation() != pid.generation() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
cold.actor.as_ref().map(|a| a.stack.shape())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn actor_info(pid: Pid) -> Option<ActorInfo> {
|
pub fn actor_info(pid: Pid) -> Option<ActorInfo> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let slot = inner.slot_at(pid)?;
|
let slot = inner.slot_at(pid)?;
|
||||||
|
|||||||
+3
-2
@@ -83,8 +83,9 @@ pub use runtime::{init, Config, Runtime};
|
|||||||
pub use scheduler::{
|
pub use scheduler::{
|
||||||
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
||||||
send_after_named_wall, send_after_wall, sleep, sleep_wall,
|
send_after_named_wall, send_after_wall, sleep, sleep_wall,
|
||||||
spawn, spawn_addr, spawn_under, wait_readable, wait_readable_timeout, wait_writable,
|
spawn, spawn_addr, spawn_addr_with, spawn_under, spawn_under_with, spawn_with,
|
||||||
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle,
|
wait_readable, wait_readable_timeout, wait_writable,
|
||||||
|
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle, SpawnOpts,
|
||||||
};
|
};
|
||||||
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
||||||
pub use timer::TimerId;
|
pub use timer::TimerId;
|
||||||
|
|||||||
+5
-3
@@ -1360,7 +1360,8 @@ pub const ROOT_PID: Pid = Pid::new(u32::MAX, u32::MAX);
|
|||||||
// Stack acquisition / recycling — RFC 019 pool rule
|
// Stack acquisition / recycling — RFC 019 pool rule
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Get a stack of the requested shape (`None` ⇒ the runtime defaults).
|
/// Get a stack of the shape `opts` requests (`None` fields ⇒ the runtime
|
||||||
|
/// defaults).
|
||||||
///
|
///
|
||||||
/// Pool rule (RFC 019 §1): the pool is a uniform `Vec<Stack>` of
|
/// Pool rule (RFC 019 §1): the pool is a uniform `Vec<Stack>` of
|
||||||
/// default-shaped stacks and stays that way. Default-shaped requests try the
|
/// default-shaped stacks and stays that way. Default-shaped requests try the
|
||||||
@@ -1369,9 +1370,10 @@ pub const ROOT_PID: Pid = Pid::new(u32::MAX, u32::MAX);
|
|||||||
/// lock is dropped before any mmap: no syscall ever stalls another spawner.
|
/// lock is dropped before any mmap: no syscall ever stalls another spawner.
|
||||||
pub(crate) fn acquire_stack(
|
pub(crate) fn acquire_stack(
|
||||||
inner: &RuntimeInner,
|
inner: &RuntimeInner,
|
||||||
shape: Option<(usize, usize)>,
|
opts: crate::scheduler::SpawnOpts,
|
||||||
) -> crate::stack::Stack {
|
) -> crate::stack::Stack {
|
||||||
let (reserve, guard) = shape.unwrap_or((inner.stack_reserve, inner.stack_guard));
|
let reserve = opts.stack_reserve.unwrap_or(inner.stack_reserve);
|
||||||
|
let guard = opts.guard_size.unwrap_or(inner.stack_guard);
|
||||||
let default_shaped = crate::stack::round_to_pages(reserve) == inner.stack_reserve
|
let default_shaped = crate::stack::round_to_pages(reserve) == inner.stack_reserve
|
||||||
&& crate::stack::round_to_pages(guard) == inner.stack_guard;
|
&& crate::stack::round_to_pages(guard) == inner.stack_guard;
|
||||||
if default_shaped {
|
if default_shaped {
|
||||||
|
|||||||
+52
-1
@@ -259,6 +259,28 @@ impl Drop for JoinHandle {
|
|||||||
// spawn / spawn_under / self_pid
|
// spawn / spawn_under / self_pid
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Per-spawn stack shape overrides (RFC 019). `None` fields resolve to the
|
||||||
|
/// runtime's [`Config`](crate::runtime::Config) defaults at spawn time, so
|
||||||
|
/// struct-update syntax works anywhere without a runtime handle:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use smarm::SpawnOpts;
|
||||||
|
/// let opts = SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() };
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Both sizes are page-rounded. The reserve is *virtual* (demand-paged):
|
||||||
|
/// an 8 MiB reserve costs address space, not memory — RSS follows touched
|
||||||
|
/// pages. The guard is PROT_NONE below the stack; raise it for FFI code
|
||||||
|
/// with unusually large C frames. Custom-shaped stacks bypass the recycle
|
||||||
|
/// pool: they are mmapped fresh at spawn and munmapped at death.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct SpawnOpts {
|
||||||
|
/// Usable stack reservation. `None` ⇒ [`Config::stack_reserve`](crate::runtime::Config::stack_reserve).
|
||||||
|
pub stack_reserve: Option<usize>,
|
||||||
|
/// PROT_NONE guard below the stack. `None` ⇒ [`Config::stack_guard`](crate::runtime::Config::stack_guard).
|
||||||
|
pub guard_size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
||||||
///
|
///
|
||||||
/// The new actor runs concurrently with its caller and with every other
|
/// The new actor runs concurrently with its caller and with every other
|
||||||
@@ -281,17 +303,34 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
|||||||
spawn_under(parent, f)
|
spawn_under(parent, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
||||||
|
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||||
|
let parent = current_pid().unwrap_or_else(|| {
|
||||||
|
with_runtime(|_| crate::runtime::ROOT_PID)
|
||||||
|
});
|
||||||
|
spawn_under_with(parent, opts, f)
|
||||||
|
}
|
||||||
|
|
||||||
/// Like [`spawn`], but explicitly attaches the new actor to `supervisor`
|
/// Like [`spawn`], but explicitly attaches the new actor to `supervisor`
|
||||||
/// instead of the calling actor. Ordinary code should reach for [`spawn`];
|
/// instead of the calling actor. Ordinary code should reach for [`spawn`];
|
||||||
/// this exists for supervision trees (see [`supervisor`](crate::supervisor))
|
/// this exists for supervision trees (see [`supervisor`](crate::supervisor))
|
||||||
/// and other cases that need to place a child under a specific ancestor
|
/// and other cases that need to place a child under a specific ancestor
|
||||||
/// rather than its true caller.
|
/// rather than its true caller.
|
||||||
pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||||
|
spawn_under_with(supervisor, SpawnOpts::default(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`spawn_under`] with per-actor stack shape overrides (RFC 019).
|
||||||
|
pub fn spawn_under_with<A>(
|
||||||
|
supervisor: Pid<A>,
|
||||||
|
opts: SpawnOpts,
|
||||||
|
f: impl FnOnce() + Send + 'static,
|
||||||
|
) -> JoinHandle {
|
||||||
let supervisor = supervisor.erase();
|
let supervisor = supervisor.erase();
|
||||||
// Stack + closure boxing happen before the slot locks are taken; the
|
// Stack + closure boxing happen before the slot locks are taken; the
|
||||||
// pool lock inside acquire_stack is dropped before any mmap, so no
|
// pool lock inside acquire_stack is dropped before any mmap, so no
|
||||||
// syscall ever stalls another scheduler thread.
|
// syscall ever stalls another scheduler thread.
|
||||||
let stack = with_runtime(|inner| crate::runtime::acquire_stack(inner, None));
|
let stack = with_runtime(|inner| crate::runtime::acquire_stack(inner, opts));
|
||||||
let sp = init_actor_stack(stack.top(), crate::actor::trampoline);
|
let sp = init_actor_stack(stack.top(), crate::actor::trampoline);
|
||||||
let closure: crate::runtime::Closure = Box::new(f);
|
let closure: crate::runtime::Closure = Box::new(f);
|
||||||
|
|
||||||
@@ -331,6 +370,18 @@ pub fn spawn_addr<A: crate::pid::Addressable>(
|
|||||||
crate::pid::assert_type::<A>(pid)
|
crate::pid::assert_type::<A>(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`spawn_addr`] with per-actor stack shape overrides (RFC 019).
|
||||||
|
pub fn spawn_addr_with<A: crate::pid::Addressable>(
|
||||||
|
opts: SpawnOpts,
|
||||||
|
body: impl FnOnce(crate::channel::Receiver<A::Msg>) + Send + 'static,
|
||||||
|
) -> Pid<A> {
|
||||||
|
let (tx, rx) = crate::channel::channel::<A::Msg>();
|
||||||
|
let handle = spawn_with(opts, move || body(rx));
|
||||||
|
let pid = handle.pid();
|
||||||
|
crate::registry::install_for::<A::Msg>(pid, tx);
|
||||||
|
crate::pid::assert_type::<A>(pid)
|
||||||
|
}
|
||||||
|
|
||||||
use crate::context::init_actor_stack;
|
use crate::context::init_actor_stack;
|
||||||
|
|
||||||
/// The identity of the actor currently running. Use it to hand your own
|
/// The identity of the actor currently running. Use it to hand your own
|
||||||
|
|||||||
+3
-1
@@ -545,7 +545,9 @@ fn config_stack_reserve_permits_deep_recursion() {
|
|||||||
spawn(move || {
|
spawn(move || {
|
||||||
std::hint::black_box(burn_stack(64));
|
std::hint::black_box(burn_stack(64));
|
||||||
done2.store(true, Ordering::SeqCst);
|
done2.store(true, Ordering::SeqCst);
|
||||||
}).join();
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
assert!(done.load(Ordering::SeqCst));
|
assert!(done.load(Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
//! RFC 019 commit 2 — the `SpawnOpts` surface.
|
||||||
|
//!
|
||||||
|
//! Covers: per-spawn stack shape overrides on every spawn surface, the
|
||||||
|
//! `None ⇒ Config default` resolution, the pool rule from the outside
|
||||||
|
//! (obligation 4: a custom-shaped stack never enters the pool), and that a
|
||||||
|
//! big reserve behaviorally takes effect (deep recursion completes).
|
||||||
|
|
||||||
|
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
||||||
|
use smarm::{
|
||||||
|
self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts,
|
||||||
|
};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn rt1() -> smarm::runtime::Runtime {
|
||||||
|
smarm::runtime::init(Config::exact(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_spawn_has_default_shape() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_overrides_reserve_and_guard() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
guard_size: Some(256 * 1024),
|
||||||
|
};
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (1024 * 1024, 256 * 1024));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), ..SpawnOpts::default() };
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_with_rounds_to_pages() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let opts = SpawnOpts { stack_reserve: Some(64 * 1024 + 1), guard_size: Some(4097) };
|
||||||
|
let h = spawn_with(opts, || {
|
||||||
|
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(reserve % 4096, 0);
|
||||||
|
assert_eq!(guard % 4096, 0);
|
||||||
|
assert!(reserve >= 64 * 1024 + 1);
|
||||||
|
assert!(guard >= 4097);
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_under_with_takes_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let me = self_pid();
|
||||||
|
let opts = SpawnOpts { stack_reserve: Some(128 * 1024), ..SpawnOpts::default() };
|
||||||
|
let h = spawn_under_with(me, opts, || {
|
||||||
|
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(reserve, 128 * 1024);
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obligation 4, from the outside: a dead custom stack must not be handed to
|
||||||
|
/// the next default spawn. The pool is LIFO, so if the custom stack had been
|
||||||
|
/// (wrongly) pushed at death, the very next default-shaped spawn on this
|
||||||
|
/// single-threaded runtime would pop it and report a custom shape.
|
||||||
|
#[test]
|
||||||
|
fn custom_stack_never_enters_the_pool() {
|
||||||
|
rt1().run(|| {
|
||||||
|
spawn_with(
|
||||||
|
SpawnOpts { stack_reserve: Some(512 * 1024), guard_size: Some(128 * 1024) },
|
||||||
|
|| {},
|
||||||
|
)
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reverse direction of the pool rule: a default-shaped stack IS pooled
|
||||||
|
/// and reused (cap = threads × 4 ≥ 1 here, pool empty at start).
|
||||||
|
#[test]
|
||||||
|
fn default_stack_is_recycled() {
|
||||||
|
rt1().run(|| {
|
||||||
|
spawn(|| {}).join().unwrap();
|
||||||
|
let h = spawn(|| {
|
||||||
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
|
assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD));
|
||||||
|
});
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Burn ~`frames` × 4 KiB of stack (see tests/runtime.rs twin).
|
||||||
|
#[inline(never)]
|
||||||
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
|
let mut local = [0u8; 4096];
|
||||||
|
local[0] = frames as u8;
|
||||||
|
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
||||||
|
std::hint::black_box(&mut local);
|
||||||
|
below.wrapping_add(local[0] as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_reserve_behaviorally_takes_effect() {
|
||||||
|
// ~1 MiB deep on an 8 MiB per-spawn reserve, runtime default untouched.
|
||||||
|
rt1().run(|| {
|
||||||
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
|
let done2 = done.clone();
|
||||||
|
spawn_with(
|
||||||
|
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
||||||
|
move || {
|
||||||
|
std::hint::black_box(burn_stack(256));
|
||||||
|
done2.store(true, Ordering::SeqCst);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
assert!(done.load(Ordering::SeqCst));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Builder surfaces
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Echo;
|
||||||
|
impl smarm::GenServer for Echo {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = (usize, usize);
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
fn handle_call(&mut self, _c: ()) -> (usize, usize) {
|
||||||
|
smarm::introspect::stack_shape(self_pid()).unwrap()
|
||||||
|
}
|
||||||
|
fn handle_cast(&mut self, _c: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_server_builder_stack_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let server = GenServerBuilder::new(Echo)
|
||||||
|
.stack_opts(SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() })
|
||||||
|
.start();
|
||||||
|
let (reserve, guard) = server.call(()).unwrap();
|
||||||
|
assert_eq!(reserve, 256 * 1024);
|
||||||
|
assert_eq!(guard, DEFAULT_STACK_GUARD);
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Probe;
|
||||||
|
impl smarm::Machine for Probe {
|
||||||
|
type Ev = smarm::channel::Sender<(usize, usize)>;
|
||||||
|
fn state_timeout_ev() -> Self::Ev {
|
||||||
|
unreachable!("no timers in this test")
|
||||||
|
}
|
||||||
|
fn timeout_ev(_name: &'static str) -> Self::Ev {
|
||||||
|
unreachable!("no timers in this test")
|
||||||
|
}
|
||||||
|
fn on_start(&mut self, _cx: &mut smarm::Cx<Self::Ev>) {}
|
||||||
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
ev: Self::Ev,
|
||||||
|
_cx: &mut smarm::Cx<Self::Ev>,
|
||||||
|
) -> smarm::gen_statem::Step<Self::Ev> {
|
||||||
|
let _ = ev.send(smarm::introspect::stack_shape(self_pid()).unwrap());
|
||||||
|
smarm::gen_statem::Step::Stayed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_statem_spawn_with_stack_opts() {
|
||||||
|
rt1().run(|| {
|
||||||
|
let m = smarm::gen_statem::spawn_with(
|
||||||
|
SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
||||||
|
Probe,
|
||||||
|
);
|
||||||
|
let (tx, rx) = smarm::channel::channel();
|
||||||
|
m.send(tx).unwrap();
|
||||||
|
let (reserve, guard) = rx.recv().unwrap();
|
||||||
|
assert_eq!(reserve, 256 * 1024);
|
||||||
|
assert_eq!(guard, DEFAULT_STACK_GUARD);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user