Skip to main content

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}