Skip to main content

moonpool_explorer/
energy.rs

1//! 3-level energy budget for adaptive exploration.
2//!
3//! Provides global, per-mark, and reallocation pool energy levels in
4//! `MAP_SHARED` memory. When an assertion mark is barren (no new coverage),
5//! its remaining per-mark energy is returned to the reallocation pool.
6//! Productive marks that exhaust their budget can draw from the pool.
7//!
8//! All atomics use `Relaxed` ordering because the fork tree is sequential
9//! (parent waits on each child).
10
11use std::io;
12use std::sync::atomic::{AtomicI64, Ordering};
13
14use crate::assertion_slots::MAX_ASSERTION_SLOTS;
15use crate::shared_mem;
16
17/// 3-level energy budget in shared memory.
18///
19/// Lives in `MAP_SHARED` memory so all forked processes share the same counters.
20#[repr(C)]
21pub struct EnergyBudget {
22    /// Global energy remaining across all marks.
23    pub global_remaining: AtomicI64,
24    /// Per-mark energy budgets (indexed by assertion slot index).
25    pub per_mark: [AtomicI64; MAX_ASSERTION_SLOTS],
26    /// Initial energy assigned to each new mark.
27    pub per_mark_initial: i64,
28    /// Pool of energy returned by barren marks, available for productive marks.
29    pub realloc_pool: AtomicI64,
30}
31
32/// Allocate and initialize an energy budget in shared memory.
33///
34/// # Errors
35///
36/// Returns an error if shared memory allocation fails.
37pub fn init_energy_budget(
38    global_energy: i64,
39    per_mark_initial: i64,
40) -> Result<*mut EnergyBudget, io::Error> {
41    let ptr = shared_mem::alloc_shared(std::mem::size_of::<EnergyBudget>())?;
42    let budget = ptr as *mut EnergyBudget;
43    // Safety: ptr is valid, zeroed by mmap. Initialize non-zero fields.
44    unsafe {
45        (*budget)
46            .global_remaining
47            .store(global_energy, Ordering::Relaxed);
48        (*budget).per_mark_initial = per_mark_initial;
49        // per_mark and realloc_pool start at 0 (zeroed by mmap).
50        // Per-mark budgets are lazily initialized via init_mark_budget().
51    }
52    Ok(budget)
53}
54
55/// Initialize a mark's per-mark energy budget (called on first fork for a slot).
56///
57/// # Safety
58///
59/// `budget` must point to a valid `EnergyBudget` in shared memory.
60pub unsafe fn init_mark_budget(budget: *mut EnergyBudget, slot_idx: usize) {
61    if slot_idx < MAX_ASSERTION_SLOTS {
62        let b = unsafe { &*budget };
63        b.per_mark[slot_idx].store(b.per_mark_initial, Ordering::Relaxed);
64    }
65}
66
67/// Try to consume one unit of energy for a mark. Returns `true` if successful.
68///
69/// Attempts in order: global budget, per-mark budget, then reallocation pool.
70/// Undoes partial decrements on failure to maintain consistency.
71///
72/// # Safety
73///
74/// `budget` must point to a valid `EnergyBudget` in shared memory.
75pub unsafe fn decrement_mark_energy(budget: *mut EnergyBudget, slot_idx: usize) -> bool {
76    let b = unsafe { &*budget };
77
78    // Check global budget first
79    if b.global_remaining.fetch_sub(1, Ordering::Relaxed) <= 0 {
80        b.global_remaining.fetch_add(1, Ordering::Relaxed);
81        return false;
82    }
83
84    // Try per-mark budget
85    if slot_idx < MAX_ASSERTION_SLOTS {
86        if b.per_mark[slot_idx].fetch_sub(1, Ordering::Relaxed) > 0 {
87            return true;
88        }
89        // Per-mark exhausted, undo and try realloc pool
90        b.per_mark[slot_idx].fetch_add(1, Ordering::Relaxed);
91
92        if b.realloc_pool.fetch_sub(1, Ordering::Relaxed) > 0 {
93            return true;
94        }
95        b.realloc_pool.fetch_add(1, Ordering::Relaxed);
96    }
97
98    // Neither per-mark nor realloc pool had energy — undo global
99    b.global_remaining.fetch_add(1, Ordering::Relaxed);
100    false
101}
102
103/// Reset the energy budget for a new seed in multi-seed exploration.
104///
105/// Sets fresh global energy, zeros all per-mark budgets and the realloc pool.
106/// Keeps `per_mark_initial` unchanged (set during [`init_energy_budget`]).
107///
108/// # Safety
109///
110/// `budget` must point to a valid `EnergyBudget` in shared memory.
111pub unsafe fn reset_energy_budget(budget: *mut EnergyBudget, new_global_energy: i64) {
112    let b = unsafe { &*budget };
113    b.global_remaining
114        .store(new_global_energy, Ordering::Relaxed);
115    b.realloc_pool.store(0, Ordering::Relaxed);
116    for slot in &b.per_mark {
117        slot.store(0, Ordering::Relaxed);
118    }
119}
120
121/// Return a mark's remaining per-mark energy to the reallocation pool.
122///
123/// Called when a mark is determined to be barren (no new coverage found).
124///
125/// # Safety
126///
127/// `budget` must point to a valid `EnergyBudget` in shared memory.
128pub unsafe fn return_mark_energy_to_pool(budget: *mut EnergyBudget, slot_idx: usize) {
129    if slot_idx < MAX_ASSERTION_SLOTS {
130        let b = unsafe { &*budget };
131        let remaining = b.per_mark[slot_idx].swap(0, Ordering::Relaxed);
132        if remaining > 0 {
133            b.realloc_pool.fetch_add(remaining, Ordering::Relaxed);
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_mark_budget_decrement_and_return() {
144        let ptr = init_energy_budget(100, 10).expect("init failed");
145        unsafe {
146            init_mark_budget(ptr, 0);
147            // Consume 2 from per_mark[0]
148            assert!(decrement_mark_energy(ptr, 0));
149            assert!(decrement_mark_energy(ptr, 0));
150
151            // Return remaining (8) to pool
152            return_mark_energy_to_pool(ptr, 0);
153            let b = &*ptr;
154            assert_eq!(b.per_mark[0].load(Ordering::Relaxed), 0);
155            assert_eq!(b.realloc_pool.load(Ordering::Relaxed), 8);
156
157            shared_mem::free_shared(ptr as *mut u8, std::mem::size_of::<EnergyBudget>());
158        }
159    }
160
161    #[test]
162    fn test_productive_mark_draws_from_realloc() {
163        let ptr = init_energy_budget(100, 5).expect("init failed");
164        unsafe {
165            // Mark 0: barren, returns energy to pool
166            init_mark_budget(ptr, 0);
167            decrement_mark_energy(ptr, 0); // consume 1
168            return_mark_energy_to_pool(ptr, 0); // return 4 to pool
169
170            // Mark 1: productive, exhausts per_mark budget
171            init_mark_budget(ptr, 1);
172            for _ in 0..5 {
173                assert!(decrement_mark_energy(ptr, 1));
174            }
175            // Per-mark exhausted, draws from realloc pool (which has 4)
176            assert!(decrement_mark_energy(ptr, 1));
177            let b = &*ptr;
178            assert_eq!(b.realloc_pool.load(Ordering::Relaxed), 3);
179
180            shared_mem::free_shared(ptr as *mut u8, std::mem::size_of::<EnergyBudget>());
181        }
182    }
183
184    #[test]
185    fn test_global_energy_exhaustion() {
186        let ptr = init_energy_budget(3, 100).expect("init failed");
187        unsafe {
188            init_mark_budget(ptr, 0);
189            assert!(decrement_mark_energy(ptr, 0));
190            assert!(decrement_mark_energy(ptr, 0));
191            assert!(decrement_mark_energy(ptr, 0));
192            // Global exhausted — 4th fails
193            assert!(!decrement_mark_energy(ptr, 0));
194            // Global should be back to 0 (not negative)
195            let b = &*ptr;
196            assert_eq!(b.global_remaining.load(Ordering::Relaxed), 0);
197
198            shared_mem::free_shared(ptr as *mut u8, std::mem::size_of::<EnergyBudget>());
199        }
200    }
201
202    #[test]
203    fn test_realloc_flow() {
204        let ptr = init_energy_budget(50, 5).expect("init failed");
205        unsafe {
206            // Mark 0: barren, consume 1, return 4 to pool
207            init_mark_budget(ptr, 0);
208            decrement_mark_energy(ptr, 0);
209            return_mark_energy_to_pool(ptr, 0);
210
211            let b = &*ptr;
212            assert_eq!(b.realloc_pool.load(Ordering::Relaxed), 4);
213
214            // Mark 1: productive, exhausts budget then draws 3 from pool
215            init_mark_budget(ptr, 1);
216            for _ in 0..5 {
217                assert!(decrement_mark_energy(ptr, 1));
218            }
219            for _ in 0..3 {
220                assert!(decrement_mark_energy(ptr, 1));
221            }
222            assert_eq!(b.realloc_pool.load(Ordering::Relaxed), 1);
223
224            shared_mem::free_shared(ptr as *mut u8, std::mem::size_of::<EnergyBudget>());
225        }
226    }
227}