moonpool_explorer/shared_stats.rs
1//! Cross-process exploration statistics.
2//!
3//! All statistics live in `MAP_SHARED` memory so they are visible
4//! across `fork()` boundaries. Atomic operations use `Relaxed` ordering
5//! because the fork tree is sequential (parent waits on each child).
6
7use std::io;
8use std::sync::atomic::{AtomicI64, AtomicU32, AtomicU64, Ordering};
9
10use crate::shared_mem;
11
12/// Global exploration statistics in shared memory.
13#[repr(C)]
14pub struct SharedStats {
15 /// Remaining global energy budget (decremented per fork).
16 pub global_energy: AtomicI64,
17 /// Total number of timelines explored.
18 pub total_timelines: AtomicU64,
19 /// Total number of fork points triggered.
20 pub fork_points: AtomicU64,
21 /// Number of bugs found (child exited with code 42).
22 pub bug_found: AtomicU64,
23}
24
25/// Maximum number of recipe entries.
26pub const MAX_RECIPE_ENTRIES: usize = 128;
27
28/// Shared recipe storage for the bug-finding timeline.
29///
30/// When a child exits with code 42 (bug found), the parent copies
31/// the child's recipe here for later replay.
32#[repr(C)]
33pub struct SharedRecipe {
34 /// Whether this recipe has been claimed (0 = no, 1 = yes).
35 pub claimed: AtomicU32,
36 /// Number of valid entries in [`Self::entries`].
37 pub len: u32,
38 /// Recipe entries: `(rng_call_count, child_seed)` pairs.
39 pub entries: [(u64, u64); MAX_RECIPE_ENTRIES],
40}
41
42/// Snapshot of exploration statistics for test assertions.
43#[derive(Debug, Clone)]
44pub struct ExplorationStats {
45 /// Remaining global energy.
46 pub global_energy: i64,
47 /// Total timelines explored.
48 pub total_timelines: u64,
49 /// Total fork points triggered.
50 pub fork_points: u64,
51 /// Number of bugs found.
52 pub bug_found: u64,
53 /// Energy remaining in the reallocation pool (0 when adaptive is disabled).
54 pub realloc_pool_remaining: i64,
55}
56
57/// Allocate and initialize shared stats.
58///
59/// # Errors
60///
61/// Returns an error if shared memory allocation fails.
62pub fn init_shared_stats(energy: i64) -> Result<*mut SharedStats, io::Error> {
63 let ptr = shared_mem::alloc_shared(std::mem::size_of::<SharedStats>())?;
64 let stats = ptr as *mut SharedStats;
65 // Safety: ptr is valid, properly aligned (mmap returns page-aligned memory),
66 // and zeroed. We initialize the energy field.
67 unsafe {
68 (*stats).global_energy.store(energy, Ordering::Relaxed);
69 }
70 Ok(stats)
71}
72
73/// Allocate and initialize shared recipe storage.
74///
75/// # Errors
76///
77/// Returns an error if shared memory allocation fails.
78pub fn init_shared_recipe() -> Result<*mut SharedRecipe, io::Error> {
79 let ptr = shared_mem::alloc_shared(std::mem::size_of::<SharedRecipe>())?;
80 Ok(ptr as *mut SharedRecipe)
81}
82
83/// Try to consume one unit of energy. Returns `true` if successful.
84///
85/// # Safety
86///
87/// `stats` must point to valid shared stats allocated by [`init_shared_stats`].
88pub unsafe fn decrement_energy(stats: *mut SharedStats) -> bool {
89 unsafe { (*stats).global_energy.fetch_sub(1, Ordering::Relaxed) > 0 }
90}
91
92/// Reset shared stats for a new seed in multi-seed exploration.
93///
94/// Zeros counters and sets the display energy value.
95///
96/// # Safety
97///
98/// `stats` must point to valid shared memory allocated by [`init_shared_stats`].
99pub unsafe fn reset_shared_stats(stats: *mut SharedStats, new_energy: i64) {
100 let s = unsafe { &*stats };
101 s.global_energy.store(new_energy, Ordering::Relaxed);
102 s.total_timelines.store(0, Ordering::Relaxed);
103 s.fork_points.store(0, Ordering::Relaxed);
104 s.bug_found.store(0, Ordering::Relaxed);
105}
106
107/// Reset shared recipe for a new seed in multi-seed exploration.
108///
109/// Clears the claimed flag so the next seed can capture a new bug recipe.
110///
111/// # Safety
112///
113/// `recipe` must point to valid shared memory allocated by [`init_shared_recipe`].
114pub unsafe fn reset_shared_recipe(recipe: *mut SharedRecipe) {
115 let r = unsafe { &*recipe };
116 r.claimed.store(0, Ordering::Relaxed);
117}
118
119/// Get a snapshot of the current exploration statistics.
120///
121/// Returns `None` if the stats pointer is null (exploration not initialized).
122pub fn get_exploration_stats() -> Option<ExplorationStats> {
123 let ptr = crate::context::SHARED_STATS.with(|c| c.get());
124 if ptr.is_null() {
125 return None;
126 }
127
128 // Read realloc pool from energy budget if available
129 let realloc_pool = crate::context::ENERGY_BUDGET_PTR.with(|c| {
130 let energy_ptr = c.get();
131 if energy_ptr.is_null() {
132 0
133 } else {
134 // Safety: energy_ptr was set during init
135 unsafe { (*energy_ptr).realloc_pool.load(Ordering::Relaxed) }
136 }
137 });
138
139 // Safety: ptr was set during init and points to valid shared stats
140 unsafe {
141 Some(ExplorationStats {
142 global_energy: (*ptr).global_energy.load(Ordering::Relaxed),
143 total_timelines: (*ptr).total_timelines.load(Ordering::Relaxed),
144 fork_points: (*ptr).fork_points.load(Ordering::Relaxed),
145 bug_found: (*ptr).bug_found.load(Ordering::Relaxed),
146 realloc_pool_remaining: realloc_pool,
147 })
148 }
149}
150
151/// Get the bug recipe if one was captured.
152///
153/// Returns `None` if no bug was found or exploration is not initialized.
154pub fn get_bug_recipe() -> Option<Vec<(u64, u64)>> {
155 let ptr = crate::context::SHARED_RECIPE.with(|c| c.get());
156 if ptr.is_null() {
157 return None;
158 }
159 // Safety: ptr was set during init
160 unsafe {
161 let recipe = &*ptr;
162 if recipe.claimed.load(Ordering::Relaxed) == 0 {
163 return None;
164 }
165 let len = recipe.len as usize;
166 Some(recipe.entries[..len].to_vec())
167 }
168}