feat(stack,runtime): pool-recycle DONTNEED above the retained entry end (RFC 019 §6)
- stack::retain_range: pure checked span fn (retain page-up = zap less; None when retain covers the reserve, so the 64 KiB default config never pays a syscall) + 6 adversarial units mirroring shrink_range's. - Stack::recycle_zap: advisory MADV_DONTNEED of [usable_base, top-RETAIN); stack is unowned at the call site, synchronous eager zap races nothing. - recycle_stack: zap OFF-LOCK before pool admission (acquire_stack's no-syscall-under-the-pool-lock invariant); rare cap-overflow pays a wasted zap ahead of munmap, accepted over a second lock round-trip. - pub const RECYCLE_RETAIN = 64 KiB beside the shrink knobs, ratified-as- constant rationale in doc. - tests/stack_recycle.rs: mincore-based exact-zero-resident assert over the zap span. smaps was tried first and over-counts: a neighboring rw anon VMA can merge flush against the stack top (observed once under the full-suite run); the PROT_NONE guard pins the usable base exactly. FLAGGED (Claude-solo calls): - RFC §6 'above the bottom RETAIN' is direction-ambiguous in address terms; implemented as retain the ENTRY end (highest addresses, the pages the next actor faults first), zap the cold deep span below. - Const named RECYCLE_RETAIN (RFC says RETAIN) to sit beside SHRINK_*.
This commit is contained in:
@@ -441,6 +441,15 @@ pub const SHRINK_THRESHOLD: usize = 256 * 1024;
|
||||
/// rationale as [`SHRINK_THRESHOLD`]).
|
||||
pub const SHRINK_COOLDOWN: u32 = 64;
|
||||
|
||||
/// RFC 019 §6: the entry-end span (highest addresses — the frames the next
|
||||
/// actor faults first) a recycled stack keeps resident; everything below it
|
||||
/// is `MADV_DONTNEED`ed before the stack re-enters the pool. Ratified as a
|
||||
/// constant, not Config, alongside the shrink knobs; the 64 KiB value was a
|
||||
/// flagged Claude-solo call at ratification — it equals the default reserve,
|
||||
/// so with an unraised Config the zap is a no-op and only Configs that raise
|
||||
/// the default reserve pay it.
|
||||
pub const RECYCLE_RETAIN: usize = 64 * 1024;
|
||||
|
||||
pub(crate) type Closure = Box<dyn FnOnce() + Send>;
|
||||
|
||||
/// Lifecycle data, mutated only under the slot's cold [`RawMutex`]. Everything
|
||||
@@ -1472,6 +1481,12 @@ pub(crate) fn acquire_stack(
|
||||
/// otherwise dropped here → munmap (custom shapes and cap overflow alike).
|
||||
pub(crate) fn recycle_stack(inner: &RuntimeInner, stack: crate::stack::Stack) {
|
||||
if stack.shape() == (inner.stack_reserve, inner.stack_guard) {
|
||||
// RFC 019 §6: zap the dead spike before pooling, BEFORE taking the
|
||||
// pool lock — acquire_stack's invariant is that no syscall ever
|
||||
// stalls another spawner under it. On the rare cap-overflow the zap
|
||||
// is wasted work ahead of the munmap; harmless, and cheaper than a
|
||||
// second lock round-trip to find out.
|
||||
stack.recycle_zap(RECYCLE_RETAIN);
|
||||
let mut pool = inner.stack_pool.lock();
|
||||
if pool.len() < inner.stack_pool_cap {
|
||||
pool.push(stack);
|
||||
|
||||
Reference in New Issue
Block a user