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}
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}