feat(introspect,runtime): per-actor stack surface on ActorInfo (RFC 019 §8)
- introspect::StackInfo { reserve, guard, depth_high_water,
parks_since_shrink, shrinks } as ActorInfo.stack; re-exported at crate
root beside ActorInfo.
- All reads lock-free: geometry from the c6 diag slot atomics, depth =
top - hwm (the §2 sampled high-water; doc spells out sampled-not-exact
and that 0 means never-descheduled-at-depth), counters straight off the
§3 atomics. Coherence for the incarnation rides read_slot's existing
generation check, same as overruns/messages_received.
- Slot::stack_introspect(): one pub(crate) tuple accessor beside the other
counter accessors.
- Exact RSS deliberately absent per RFC (mincore = debug tooling only,
never a runtime path); stack_shape(pid) untouched (cold-lock exact
variant from c2).
- tests/introspect.rs: defaults surface (64 KiB reserve / 1 MiB guard /
sampled ~32 KiB depth / gate park counted / zero shrinks) + live shrink
counters (spike visible pre-shrink; shrinks>=1, cooldown counter reset,
hwm reset after crossing COOLDOWN) read mid-run -- post-join the slot
reclaim correctly hides the incarnation, which the first draft of the
test learned the hard way.
FLAGGED (Claude-solo calls):
- Nested StackInfo struct over five flat ActorInfo fields (grain break;
the five fields are one concern and ActorInfo is already 12 fields).
- Field names reserve/guard/shrinks (RFC says stack_reserve/stack_guard/
shrink count; the stack_ prefix is redundant inside StackInfo).
This commit is contained in:
@@ -237,6 +237,13 @@ fn tree_from_nests_children_and_reroots_orphans() {
|
||||
overruns: 0,
|
||||
messages_received: 0,
|
||||
budget_cycles: 0,
|
||||
stack: smarm::StackInfo {
|
||||
reserve: 0,
|
||||
guard: 0,
|
||||
depth_high_water: 0,
|
||||
parks_since_shrink: 0,
|
||||
shrinks: 0,
|
||||
},
|
||||
};
|
||||
|
||||
let snap = RuntimeSnapshot {
|
||||
@@ -352,3 +359,122 @@ fn budget_cycles_accumulate_when_enabled() {
|
||||
h.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RFC 019 §8 — the stack introspection surface.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Burn ~`frames` × 4 KiB of stack with a yield at max depth, so the context
|
||||
/// save samples the high-water there (RFC 019 §2: hwm is SAMPLED at
|
||||
/// deschedule, not tracked continuously).
|
||||
#[inline(never)]
|
||||
fn burn_stack_yielding(frames: usize) -> u64 {
|
||||
let mut local = [0u8; 4096];
|
||||
local[0] = frames as u8;
|
||||
let below = if frames == 0 {
|
||||
smarm::yield_now();
|
||||
0
|
||||
} else {
|
||||
burn_stack_yielding(frames - 1)
|
||||
};
|
||||
std::hint::black_box(&mut local);
|
||||
below.wrapping_add(local[0] as u64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_info_reports_defaults_and_sampled_depth() {
|
||||
run(|| {
|
||||
let (ready_tx, ready_rx) = channel::<()>();
|
||||
let (gate_tx, gate_rx) = channel::<()>();
|
||||
|
||||
let h = spawn(move || {
|
||||
// ~32 KiB deep with a yield at the bottom: the sample point.
|
||||
std::hint::black_box(burn_stack_yielding(8));
|
||||
ready_tx.send(()).unwrap();
|
||||
gate_rx.recv().unwrap();
|
||||
});
|
||||
ready_rx.recv().unwrap();
|
||||
|
||||
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
||||
let s = info.stack;
|
||||
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
||||
assert_eq!(s.guard, 1024 * 1024, "default guard (kernel stack_guard_gap convention)");
|
||||
assert!(
|
||||
s.depth_high_water >= 8 * 4096,
|
||||
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
||||
s.depth_high_water
|
||||
);
|
||||
assert!(
|
||||
s.depth_high_water < s.reserve,
|
||||
"depth {} cannot exceed the reserve {}",
|
||||
s.depth_high_water,
|
||||
s.reserve
|
||||
);
|
||||
// Parked at the gate right now, never shrunk (64 KiB reserve cannot
|
||||
// cross the shrink threshold).
|
||||
assert!(s.parks_since_shrink >= 1, "the gate park must be counted");
|
||||
assert_eq!(s.shrinks, 0);
|
||||
|
||||
gate_tx.send(()).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_info_shrink_counters_are_live() {
|
||||
use smarm::runtime::{Config, SHRINK_COOLDOWN, SHRINK_THRESHOLD};
|
||||
use smarm::{spawn_with, SpawnOpts};
|
||||
|
||||
let rt = smarm::runtime::init(Config::exact(1));
|
||||
rt.run(|| {
|
||||
let (park_tx, park_rx) = channel::<()>();
|
||||
|
||||
let spike = 768 * 4096;
|
||||
assert!(spike > SHRINK_THRESHOLD);
|
||||
let worker = spawn_with(
|
||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
||||
move || {
|
||||
std::hint::black_box(burn_stack_yielding(768));
|
||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||
park_rx.recv().unwrap();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let wpid = worker.pid();
|
||||
// Before any parks complete: the spike depth is visible.
|
||||
let info = spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||
assert!(
|
||||
info.stack.depth_high_water >= spike,
|
||||
"spike should be sampled: {} < {spike}",
|
||||
info.stack.depth_high_water
|
||||
);
|
||||
|
||||
// Cross the cooldown, then read the counters live while the worker
|
||||
// is parked waiting for the remaining rounds (post-join the slot is
|
||||
// reclaimed and the generation check correctly hides it).
|
||||
for _ in 0..(SHRINK_COOLDOWN + 2) {
|
||||
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||
park_tx.send(()).unwrap();
|
||||
}
|
||||
let info = spin_until(wpid, |a| a.state == ActorState::Parked && a.stack.shrinks >= 1);
|
||||
let s = info.stack;
|
||||
assert!(s.shrinks >= 1, "cooldown was crossed with a spike above threshold");
|
||||
assert!(
|
||||
s.parks_since_shrink < SHRINK_COOLDOWN,
|
||||
"counter must reset at shrink: {}",
|
||||
s.parks_since_shrink
|
||||
);
|
||||
assert!(
|
||||
s.depth_high_water < spike,
|
||||
"hwm resets to the shallow park sp at shrink; got {}",
|
||||
s.depth_high_water
|
||||
);
|
||||
|
||||
for _ in 0..6 {
|
||||
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||
park_tx.send(()).unwrap();
|
||||
}
|
||||
worker.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user