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 /// Total instrumented code edges (from LLVM sancov). 0 when sancov unavailable.
56 pub sancov_edges_total: usize,
57 /// Code edges covered across all timelines. 0 when sancov unavailable.
58 pub sancov_edges_covered: usize,
59}
60
61/// Allocate and initialize shared stats.
62///
63/// # Errors
64///
65/// Returns an error if shared memory allocation fails.
66pub fn init_shared_stats(energy: i64) -> Result<*mut SharedStats, io::Error> {
67 let ptr = shared_mem::alloc_shared(std::mem::size_of::<SharedStats>())?;
68 let stats = ptr as *mut SharedStats;
69 // Safety: ptr is valid, properly aligned (mmap returns page-aligned memory),
70 // and zeroed. We initialize the energy field.
71 unsafe {
72 (*stats).global_energy.store(energy, Ordering::Relaxed);
73 }
74 Ok(stats)
75}
76
77/// Allocate and initialize shared recipe storage.
78///
79/// # Errors
80///
81/// Returns an error if shared memory allocation fails.
82pub fn init_shared_recipe() -> Result<*mut SharedRecipe, io::Error> {
83 let ptr = shared_mem::alloc_shared(std::mem::size_of::<SharedRecipe>())?;
84 Ok(ptr as *mut SharedRecipe)
85}
86
87/// Try to consume one unit of energy. Returns `true` if successful.
88///
89/// # Safety
90///
91/// `stats` must point to valid shared stats allocated by [`init_shared_stats`].
92pub unsafe fn decrement_energy(stats: *mut SharedStats) -> bool {
93 unsafe { (*stats).global_energy.fetch_sub(1, Ordering::Relaxed) > 0 }
94}
95
96/// Reset shared stats for a new seed in multi-seed exploration.
97///
98/// Zeros counters and sets the display energy value.
99///
100/// # Safety
101///
102/// `stats` must point to valid shared memory allocated by [`init_shared_stats`].
103pub unsafe fn reset_shared_stats(stats: *mut SharedStats, new_energy: i64) {
104 let s = unsafe { &*stats };
105 s.global_energy.store(new_energy, Ordering::Relaxed);
106 s.total_timelines.store(0, Ordering::Relaxed);
107 s.fork_points.store(0, Ordering::Relaxed);
108 s.bug_found.store(0, Ordering::Relaxed);
109}
110
111/// Reset shared recipe for a new seed in multi-seed exploration.
112///
113/// Clears the claimed flag so the next seed can capture a new bug recipe.
114///
115/// # Safety
116///
117/// `recipe` must point to valid shared memory allocated by [`init_shared_recipe`].
118pub unsafe fn reset_shared_recipe(recipe: *mut SharedRecipe) {
119 let r = unsafe { &*recipe };
120 r.claimed.store(0, Ordering::Relaxed);
121}
122
123/// Get a snapshot of the current exploration statistics.
124///
125/// Returns `None` if the stats pointer is null (exploration not initialized).
126pub fn get_exploration_stats() -> Option<ExplorationStats> {
127 let ptr = crate::context::SHARED_STATS.with(|c| c.get());
128 if ptr.is_null() {
129 return None;
130 }
131
132 // Read realloc pool from energy budget if available
133 let realloc_pool = crate::context::ENERGY_BUDGET_PTR.with(|c| {
134 let energy_ptr = c.get();
135 if energy_ptr.is_null() {
136 0
137 } else {
138 // Safety: energy_ptr was set during init
139 unsafe { (*energy_ptr).realloc_pool.load(Ordering::Relaxed) }
140 }
141 });
142
143 // Safety: ptr was set during init and points to valid shared stats
144 unsafe {
145 Some(ExplorationStats {
146 global_energy: (*ptr).global_energy.load(Ordering::Relaxed),
147 total_timelines: (*ptr).total_timelines.load(Ordering::Relaxed),
148 fork_points: (*ptr).fork_points.load(Ordering::Relaxed),
149 bug_found: (*ptr).bug_found.load(Ordering::Relaxed),
150 realloc_pool_remaining: realloc_pool,
151 sancov_edges_total: crate::sancov::sancov_edge_count(),
152 sancov_edges_covered: crate::sancov::sancov_edges_covered(),
153 })
154 }
155}
156
157/// Get the bug recipe if one was captured.
158///
159/// Returns `None` if no bug was found or exploration is not initialized.
160pub fn get_bug_recipe() -> Option<Vec<(u64, u64)>> {
161 let ptr = crate::context::SHARED_RECIPE.with(|c| c.get());
162 if ptr.is_null() {
163 return None;
164 }
165 // Safety: ptr was set during init
166 unsafe {
167 let recipe = &*ptr;
168 if recipe.claimed.load(Ordering::Relaxed) == 0 {
169 return None;
170 }
171 let len = recipe.len as usize;
172 Some(recipe.entries[..len].to_vec())
173 }
174}