Skip to main content

oxiz_solver/
resource_limits.rs

1//! Resource limits and monitoring for the SMT solver.
2//!
3//! This module provides comprehensive resource limit support including:
4//! - Wall-clock timeout
5//! - Conflict budget for the SAT solver
6//! - Decision budget
7//! - Restart budget
8//! - Memory limits
9//! - Theory check budget
10//!
11//! # Example
12//!
13//! ```
14//! use oxiz_solver::resource_limits::{ResourceLimits, ResourceMonitor};
15//! use core::time::Duration;
16//!
17//! let limits = ResourceLimits::new()
18//!     .with_timeout(Duration::from_secs(30))
19//!     .with_max_conflicts(10_000)
20//!     .with_max_decisions(100_000);
21//!
22//! let monitor = ResourceMonitor::new(limits);
23//! ```
24
25#[allow(unused_imports)]
26use crate::prelude::*;
27
28/// Which resource limit was exhausted
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ResourceExhausted {
31    /// Wall-clock timeout was reached
32    Timeout,
33    /// Maximum number of SAT conflicts was reached
34    MaxConflicts,
35    /// Maximum memory usage was reached
36    MaxMemory,
37    /// Maximum number of decisions was reached
38    MaxDecisions,
39    /// Maximum number of restarts was reached
40    MaxRestarts,
41    /// Maximum number of theory checks was reached
42    MaxTheoryChecks,
43}
44
45impl core::fmt::Display for ResourceExhausted {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        match self {
48            ResourceExhausted::Timeout => write!(f, "timeout"),
49            ResourceExhausted::MaxConflicts => write!(f, "max conflicts reached"),
50            ResourceExhausted::MaxMemory => write!(f, "max memory reached"),
51            ResourceExhausted::MaxDecisions => write!(f, "max decisions reached"),
52            ResourceExhausted::MaxRestarts => write!(f, "max restarts reached"),
53            ResourceExhausted::MaxTheoryChecks => write!(f, "max theory checks reached"),
54        }
55    }
56}
57
58/// Configuration for resource limits on solver execution.
59///
60/// All limits are optional. When a limit is `None`, that resource is unlimited.
61#[derive(Debug, Clone, Default)]
62pub struct ResourceLimits {
63    /// Wall-clock timeout duration
64    pub timeout: Option<core::time::Duration>,
65    /// Maximum number of SAT conflicts before giving up
66    pub max_conflicts: Option<u64>,
67    /// Maximum memory in megabytes
68    pub max_memory_mb: Option<u64>,
69    /// Maximum number of decisions before giving up
70    pub max_decisions: Option<u64>,
71    /// Maximum number of restarts before giving up
72    pub max_restarts: Option<u64>,
73    /// Maximum number of theory consistency checks
74    pub max_theory_checks: Option<u64>,
75}
76
77impl ResourceLimits {
78    /// Create a new `ResourceLimits` with no limits set.
79    #[must_use]
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Set a wall-clock timeout.
85    #[must_use]
86    pub fn with_timeout(mut self, timeout: core::time::Duration) -> Self {
87        self.timeout = Some(timeout);
88        self
89    }
90
91    /// Set the maximum number of SAT conflicts.
92    #[must_use]
93    pub fn with_max_conflicts(mut self, max: u64) -> Self {
94        self.max_conflicts = Some(max);
95        self
96    }
97
98    /// Set the maximum memory in megabytes.
99    #[must_use]
100    pub fn with_max_memory_mb(mut self, max: u64) -> Self {
101        self.max_memory_mb = Some(max);
102        self
103    }
104
105    /// Set the maximum number of decisions.
106    #[must_use]
107    pub fn with_max_decisions(mut self, max: u64) -> Self {
108        self.max_decisions = Some(max);
109        self
110    }
111
112    /// Set the maximum number of restarts.
113    #[must_use]
114    pub fn with_max_restarts(mut self, max: u64) -> Self {
115        self.max_restarts = Some(max);
116        self
117    }
118
119    /// Set the maximum number of theory checks.
120    #[must_use]
121    pub fn with_max_theory_checks(mut self, max: u64) -> Self {
122        self.max_theory_checks = Some(max);
123        self
124    }
125
126    /// Check if any limits are set.
127    #[must_use]
128    pub fn has_any_limit(&self) -> bool {
129        self.timeout.is_some()
130            || self.max_conflicts.is_some()
131            || self.max_memory_mb.is_some()
132            || self.max_decisions.is_some()
133            || self.max_restarts.is_some()
134            || self.max_theory_checks.is_some()
135    }
136}
137
138/// Tracks resource usage and checks whether limits have been exceeded.
139///
140/// The monitor is created from a [`ResourceLimits`] configuration and then
141/// updated as the solver progresses. Call [`check`](ResourceMonitor::check) to
142/// see if any limit has been hit.
143#[derive(Debug, Clone)]
144pub struct ResourceMonitor {
145    /// The configured limits
146    limits: ResourceLimits,
147    /// Number of conflicts observed so far
148    pub conflicts: u64,
149    /// Number of decisions observed so far
150    pub decisions: u64,
151    /// Number of restarts observed so far
152    pub restarts: u64,
153    /// Number of theory checks observed so far
154    pub theory_checks: u64,
155    /// Start time (only meaningful with std)
156    #[cfg(feature = "std")]
157    start_time: Option<std::time::Instant>,
158}
159
160impl ResourceMonitor {
161    /// Create a new monitor with the given limits and reset all counters.
162    #[must_use]
163    pub fn new(limits: ResourceLimits) -> Self {
164        Self {
165            #[cfg(feature = "std")]
166            start_time: if limits.timeout.is_some() {
167                Some(std::time::Instant::now())
168            } else {
169                None
170            },
171            limits,
172            conflicts: 0,
173            decisions: 0,
174            restarts: 0,
175            theory_checks: 0,
176        }
177    }
178
179    /// Reset counters and restart the timer.
180    pub fn reset(&mut self) {
181        self.conflicts = 0;
182        self.decisions = 0;
183        self.restarts = 0;
184        self.theory_checks = 0;
185        #[cfg(feature = "std")]
186        {
187            self.start_time = if self.limits.timeout.is_some() {
188                Some(std::time::Instant::now())
189            } else {
190                None
191            };
192        }
193    }
194
195    /// Record one conflict.
196    pub fn record_conflict(&mut self) {
197        self.conflicts += 1;
198    }
199
200    /// Record one decision.
201    pub fn record_decision(&mut self) {
202        self.decisions += 1;
203    }
204
205    /// Record one restart.
206    pub fn record_restart(&mut self) {
207        self.restarts += 1;
208    }
209
210    /// Record one theory check.
211    pub fn record_theory_check(&mut self) {
212        self.theory_checks += 1;
213    }
214
215    /// Check whether any resource limit has been exceeded.
216    ///
217    /// Returns `Some(reason)` if a limit was hit, or `None` if all limits are
218    /// still satisfied.
219    #[must_use]
220    pub fn check(&self) -> Option<ResourceExhausted> {
221        // Check timeout (std only)
222        #[cfg(feature = "std")]
223        if let (Some(timeout), Some(start)) = (self.limits.timeout, self.start_time) {
224            if start.elapsed() >= timeout {
225                return Some(ResourceExhausted::Timeout);
226            }
227        }
228
229        // Check conflicts
230        if let Some(max) = self.limits.max_conflicts {
231            if self.conflicts >= max {
232                return Some(ResourceExhausted::MaxConflicts);
233            }
234        }
235
236        // Check decisions
237        if let Some(max) = self.limits.max_decisions {
238            if self.decisions >= max {
239                return Some(ResourceExhausted::MaxDecisions);
240            }
241        }
242
243        // Check restarts
244        if let Some(max) = self.limits.max_restarts {
245            if self.restarts >= max {
246                return Some(ResourceExhausted::MaxRestarts);
247            }
248        }
249
250        // Check theory checks
251        if let Some(max) = self.limits.max_theory_checks {
252            if self.theory_checks >= max {
253                return Some(ResourceExhausted::MaxTheoryChecks);
254            }
255        }
256
257        // Check memory (std only, best-effort)
258        #[cfg(feature = "std")]
259        if let Some(max_mb) = self.limits.max_memory_mb {
260            // Use a simple heuristic: check process memory via /proc on Linux
261            // or fallback to allocated heap estimation
262            if let Some(current_mb) = Self::estimate_memory_mb() {
263                if current_mb >= max_mb {
264                    return Some(ResourceExhausted::MaxMemory);
265                }
266            }
267        }
268
269        None
270    }
271
272    /// Check limits and return a `Result`.
273    ///
274    /// Convenience wrapper: returns `Ok(())` when no limit is hit, or
275    /// `Err(ResourceExhausted)` when one is.
276    pub fn check_result(&self) -> core::result::Result<(), ResourceExhausted> {
277        match self.check() {
278            Some(reason) => Err(reason),
279            None => Ok(()),
280        }
281    }
282
283    /// Get the configured limits.
284    #[must_use]
285    pub fn limits(&self) -> &ResourceLimits {
286        &self.limits
287    }
288
289    /// Get elapsed time (std only).
290    #[cfg(feature = "std")]
291    #[must_use]
292    pub fn elapsed(&self) -> Option<core::time::Duration> {
293        self.start_time.map(|s| s.elapsed())
294    }
295
296    /// Estimate current process memory usage in megabytes (best-effort).
297    #[cfg(feature = "std")]
298    fn estimate_memory_mb() -> Option<u64> {
299        // Try reading /proc/self/statm on Linux
300        #[cfg(target_os = "linux")]
301        {
302            if let Ok(contents) = std::fs::read_to_string("/proc/self/statm") {
303                // statm fields: size resident shared text lib data dt (in pages)
304                let mut parts = contents.split_whitespace();
305                if let Some(resident_str) = parts.nth(1) {
306                    if let Ok(pages) = resident_str.parse::<u64>() {
307                        let page_size = 4096u64; // typical page size
308                        return Some(pages * page_size / (1024 * 1024));
309                    }
310                }
311            }
312            None
313        }
314
315        #[cfg(target_os = "macos")]
316        {
317            // On macOS, use mach task_info to get resident memory
318            // Fallback: rough estimate based on allocator stats is not portable,
319            // so return None to skip the memory limit check.
320            None
321        }
322
323        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
324        {
325            None
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use core::time::Duration;
334
335    #[test]
336    fn test_resource_limits_default() {
337        let limits = ResourceLimits::new();
338        assert!(limits.timeout.is_none());
339        assert!(limits.max_conflicts.is_none());
340        assert!(limits.max_memory_mb.is_none());
341        assert!(limits.max_decisions.is_none());
342        assert!(limits.max_restarts.is_none());
343        assert!(limits.max_theory_checks.is_none());
344        assert!(!limits.has_any_limit());
345    }
346
347    #[test]
348    fn test_resource_limits_builder() {
349        let limits = ResourceLimits::new()
350            .with_timeout(Duration::from_secs(30))
351            .with_max_conflicts(10_000)
352            .with_max_decisions(100_000)
353            .with_max_restarts(500)
354            .with_max_theory_checks(50_000)
355            .with_max_memory_mb(1024);
356
357        assert_eq!(limits.timeout, Some(Duration::from_secs(30)));
358        assert_eq!(limits.max_conflicts, Some(10_000));
359        assert_eq!(limits.max_decisions, Some(100_000));
360        assert_eq!(limits.max_restarts, Some(500));
361        assert_eq!(limits.max_theory_checks, Some(50_000));
362        assert_eq!(limits.max_memory_mb, Some(1024));
363        assert!(limits.has_any_limit());
364    }
365
366    #[test]
367    fn test_monitor_no_limits() {
368        let monitor = ResourceMonitor::new(ResourceLimits::new());
369        assert!(monitor.check().is_none());
370    }
371
372    #[test]
373    fn test_monitor_conflict_limit() {
374        let limits = ResourceLimits::new().with_max_conflicts(5);
375        let mut monitor = ResourceMonitor::new(limits);
376
377        for _ in 0..4 {
378            monitor.record_conflict();
379            assert!(monitor.check().is_none());
380        }
381        monitor.record_conflict();
382        assert_eq!(monitor.check(), Some(ResourceExhausted::MaxConflicts));
383    }
384
385    #[test]
386    fn test_monitor_decision_limit() {
387        let limits = ResourceLimits::new().with_max_decisions(3);
388        let mut monitor = ResourceMonitor::new(limits);
389
390        monitor.record_decision();
391        monitor.record_decision();
392        assert!(monitor.check().is_none());
393        monitor.record_decision();
394        assert_eq!(monitor.check(), Some(ResourceExhausted::MaxDecisions));
395    }
396
397    #[test]
398    fn test_monitor_restart_limit() {
399        let limits = ResourceLimits::new().with_max_restarts(2);
400        let mut monitor = ResourceMonitor::new(limits);
401
402        monitor.record_restart();
403        assert!(monitor.check().is_none());
404        monitor.record_restart();
405        assert_eq!(monitor.check(), Some(ResourceExhausted::MaxRestarts));
406    }
407
408    #[test]
409    fn test_monitor_theory_check_limit() {
410        let limits = ResourceLimits::new().with_max_theory_checks(10);
411        let mut monitor = ResourceMonitor::new(limits);
412
413        for _ in 0..9 {
414            monitor.record_theory_check();
415        }
416        assert!(monitor.check().is_none());
417        monitor.record_theory_check();
418        assert_eq!(monitor.check(), Some(ResourceExhausted::MaxTheoryChecks));
419    }
420
421    #[test]
422    fn test_monitor_reset() {
423        let limits = ResourceLimits::new().with_max_conflicts(5);
424        let mut monitor = ResourceMonitor::new(limits);
425
426        for _ in 0..5 {
427            monitor.record_conflict();
428        }
429        assert!(monitor.check().is_some());
430
431        monitor.reset();
432        assert!(monitor.check().is_none());
433        assert_eq!(monitor.conflicts, 0);
434    }
435
436    #[test]
437    fn test_monitor_check_result() {
438        let limits = ResourceLimits::new().with_max_conflicts(1);
439        let mut monitor = ResourceMonitor::new(limits);
440
441        assert!(monitor.check_result().is_ok());
442        monitor.record_conflict();
443        assert!(monitor.check_result().is_err());
444    }
445
446    #[test]
447    fn test_resource_exhausted_display() {
448        assert_eq!(ResourceExhausted::Timeout.to_string(), "timeout");
449        assert_eq!(
450            ResourceExhausted::MaxConflicts.to_string(),
451            "max conflicts reached"
452        );
453        assert_eq!(
454            ResourceExhausted::MaxMemory.to_string(),
455            "max memory reached"
456        );
457        assert_eq!(
458            ResourceExhausted::MaxDecisions.to_string(),
459            "max decisions reached"
460        );
461        assert_eq!(
462            ResourceExhausted::MaxRestarts.to_string(),
463            "max restarts reached"
464        );
465        assert_eq!(
466            ResourceExhausted::MaxTheoryChecks.to_string(),
467            "max theory checks reached"
468        );
469    }
470
471    #[cfg(feature = "std")]
472    #[test]
473    fn test_monitor_timeout_not_hit_immediately() {
474        let limits = ResourceLimits::new().with_timeout(Duration::from_secs(60));
475        let monitor = ResourceMonitor::new(limits);
476        // A 60-second timeout should definitely not be hit immediately
477        assert!(monitor.check().is_none());
478    }
479
480    #[cfg(feature = "std")]
481    #[test]
482    fn test_monitor_elapsed() {
483        let limits = ResourceLimits::new().with_timeout(Duration::from_secs(60));
484        let monitor = ResourceMonitor::new(limits);
485        let elapsed = monitor.elapsed();
486        assert!(elapsed.is_some());
487        // Should be very small since we just created it
488        assert!(elapsed.is_some_and(|e| e < Duration::from_secs(1)));
489    }
490}