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:
@@ -179,6 +179,34 @@ pub struct ActorInfo {
|
||||
/// `budget-accounting` feature is enabled, since measuring it costs a
|
||||
/// timestamp read on every resume.
|
||||
pub budget_cycles: u64,
|
||||
/// RFC 019 §8 — this actor's stack, as the runtime sees it. All fields
|
||||
/// are lock-free atomic reads, coherent for this incarnation via the
|
||||
/// same generation check as the counters above. Exact RSS is
|
||||
/// deliberately absent: `mincore` is debug tooling, never a runtime
|
||||
/// path.
|
||||
pub stack: StackInfo,
|
||||
}
|
||||
|
||||
/// RFC 019 §8 — per-actor stack introspection. Sizes are page-rounded, as
|
||||
/// [`Stack::new`](crate::stack::Stack::new) rounds them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StackInfo {
|
||||
/// Usable stack size ([`SpawnOpts::stack_reserve`]
|
||||
/// (crate::SpawnOpts::stack_reserve) or the Config/default).
|
||||
pub reserve: usize,
|
||||
/// PROT_NONE guard below the usable region.
|
||||
pub guard: usize,
|
||||
/// Sampled high-water depth in bytes: `top − lowest saved sp`. Sampled,
|
||||
/// not exact — the context save at yields/parks/preemptions is the
|
||||
/// sampler (RFC 019 §2), so a spike the actor never yielded inside is
|
||||
/// invisible. 0 depth means "never descheduled at any depth", not
|
||||
/// "never ran".
|
||||
pub depth_high_water: usize,
|
||||
/// Parks on this incarnation since its last shrink (or since install if
|
||||
/// it has never shrunk) — the §3 cooldown counter, live.
|
||||
pub parks_since_shrink: u32,
|
||||
/// §3 shrinks performed on this incarnation.
|
||||
pub shrinks: u32,
|
||||
}
|
||||
|
||||
/// A snapshot of every actor in the runtime at (approximately) one moment.
|
||||
@@ -281,6 +309,14 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
||||
drop(cold);
|
||||
|
||||
// Counters are plain atomics, read lock-free.
|
||||
let (reserve, guard, top, hwm, parks_since_shrink, shrinks) = slot.stack_introspect();
|
||||
let stack = StackInfo {
|
||||
reserve,
|
||||
guard,
|
||||
depth_high_water: top.saturating_sub(hwm),
|
||||
parks_since_shrink,
|
||||
shrinks,
|
||||
};
|
||||
let overruns = slot.overruns();
|
||||
let messages_received = slot.messages_received();
|
||||
let budget_cycles = slot.budget_cycles();
|
||||
@@ -306,6 +342,7 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
||||
overruns,
|
||||
messages_received,
|
||||
budget_cycles,
|
||||
stack,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ pub use gen_statem::{
|
||||
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
|
||||
GenStatemRef,
|
||||
};
|
||||
pub use introspect::{
|
||||
pub use introspect::{StackInfo,
|
||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||
TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||
};
|
||||
|
||||
@@ -662,6 +662,23 @@ impl Slot {
|
||||
|
||||
/// Read the overrun tally (Relaxed; the snapshot reads cross-thread).
|
||||
#[inline]
|
||||
/// RFC 019 §8 — the stack introspection tuple, all lock-free:
|
||||
/// `(reserve, guard, top, hwm, parks_since_shrink, shrink_count)`.
|
||||
/// Geometry from the c6 diag atomics (install-time, gen-coherent under
|
||||
/// `read_slot`'s gen check exactly like the other counters); `hwm` is the
|
||||
/// §2 sampled high-water (lowest saved sp). All zeros before first
|
||||
/// install.
|
||||
pub(crate) fn stack_introspect(&self) -> (usize, usize, usize, usize, u32, u32) {
|
||||
(
|
||||
self.diag_stack_reserve.load(Ordering::Relaxed),
|
||||
self.diag_stack_guard.load(Ordering::Relaxed),
|
||||
self.diag_stack_top.load(Ordering::Relaxed),
|
||||
self.hwm.load(Ordering::Relaxed),
|
||||
self.parks_since_shrink.load(Ordering::Relaxed),
|
||||
self.shrink_count.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn overruns(&self) -> u64 {
|
||||
self.overruns.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user