ruvector_solver/
budget.rs1use std::time::Instant;
14
15use crate::error::SolverError;
16use crate::types::ComputeBudget;
17
18const DEFAULT_MEMORY_LIMIT: usize = 256 * 1024 * 1024;
20
21pub struct BudgetEnforcer {
43 start_time: Instant,
45
46 budget: ComputeBudget,
48
49 iterations_used: usize,
51
52 memory_used: usize,
54
55 memory_limit: usize,
58}
59
60impl BudgetEnforcer {
61 pub fn new(budget: ComputeBudget) -> Self {
65 Self {
66 start_time: Instant::now(),
67 budget,
68 iterations_used: 0,
69 memory_used: 0,
70 memory_limit: DEFAULT_MEMORY_LIMIT,
71 }
72 }
73
74 pub fn with_memory_limit(budget: ComputeBudget, memory_limit: usize) -> Self {
79 Self {
80 start_time: Instant::now(),
81 budget,
82 iterations_used: 0,
83 memory_used: 0,
84 memory_limit,
85 }
86 }
87
88 pub fn check_iteration(&mut self) -> Result<(), SolverError> {
99 self.iterations_used += 1;
100
101 if self.iterations_used > self.budget.max_iterations {
103 return Err(SolverError::BudgetExhausted {
104 reason: format!(
105 "iteration limit reached ({} > {})",
106 self.iterations_used, self.budget.max_iterations,
107 ),
108 elapsed: self.start_time.elapsed(),
109 });
110 }
111
112 let elapsed = self.start_time.elapsed();
114 if elapsed > self.budget.max_time {
115 return Err(SolverError::BudgetExhausted {
116 reason: format!(
117 "wall-clock time limit reached ({:.2?} > {:.2?})",
118 elapsed, self.budget.max_time,
119 ),
120 elapsed,
121 });
122 }
123
124 Ok(())
125 }
126
127 pub fn check_memory(&mut self, additional: usize) -> Result<(), SolverError> {
141 let new_total = self.memory_used.saturating_add(additional);
142 if new_total > self.memory_limit {
143 return Err(SolverError::BudgetExhausted {
144 reason: format!(
145 "memory limit reached ({} + {} = {} > {} bytes)",
146 self.memory_used, additional, new_total, self.memory_limit,
147 ),
148 elapsed: self.start_time.elapsed(),
149 });
150 }
151 self.memory_used = new_total;
152 Ok(())
153 }
154
155 #[inline]
157 pub fn elapsed_us(&self) -> u64 {
158 self.start_time.elapsed().as_micros() as u64
159 }
160
161 #[inline]
163 pub fn elapsed(&self) -> std::time::Duration {
164 self.start_time.elapsed()
165 }
166
167 #[inline]
169 pub fn iterations_used(&self) -> usize {
170 self.iterations_used
171 }
172
173 #[inline]
175 pub fn memory_used(&self) -> usize {
176 self.memory_used
177 }
178
179 #[inline]
181 pub fn tolerance(&self) -> f64 {
182 self.budget.tolerance
183 }
184
185 #[inline]
187 pub fn budget(&self) -> &ComputeBudget {
188 &self.budget
189 }
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::types::ComputeBudget;
200 use std::time::Duration;
201
202 fn tiny_budget() -> ComputeBudget {
203 ComputeBudget {
204 max_time: Duration::from_secs(60),
205 max_iterations: 5,
206 tolerance: 1e-6,
207 }
208 }
209
210 #[test]
211 fn iterations_within_budget() {
212 let mut enforcer = BudgetEnforcer::new(tiny_budget());
213 for _ in 0..5 {
214 enforcer.check_iteration().unwrap();
215 }
216 assert_eq!(enforcer.iterations_used(), 5);
217 }
218
219 #[test]
220 fn iteration_limit_exceeded() {
221 let mut enforcer = BudgetEnforcer::new(tiny_budget());
222 for _ in 0..5 {
223 enforcer.check_iteration().unwrap();
224 }
225 let err = enforcer.check_iteration().unwrap_err();
227 match err {
228 SolverError::BudgetExhausted { ref reason, .. } => {
229 assert!(reason.contains("iteration"), "reason: {reason}");
230 }
231 other => panic!("expected BudgetExhausted, got {other:?}"),
232 }
233 }
234
235 #[test]
236 fn wall_clock_limit_exceeded() {
237 let budget = ComputeBudget {
238 max_time: Duration::from_nanos(1), max_iterations: 1_000_000,
240 tolerance: 1e-6,
241 };
242 let mut enforcer = BudgetEnforcer::new(budget);
243
244 std::thread::sleep(Duration::from_micros(10));
246
247 let err = enforcer.check_iteration().unwrap_err();
248 match err {
249 SolverError::BudgetExhausted { ref reason, .. } => {
250 assert!(reason.contains("wall-clock"), "reason: {reason}");
251 }
252 other => panic!("expected BudgetExhausted for time, got {other:?}"),
253 }
254 }
255
256 #[test]
257 fn memory_within_budget() {
258 let mut enforcer = BudgetEnforcer::with_memory_limit(tiny_budget(), 1024);
259 enforcer.check_memory(512).unwrap();
260 enforcer.check_memory(512).unwrap();
261 assert_eq!(enforcer.memory_used(), 1024);
262 }
263
264 #[test]
265 fn memory_limit_exceeded() {
266 let mut enforcer = BudgetEnforcer::with_memory_limit(tiny_budget(), 1024);
267 enforcer.check_memory(800).unwrap();
268
269 let err = enforcer.check_memory(300).unwrap_err();
270 match err {
271 SolverError::BudgetExhausted { ref reason, .. } => {
272 assert!(reason.contains("memory"), "reason: {reason}");
273 }
274 other => panic!("expected BudgetExhausted for memory, got {other:?}"),
275 }
276 assert_eq!(enforcer.memory_used(), 800);
278 }
279
280 #[test]
281 fn memory_saturating_add_no_panic() {
282 let limit = usize::MAX / 2;
284 let mut enforcer = BudgetEnforcer::with_memory_limit(tiny_budget(), limit);
285 enforcer.check_memory(limit - 1).unwrap();
286 let err = enforcer.check_memory(usize::MAX).unwrap_err();
288 assert!(matches!(err, SolverError::BudgetExhausted { .. }));
289 }
290
291 #[test]
292 fn elapsed_us_positive() {
293 let enforcer = BudgetEnforcer::new(tiny_budget());
294 let _ = enforcer.elapsed_us();
296 }
297
298 #[test]
299 fn tolerance_accessor() {
300 let enforcer = BudgetEnforcer::new(tiny_budget());
301 assert!((enforcer.tolerance() - 1e-6).abs() < f64::EPSILON);
302 }
303
304 #[test]
305 fn budget_accessor() {
306 let budget = tiny_budget();
307 let enforcer = BudgetEnforcer::new(budget.clone());
308 assert_eq!(enforcer.budget().max_iterations, 5);
309 }
310}