Skip to main content

optirs_core/streaming/adaptive_streaming/
resource_management.rs

1// Resource allocation and monitoring for streaming optimization
2//
3// This module provides comprehensive resource management capabilities including
4// dynamic resource allocation, monitoring, budgeting, and optimization for
5// streaming optimization workloads.
6
7use super::config::*;
8use super::optimizer::{Adaptation, AdaptationPriority, AdaptationType};
9
10use serde::Serialize;
11use std::collections::{HashMap, VecDeque};
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
14use std::time::{Duration, Instant};
15
16mod alerts;
17mod optimization;
18mod prediction;
19mod probe;
20
21#[cfg(test)]
22mod regression_tests;
23
24pub use probe::SystemProbe;
25
26/// How often the monitoring thread checks its shutdown flag (R4). Kept short
27/// so `stop_monitoring` returns promptly even when `monitoring_frequency` is
28/// measured in minutes.
29const SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(50);
30
31/// Recovers a mutex guard even when the lock was poisoned. The values guarded
32/// in this module are plain snapshots with no cross-field invariant a panic
33/// could leave half-written, so continuing with the recovered value is better
34/// than propagating the panic into every monitoring call.
35pub(crate) fn lock_recovered<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
36    mutex.lock().unwrap_or_else(PoisonError::into_inner)
37}
38
39/// Current resource usage information
40#[derive(Debug, Clone, Serialize)]
41pub struct ResourceUsage {
42    /// Memory usage in MB
43    pub memory_usage_mb: usize,
44    /// Total system memory in MB (R2 fix). Populated from the real system
45    /// total at collection time so `memory_usage_mb` can be turned into an
46    /// honest percentage instead of assuming a fixed 1024 MB (1 GB) total,
47    /// which pinned `memory_percent`/`memory_utilization` above 100% (and
48    /// therefore alert severity at `Emergency`) on essentially every real
49    /// machine. `0` (the `Default` value) means "unknown"; callers must
50    /// treat that as "cannot compute a percentage", not as "0 MB total".
51    pub total_memory_mb: usize,
52    /// Memory this *process* is using, in MB, or `None` when the process
53    /// counters are unavailable.
54    ///
55    /// `memory_usage_mb`/`total_memory_mb` are system-wide and only meaningful
56    /// as a ratio (which is what the memory alert thresholds compare). The
57    /// allocation budget, by contrast, is a per-process figure, so comparing
58    /// system-wide usage against it reports several hundred percent utilization
59    /// on any real machine — the same class of error as R2, one level up. That
60    /// comparison was inert only because `start_monitoring` was never called
61    /// and `current_usage` stayed all-zero; every budget consumer now reads this
62    /// field instead.
63    pub process_memory_mb: Option<usize>,
64    /// CPU usage percentage (0-100).
65    ///
66    /// R1: this used to be the hardcoded literal `50.0`. It is now the real
67    /// system-wide CPU usage read from a *persistent* `sysinfo::System`,
68    /// because CPU usage is a delta between two refreshes: a freshly built
69    /// `System` always reports 0. Until two refreshes at least
70    /// `sysinfo::MINIMUM_CPU_UPDATE_INTERVAL` apart have happened this field
71    /// holds `0.0` and `cpu_usage_percent_valid` is `false`; consumers must
72    /// check the flag rather than treating the placeholder as a measurement.
73    pub cpu_usage_percent: f64,
74    /// Whether `cpu_usage_percent` holds a real measurement.
75    pub cpu_usage_percent_valid: bool,
76    /// GPU usage percentage (0-100) if applicable. There is no pure-Rust,
77    /// vendor-neutral way to read this, so it stays `None` unless an external
78    /// probe supplies it.
79    pub gpu_usage_percent: Option<f64>,
80    /// Network I/O rate in MB/s, or `None` before two interface refreshes have
81    /// produced a usable byte delta (R1: this used to be the literal `1.0`).
82    pub network_io_mbps: Option<f64>,
83    /// Disk I/O rate in MB/s for this process, or `None` before two process
84    /// refreshes have produced a usable byte delta (R1: this used to be the
85    /// literal `5.0`).
86    pub disk_io_mbps: Option<f64>,
87    /// Number of active threads
88    pub active_threads: usize,
89    /// Timestamp of measurement
90    #[serde(skip)]
91    pub timestamp: Instant,
92}
93
94impl ResourceUsage {
95    /// Memory this process is using, in MB, when it could be measured.
96    pub fn process_memory(&self) -> Option<usize> {
97        self.process_memory_mb
98    }
99
100    /// Real CPU usage, or `None` when no valid measurement has been taken yet.
101    pub fn cpu_usage(&self) -> Option<f64> {
102        if self.cpu_usage_percent_valid {
103            Some(self.cpu_usage_percent)
104        } else {
105            None
106        }
107    }
108
109    /// Real memory-usage percentage (R2), `used / total * 100`. Returns
110    /// `None` when `total_memory_mb` is unknown (0) rather than fabricating
111    /// a value against a wrong assumed total.
112    pub fn memory_usage_percent(&self) -> Option<f64> {
113        if self.total_memory_mb == 0 {
114            None
115        } else {
116            Some((self.memory_usage_mb as f64 / self.total_memory_mb as f64) * 100.0)
117        }
118    }
119}
120
121/// Resource budget and constraints
122#[derive(Debug, Clone)]
123pub struct ResourceBudget {
124    /// Memory budget constraints
125    pub memory_budget: MemoryBudget,
126    /// CPU budget constraints
127    pub cpu_budget: CpuBudget,
128    /// Network budget constraints
129    pub network_budget: NetworkBudget,
130    /// Time budget constraints
131    pub time_budget: TimeBudget,
132    /// Enforcement strategy
133    pub enforcement_strategy: BudgetEnforcementStrategy,
134    /// Budget flexibility (0.0 = strict, 1.0 = flexible)
135    pub flexibility: f64,
136}
137
138/// Memory budget configuration
139#[derive(Debug, Clone)]
140pub struct MemoryBudget {
141    /// Maximum memory allocation in MB
142    pub max_allocation_mb: usize,
143    /// Soft limit for memory usage in MB
144    pub soft_limit_mb: usize,
145    /// Memory cleanup threshold (percentage)
146    pub cleanup_threshold: f64,
147    /// Enable memory compression
148    pub enable_compression: bool,
149    /// Memory priority levels
150    pub priority_levels: Vec<MemoryPriority>,
151}
152
153/// Memory allocation priority levels
154#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
155pub enum MemoryPriority {
156    /// Critical memory for core operations
157    Critical,
158    /// High priority memory for optimization
159    High,
160    /// Normal priority memory for buffering
161    Normal,
162    /// Low priority memory for caching
163    Low,
164    /// Temporary memory that can be freed immediately
165    Temporary,
166}
167
168/// CPU budget configuration
169#[derive(Debug, Clone)]
170pub struct CpuBudget {
171    /// Maximum CPU utilization percentage
172    pub max_utilization: f64,
173    /// Target CPU utilization percentage
174    pub target_utilization: f64,
175    /// Maximum number of worker threads
176    pub max_threads: usize,
177    /// Thread priority management
178    pub thread_priority: ThreadPriorityConfig,
179    /// CPU affinity settings
180    pub cpu_affinity: Option<Vec<usize>>,
181}
182
183/// Thread priority configuration
184#[derive(Debug, Clone)]
185pub struct ThreadPriorityConfig {
186    /// High priority thread count
187    pub high_priority_threads: usize,
188    /// Normal priority thread count
189    pub normal_priority_threads: usize,
190    /// Background thread count
191    pub background_threads: usize,
192    /// Enable dynamic priority adjustment
193    pub dynamic_priority: bool,
194}
195
196/// Network budget configuration
197#[derive(Debug, Clone)]
198pub struct NetworkBudget {
199    /// Maximum bandwidth usage in MB/s
200    pub max_bandwidth_mbps: f64,
201    /// Bandwidth priority allocation
202    pub priority_allocation: HashMap<String, f64>,
203    /// Enable traffic shaping
204    pub enable_traffic_shaping: bool,
205    /// Quality of Service settings
206    pub qos_settings: QoSSettings,
207}
208
209/// Quality of Service settings for network traffic
210#[derive(Debug, Clone)]
211pub struct QoSSettings {
212    /// Latency requirements in milliseconds
213    pub max_latency_ms: u64,
214    /// Jitter tolerance in milliseconds
215    pub jitter_tolerance_ms: u64,
216    /// Packet loss tolerance (percentage)
217    pub packet_loss_tolerance: f64,
218    /// Traffic classes
219    pub traffic_classes: Vec<TrafficClass>,
220}
221
222/// Network traffic classification
223#[derive(Debug, Clone)]
224pub struct TrafficClass {
225    /// Class name
226    pub name: String,
227    /// Priority level (0 = highest)
228    pub priority: u8,
229    /// Bandwidth guarantee (percentage)
230    pub bandwidth_guarantee: f64,
231    /// Maximum bandwidth (percentage)
232    pub max_bandwidth: f64,
233}
234
235/// Time budget configuration
236#[derive(Debug, Clone)]
237pub struct TimeBudget {
238    /// Maximum processing time per batch
239    pub max_batch_processing_time: Duration,
240    /// Target processing time per batch
241    pub target_batch_processing_time: Duration,
242    /// Timeout for long-running operations
243    pub operation_timeout: Duration,
244    /// Deadline enforcement strategy
245    pub deadline_enforcement: DeadlineEnforcement,
246}
247
248/// Deadline enforcement strategies
249#[derive(Debug, Clone)]
250pub enum DeadlineEnforcement {
251    /// Strict deadline enforcement (fail if exceeded)
252    Strict,
253    /// Soft deadline with warnings
254    Soft,
255    /// Best effort (informational only)
256    BestEffort,
257    /// Adaptive deadline based on system load
258    Adaptive,
259}
260
261/// Budget enforcement strategies
262#[derive(Debug, Clone)]
263pub enum BudgetEnforcementStrategy {
264    /// Strict enforcement (fail if budget exceeded)
265    Strict,
266    /// Throttling (reduce resource usage)
267    Throttling,
268    /// Load shedding (drop low priority work)
269    LoadShedding,
270    /// Graceful degradation
271    GracefulDegradation,
272    /// Adaptive enforcement based on system state
273    Adaptive,
274}
275
276/// Resource manager for streaming optimization
277pub struct ResourceManager {
278    /// Resource configuration
279    config: ResourceConfig,
280    /// Current resource usage
281    current_usage: Arc<Mutex<ResourceUsage>>,
282    /// Resource usage history
283    usage_history: Arc<Mutex<VecDeque<ResourceUsage>>>,
284    /// Resource budget
285    budget: ResourceBudget,
286    /// Resource allocations by component
287    allocations: Arc<Mutex<HashMap<String, ResourceAllocation>>>,
288    /// Resource monitoring thread handle
289    monitoring_handle: Option<std::thread::JoinHandle<()>>,
290    /// Shutdown flag for the monitoring thread (R4). Without it the thread
291    /// looped forever, outliving the manager that spawned it.
292    shutdown: Arc<AtomicBool>,
293    /// Long-lived OS probe shared with the monitoring thread (R1)
294    probe: Arc<Mutex<SystemProbe>>,
295    /// When `update_utilization` last collected a sample itself
296    last_synchronous_sample: Option<Instant>,
297    /// Resource prediction model
298    predictor: ResourcePredictor,
299    /// Resource optimizer
300    optimizer: ResourceOptimizer,
301    /// Alert system
302    alert_system: ResourceAlertSystem,
303    /// Real budget-violation counter (R7: `get_diagnostics` used to report a
304    /// hardcoded `0` with a "would be calculated" comment)
305    budget_violations: Arc<AtomicU64>,
306    /// Accumulated penalty from budget violations, scaled by
307    /// `ResourceBudgetConstraints::violation_penalty`
308    budget_penalty: f64,
309}
310
311/// Resource allocation for a specific component
312#[derive(Debug, Clone)]
313pub struct ResourceAllocation {
314    /// Component name
315    pub component_name: String,
316    /// Allocated memory in MB
317    pub allocated_memory_mb: usize,
318    /// Allocated CPU percentage
319    pub allocated_cpu_percent: f64,
320    /// Allocated network bandwidth in MB/s
321    pub allocated_bandwidth_mbps: f64,
322    /// Priority level
323    pub priority: ResourcePriority,
324    /// Allocation timestamp
325    pub allocation_time: Instant,
326    /// Last access timestamp
327    pub last_access: Instant,
328    /// Usage statistics
329    pub usage_stats: ComponentUsageStats,
330}
331
332/// Resource priority levels
333#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
334pub enum ResourcePriority {
335    /// Critical system resources
336    Critical = 0,
337    /// High priority operations
338    High = 1,
339    /// Normal priority operations
340    Normal = 2,
341    /// Low priority background operations
342    Low = 3,
343    /// Temporary or cache operations
344    Temporary = 4,
345}
346
347/// Usage statistics for a component
348#[derive(Debug, Clone)]
349pub struct ComponentUsageStats {
350    /// Peak memory usage
351    pub peak_memory_mb: usize,
352    /// Average memory usage
353    pub avg_memory_mb: usize,
354    /// Peak CPU usage
355    pub peak_cpu_percent: f64,
356    /// Average CPU usage
357    pub avg_cpu_percent: f64,
358    /// Total processing time
359    pub total_processing_time: Duration,
360    /// Number of operations performed
361    pub operation_count: u64,
362    /// Efficiency score (0.0 to 1.0)
363    pub efficiency_score: f64,
364}
365
366/// Resource usage prediction model
367pub struct ResourcePredictor {
368    /// Historical usage patterns
369    pub(crate) usage_patterns: VecDeque<ResourceUsage>,
370    /// Prediction horizon (steps ahead)
371    pub(crate) prediction_horizon: usize,
372    /// Mean absolute percentage error per resource, measured by scoring each
373    /// prediction against the sample that actually arrived (R7)
374    pub(crate) prediction_accuracy: HashMap<String, f64>,
375    /// Per-minute-of-hour seasonal profile, keyed by resource
376    pub(crate) seasonal_patterns: HashMap<String, Vec<f64>>,
377    /// Trend analysis
378    pub(crate) trend_analysis: ResourceTrendAnalysis,
379    /// Whether prediction is enabled (CF1:
380    /// `ResourceConfig::enable_resource_prediction`)
381    pub(crate) enabled: bool,
382    /// Observation counts backing the seasonal profile
383    pub(crate) seasonal_counts: HashMap<String, Vec<u64>>,
384    /// The prediction awaiting a real observation, and how many samples remain
385    pub(crate) pending_prediction: Option<(usize, ResourceUsage)>,
386}
387
388/// Resource trend analysis
389#[derive(Debug, Clone)]
390pub struct ResourceTrendAnalysis {
391    /// Memory usage trend
392    pub memory_trend: TrendDirection,
393    /// CPU usage trend
394    pub cpu_trend: TrendDirection,
395    /// Network usage trend
396    pub network_trend: TrendDirection,
397    /// Trend confidence
398    pub trend_confidence: f64,
399    /// Trend stability
400    pub trend_stability: f64,
401}
402
403/// Trend direction indicators
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub enum TrendDirection {
406    /// Increasing trend
407    Increasing,
408    /// Decreasing trend
409    Decreasing,
410    /// Stable trend
411    Stable,
412    /// Oscillating trend
413    Oscillating,
414    /// Unknown trend
415    Unknown,
416}
417
418/// Resource optimizer for dynamic allocation
419pub struct ResourceOptimizer {
420    /// Optimization strategy
421    pub(crate) strategy: ResourceOptimizationStrategy,
422    /// Optimization history
423    pub(crate) optimization_history: VecDeque<OptimizationEvent>,
424    /// Performance impact tracking
425    pub(crate) performance_impact: HashMap<String, f64>,
426    /// Optimization constraints
427    pub(crate) constraints: OptimizationConstraints,
428    /// When each component last had a change applied, and its sign, used to
429    /// enforce `StabilityRequirements` (R7)
430    pub(crate) last_change: HashMap<String, (Instant, f64)>,
431    /// Pending change magnitude per component, set by `clamp_change`
432    pub(crate) pending_change: HashMap<String, f64>,
433}
434
435/// Resource optimization strategies
436#[derive(Debug, Clone)]
437pub enum ResourceOptimizationStrategy {
438    /// Conservative optimization (minimize changes)
439    Conservative,
440    /// Aggressive optimization (maximize performance)
441    Aggressive,
442    /// Balanced optimization
443    Balanced,
444    /// Power-efficient optimization
445    PowerEfficient,
446    /// Latency-optimized
447    LatencyOptimized,
448    /// Throughput-optimized
449    ThroughputOptimized,
450}
451
452/// Resource optimization event
453#[derive(Debug, Clone)]
454pub struct OptimizationEvent {
455    /// Event timestamp
456    pub timestamp: Instant,
457    /// Optimization type
458    pub optimization_type: String,
459    /// Resources affected
460    pub affected_resources: Vec<String>,
461    /// Resource deltas
462    pub resource_deltas: HashMap<String, f64>,
463    /// Performance impact
464    pub performance_impact: f64,
465    /// Success indicator
466    pub success: bool,
467}
468
469/// Constraints for resource optimization
470#[derive(Debug, Clone)]
471pub struct OptimizationConstraints {
472    /// Minimum resource guarantees
473    pub min_guarantees: HashMap<String, f64>,
474    /// Maximum resource limits
475    pub max_limits: HashMap<String, f64>,
476    /// Resource change rate limits
477    pub change_rate_limits: HashMap<String, f64>,
478    /// Stability requirements
479    pub stability_requirements: StabilityRequirements,
480}
481
482/// Stability requirements for resource allocation
483#[derive(Debug, Clone)]
484pub struct StabilityRequirements {
485    /// Minimum stable period before changes
486    pub min_stable_period: Duration,
487    /// Maximum change frequency
488    pub max_change_frequency: f64,
489    /// Oscillation prevention
490    pub prevent_oscillation: bool,
491    /// Hysteresis factor (0.0 to 1.0)
492    pub hysteresis_factor: f64,
493}
494
495/// Resource alert system
496pub struct ResourceAlertSystem {
497    /// Alert thresholds
498    pub(crate) thresholds: ResourceThresholds,
499    /// Active alerts
500    pub(crate) active_alerts: VecDeque<ResourceAlert>,
501    /// Alert history
502    pub(crate) alert_history: VecDeque<ResourceAlert>,
503    /// Alert handlers
504    pub(crate) alert_handlers: Vec<Box<dyn AlertHandler>>,
505    /// Monotonic counter backing collision-free alert identifiers (R6). The
506    /// previous scheme was `Instant::now().elapsed().as_nanos()`, which is
507    /// always ~0 because the instant is created on the same line, so every
508    /// alert for a resource shared the same id.
509    pub(crate) next_alert_id: u64,
510}
511
512/// Resource alert thresholds
513#[derive(Debug, Clone)]
514pub struct ResourceThresholds {
515    /// Memory usage thresholds
516    pub memory_thresholds: ThresholdSet,
517    /// CPU usage thresholds
518    pub cpu_thresholds: ThresholdSet,
519    /// Network usage thresholds
520    pub network_thresholds: ThresholdSet,
521    /// Response time thresholds
522    pub response_time_thresholds: ThresholdSet,
523}
524
525/// Threshold set for a resource type
526#[derive(Debug, Clone)]
527pub struct ThresholdSet {
528    /// Warning threshold
529    pub warning: f64,
530    /// Critical threshold
531    pub critical: f64,
532    /// Emergency threshold
533    pub emergency: f64,
534    /// Recovery threshold (for clearing alerts)
535    pub recovery: f64,
536}
537
538/// Resource alert
539#[derive(Debug, Clone)]
540pub struct ResourceAlert {
541    /// Alert ID
542    pub id: String,
543    /// Alert timestamp
544    pub timestamp: Instant,
545    /// Alert severity
546    pub severity: AlertSeverity,
547    /// Resource type
548    pub resource_type: String,
549    /// Current value
550    pub current_value: f64,
551    /// Threshold value
552    pub threshold_value: f64,
553    /// Alert message
554    pub message: String,
555    /// Suggested actions
556    pub suggested_actions: Vec<String>,
557    /// Auto-resolution attempts
558    pub auto_resolution_attempts: u32,
559}
560
561/// Alert severity levels
562#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
563pub enum AlertSeverity {
564    /// Informational alert
565    Info,
566    /// Warning alert
567    Warning,
568    /// Error alert
569    Error,
570    /// Critical alert
571    Critical,
572    /// Emergency alert
573    Emergency,
574}
575
576/// Trait for handling resource alerts
577pub trait AlertHandler: Send + Sync {
578    /// Handles a resource alert
579    fn handle_alert(&self, alert: &ResourceAlert) -> Result<(), String>;
580
581    /// Gets handler priority (lower number = higher priority)
582    fn priority(&self) -> u32;
583
584    /// Checks if this handler can handle the given alert
585    fn can_handle(&self, alert: &ResourceAlert) -> bool;
586}
587
588impl ResourceManager {
589    /// Creates a new resource manager
590    pub fn new(config: &StreamingConfig) -> Result<Self, String> {
591        let resource_config = config.resource_config.clone();
592        // CF1: `ResourceConfig::budget_constraints` was never read. Every one
593        // of its five fields now drives real behaviour below.
594        let constraints = resource_config.budget_constraints.clone();
595
596        let available_cpus = num_cpus::get().max(1);
597        // The old expression `num_cpus::get() - 2` underflows and panics on a
598        // one- or two-core machine.
599        let high_priority_threads = available_cpus.min(2);
600        let normal_priority_threads = available_cpus.saturating_sub(high_priority_threads);
601
602        // The soft limit is the tighter of "80% of the hard maximum" and the
603        // operator-supplied memory budget.
604        let soft_limit_mb = ((resource_config.max_memory_mb as f64 * 0.8) as usize)
605            .min(constraints.memory_budget_mb.max(1));
606
607        let budget = ResourceBudget {
608            memory_budget: MemoryBudget {
609                max_allocation_mb: resource_config.max_memory_mb,
610                soft_limit_mb,
611                cleanup_threshold: resource_config.cleanup_threshold,
612                enable_compression: true,
613                priority_levels: vec![
614                    MemoryPriority::Critical,
615                    MemoryPriority::High,
616                    MemoryPriority::Normal,
617                    MemoryPriority::Low,
618                ],
619            },
620            cpu_budget: CpuBudget {
621                max_utilization: resource_config.max_cpu_percent,
622                // Target the operator's CPU budget, never above the hard cap.
623                target_utilization: constraints
624                    .cpu_budget_percent
625                    .min(resource_config.max_cpu_percent),
626                max_threads: available_cpus,
627                thread_priority: ThreadPriorityConfig {
628                    high_priority_threads,
629                    normal_priority_threads,
630                    background_threads: 1,
631                    dynamic_priority: true,
632                },
633                cpu_affinity: None,
634            },
635            network_budget: NetworkBudget {
636                max_bandwidth_mbps: 100.0, // Default limit
637                priority_allocation: HashMap::new(),
638                enable_traffic_shaping: false,
639                qos_settings: QoSSettings {
640                    max_latency_ms: 100,
641                    jitter_tolerance_ms: 10,
642                    packet_loss_tolerance: 0.1,
643                    traffic_classes: Vec::new(),
644                },
645            },
646            time_budget: TimeBudget {
647                // The operator's per-operation time budget is the target; the
648                // hard cap is three times that, and the operation timeout is
649                // the configured budget itself.
650                max_batch_processing_time: constraints.time_budget.saturating_mul(3),
651                target_batch_processing_time: constraints.time_budget,
652                operation_timeout: constraints.time_budget,
653                deadline_enforcement: if constraints.strict_enforcement {
654                    DeadlineEnforcement::Strict
655                } else {
656                    DeadlineEnforcement::Soft
657                },
658            },
659            enforcement_strategy: if constraints.strict_enforcement {
660                BudgetEnforcementStrategy::Strict
661            } else {
662                match resource_config.allocation_strategy {
663                    ResourceAllocationStrategy::Static => BudgetEnforcementStrategy::Strict,
664                    ResourceAllocationStrategy::Dynamic => BudgetEnforcementStrategy::Throttling,
665                    ResourceAllocationStrategy::Adaptive => BudgetEnforcementStrategy::Adaptive,
666                    _ => BudgetEnforcementStrategy::GracefulDegradation,
667                }
668            },
669            // Strict enforcement means zero head-room above the budget.
670            flexibility: if constraints.strict_enforcement {
671                0.0
672            } else {
673                0.2
674            },
675        };
676
677        let predictor = ResourcePredictor::new(resource_config.enable_resource_prediction);
678        let optimizer = ResourceOptimizer::new(match resource_config.allocation_strategy {
679            ResourceAllocationStrategy::Static => ResourceOptimizationStrategy::Conservative,
680            ResourceAllocationStrategy::Dynamic => {
681                ResourceOptimizationStrategy::ThroughputOptimized
682            }
683            ResourceAllocationStrategy::PriorityBased => {
684                ResourceOptimizationStrategy::LatencyOptimized
685            }
686            _ => ResourceOptimizationStrategy::Balanced,
687        });
688        // R8: the alert thresholds used to be hardcoded percentages with no
689        // relationship to the configured limits.
690        let alert_system = ResourceAlertSystem::from_config(&resource_config, &budget);
691
692        Ok(Self {
693            config: resource_config,
694            current_usage: Arc::new(Mutex::new(ResourceUsage::default())),
695            usage_history: Arc::new(Mutex::new(VecDeque::with_capacity(1000))),
696            budget,
697            allocations: Arc::new(Mutex::new(HashMap::new())),
698            monitoring_handle: None,
699            shutdown: Arc::new(AtomicBool::new(false)),
700            probe: Arc::new(Mutex::new(SystemProbe::new())),
701            last_synchronous_sample: None,
702            predictor,
703            optimizer,
704            alert_system,
705            budget_violations: Arc::new(AtomicU64::new(0)),
706            budget_penalty: 0.0,
707        })
708    }
709
710    /// Starts resource monitoring.
711    ///
712    /// R4: the previous loop had no way to stop, so the thread outlived the
713    /// manager. It now polls a shutdown flag on a short tick (independent of
714    /// `monitoring_frequency`, which may be minutes) and
715    /// [`Self::stop_monitoring`] joins it.
716    pub fn start_monitoring(&mut self) -> Result<(), String> {
717        if self.monitoring_handle.is_some() {
718            return Ok(()); // Already monitoring
719        }
720
721        let current_usage = Arc::clone(&self.current_usage);
722        let usage_history = Arc::clone(&self.usage_history);
723        let shutdown = Arc::clone(&self.shutdown);
724        let probe = Arc::clone(&self.probe);
725        let monitoring_frequency = self.config.monitoring_frequency;
726
727        shutdown.store(false, Ordering::SeqCst);
728        let handle = std::thread::Builder::new()
729            .name("optirs-resource-monitor".to_string())
730            .spawn(move || {
731                let mut next_sample = Instant::now();
732                while !shutdown.load(Ordering::SeqCst) {
733                    let now = Instant::now();
734                    if now >= next_sample {
735                        let usage = lock_recovered(&probe).sample();
736                        {
737                            let mut current = lock_recovered(&current_usage);
738                            *current = usage.clone();
739                        }
740                        {
741                            let mut history = lock_recovered(&usage_history);
742                            if history.len() >= 1000 {
743                                history.pop_front();
744                            }
745                            history.push_back(usage);
746                        }
747                        next_sample = now + monitoring_frequency.max(SHUTDOWN_POLL_INTERVAL);
748                    }
749                    std::thread::sleep(SHUTDOWN_POLL_INTERVAL);
750                }
751            })
752            .map_err(|error| format!("failed to spawn the resource monitor thread: {error}"))?;
753
754        self.monitoring_handle = Some(handle);
755        Ok(())
756    }
757
758    /// Signals the monitoring thread to stop and waits for it.
759    pub fn stop_monitoring(&mut self) -> Result<(), String> {
760        self.shutdown.store(true, Ordering::SeqCst);
761        if let Some(handle) = self.monitoring_handle.take() {
762            handle
763                .join()
764                .map_err(|_| "the resource monitor thread panicked".to_string())?;
765        }
766        Ok(())
767    }
768
769    /// Whether the background monitor is running.
770    pub fn is_monitoring(&self) -> bool {
771        self.monitoring_handle.is_some()
772    }
773
774    /// Collects current resource usage from the long-lived probe.
775    pub fn collect_resource_usage(&self) -> ResourceUsage {
776        lock_recovered(&self.probe).sample()
777    }
778
779    /// Allocates resources for a component
780    pub fn allocate_resources(
781        &mut self,
782        component_name: &str,
783        memory_mb: usize,
784        cpu_percent: f64,
785        priority: ResourcePriority,
786    ) -> Result<(), String> {
787        // Check budget constraints
788        self.check_budget_constraints(memory_mb, cpu_percent)?;
789
790        let allocation = ResourceAllocation {
791            component_name: component_name.to_string(),
792            allocated_memory_mb: memory_mb,
793            allocated_cpu_percent: cpu_percent,
794            allocated_bandwidth_mbps: 0.0, // Default
795            priority,
796            allocation_time: Instant::now(),
797            last_access: Instant::now(),
798            usage_stats: ComponentUsageStats {
799                peak_memory_mb: 0,
800                avg_memory_mb: 0,
801                peak_cpu_percent: 0.0,
802                avg_cpu_percent: 0.0,
803                total_processing_time: Duration::ZERO,
804                operation_count: 0,
805                efficiency_score: 1.0,
806            },
807        };
808
809        let mut allocations = lock_recovered(&self.allocations);
810        allocations.insert(component_name.to_string(), allocation);
811
812        Ok(())
813    }
814
815    /// Checks budget constraints for resource allocation.
816    ///
817    /// CF1: `ResourceBudgetConstraints::strict_enforcement` selects whether
818    /// the budget has head-room (`flexibility`) and `violation_penalty` feeds
819    /// the accumulated penalty surfaced by [`Self::budget_penalty`].
820    fn check_budget_constraints(&self, memory_mb: usize, cpu_percent: f64) -> Result<(), String> {
821        let allocations = lock_recovered(&self.allocations);
822
823        // Calculate total allocated resources
824        let total_memory: usize = allocations
825            .values()
826            .map(|a| a.allocated_memory_mb)
827            .sum::<usize>()
828            + memory_mb;
829
830        let total_cpu: f64 = allocations
831            .values()
832            .map(|a| a.allocated_cpu_percent)
833            .sum::<f64>()
834            + cpu_percent;
835
836        let flexibility = 1.0 + self.budget.flexibility;
837        let memory_limit =
838            (self.budget.memory_budget.max_allocation_mb as f64 * flexibility) as usize;
839        let cpu_limit = self.budget.cpu_budget.max_utilization * flexibility;
840
841        // Check constraints
842        if total_memory > memory_limit {
843            self.record_budget_violation();
844            return Err(format!(
845                "Memory allocation would exceed budget: {} MB > {} MB",
846                total_memory, memory_limit
847            ));
848        }
849
850        if total_cpu > cpu_limit {
851            self.record_budget_violation();
852            return Err(format!(
853                "CPU allocation would exceed budget: {:.2}% > {:.2}%",
854                total_cpu, cpu_limit
855            ));
856        }
857
858        Ok(())
859    }
860
861    fn record_budget_violation(&self) {
862        self.budget_violations.fetch_add(1, Ordering::Relaxed);
863    }
864
865    /// Number of budget violations observed so far (R7).
866    pub fn budget_violations(&self) -> u64 {
867        self.budget_violations.load(Ordering::Relaxed)
868    }
869
870    /// Accumulated budget-violation penalty (CF1:
871    /// `ResourceBudgetConstraints::violation_penalty`).
872    pub fn budget_penalty(&self) -> f64 {
873        self.budget_penalty
874    }
875
876    /// Updates resource utilization tracking.
877    ///
878    /// When no monitoring thread is running this collects a sample itself
879    /// (throttled to `monitoring_frequency`). Previously `start_monitoring`
880    /// was never called from anywhere, so `current_usage` stayed at its
881    /// all-zero `Default` forever and every consumer of it read zeros.
882    pub fn update_utilization(&mut self) -> Result<(), String> {
883        if self.monitoring_handle.is_none() {
884            let due = self
885                .last_synchronous_sample
886                .map(|last| last.elapsed() >= self.config.monitoring_frequency)
887                .unwrap_or(true);
888            if due {
889                let usage = self.collect_resource_usage();
890                self.last_synchronous_sample = Some(Instant::now());
891                {
892                    let mut current = lock_recovered(&self.current_usage);
893                    *current = usage.clone();
894                }
895                let mut history = lock_recovered(&self.usage_history);
896                if history.len() >= 1000 {
897                    history.pop_front();
898                }
899                history.push_back(usage);
900            }
901        }
902
903        let current_usage = lock_recovered(&self.current_usage).clone();
904
905        // Raise new alerts and clear the ones that recovered (R5).
906        self.alert_system.update(&current_usage)?;
907
908        // Count real budget violations against the observed usage (R7). The
909        // allocation budget is per-process, so it is compared against this
910        // process's own footprint, never against system-wide usage.
911        let mut violated = false;
912        if let Some(process_mb) = current_usage.process_memory() {
913            if process_mb > self.budget.memory_budget.max_allocation_mb {
914                violated = true;
915            }
916        }
917        if let Some(cpu) = current_usage.cpu_usage() {
918            if cpu > self.budget.cpu_budget.max_utilization {
919                violated = true;
920            }
921        }
922        if violated {
923            self.record_budget_violation();
924            self.budget_penalty += self.config.budget_constraints.violation_penalty;
925        }
926
927        // Update predictor (CF1: gated by `enable_resource_prediction`).
928        self.predictor.update(&current_usage)?;
929
930        // Check for optimization opportunities
931        if self.config.enable_dynamic_allocation {
932            self.optimizer
933                .check_optimization_opportunities(&current_usage, &self.allocations)?;
934        }
935
936        Ok(())
937    }
938
939    /// Checks if sufficient resources are available for processing.
940    ///
941    /// The CPU term is only applied when a real CPU measurement exists; before
942    /// the probe has two refreshes to compare, the placeholder `0.0` must not
943    /// be mistaken for "idle".
944    pub fn has_sufficient_resources_for_processing(&self) -> Result<bool, String> {
945        let current_usage = lock_recovered(&self.current_usage);
946
947        // Check memory availability against this process's own footprint; the
948        // soft limit is a per-process allocation budget.
949        let memory_available = match current_usage.process_memory() {
950            Some(process_mb) => {
951                process_mb < (self.budget.memory_budget.soft_limit_mb as f64 * 0.9) as usize
952            }
953            None => true,
954        };
955
956        // Check CPU availability
957        let cpu_available = match current_usage.cpu_usage() {
958            Some(cpu) => cpu < self.budget.cpu_budget.target_utilization * 0.9,
959            None => true,
960        };
961
962        Ok(memory_available && cpu_available)
963    }
964
965    /// Computes resource allocation adaptation
966    pub fn compute_allocation_adaptation(&mut self) -> Result<Option<Adaptation<f32>>, String> {
967        let current_usage = lock_recovered(&self.current_usage);
968
969        // Check if we need to adapt resource allocation. Again per-process:
970        // shrinking this optimizer's buffers cannot help with memory another
971        // process is holding.
972        let process_memory_mb = current_usage.process_memory();
973        if process_memory_mb
974            .is_some_and(|process_mb| process_mb > self.budget.memory_budget.soft_limit_mb)
975        {
976            // Memory pressure - suggest reducing buffer sizes. The magnitude is
977            // proportional to the real overshoot and clamped by the
978            // optimizer's configured change-rate limit rather than being a
979            // fixed -20%.
980            let overshoot = (process_memory_mb.unwrap_or(0) as f64
981                - self.budget.memory_budget.soft_limit_mb as f64)
982                / (self.budget.memory_budget.soft_limit_mb.max(1) as f64);
983            let magnitude = self.optimizer.clamp_change("memory", -overshoot);
984            let adaptation = Adaptation {
985                adaptation_type: AdaptationType::ResourceAllocation,
986                magnitude: magnitude as f32,
987                target_component: "memory_manager".to_string(),
988                parameters: std::collections::HashMap::new(),
989                priority: AdaptationPriority::High,
990                timestamp: Instant::now(),
991            };
992
993            drop(current_usage);
994            if self.optimizer.accept_change("memory_manager") {
995                return Ok(Some(adaptation));
996            }
997            return Ok(None);
998        }
999
1000        // Only react to a real CPU measurement; the not-yet-measured
1001        // placeholder must not be read as "0% busy".
1002        if let Some(cpu) = current_usage.cpu_usage() {
1003            if cpu > self.budget.cpu_budget.target_utilization {
1004                let overshoot = (cpu - self.budget.cpu_budget.target_utilization)
1005                    / self.budget.cpu_budget.target_utilization.max(1.0);
1006                let magnitude = self.optimizer.clamp_change("cpu", -overshoot);
1007                let adaptation = Adaptation {
1008                    adaptation_type: AdaptationType::ResourceAllocation,
1009                    magnitude: magnitude as f32,
1010                    target_component: "cpu_manager".to_string(),
1011                    parameters: std::collections::HashMap::new(),
1012                    priority: AdaptationPriority::High,
1013                    timestamp: Instant::now(),
1014                };
1015
1016                drop(current_usage);
1017                if self.optimizer.accept_change("cpu_manager") {
1018                    return Ok(Some(adaptation));
1019                }
1020                return Ok(None);
1021            }
1022        }
1023
1024        Ok(None)
1025    }
1026
1027    /// Predicted resource usage `prediction_horizon` samples ahead, or `None`
1028    /// when prediction is disabled or there is not enough history (R7).
1029    pub fn predict_usage(&self) -> Option<ResourceUsage> {
1030        self.predictor.predict()
1031    }
1032
1033    /// Mean absolute percentage error of the predictor, per resource (R7).
1034    pub fn prediction_accuracy(&self) -> &HashMap<String, f64> {
1035        self.predictor.accuracy()
1036    }
1037
1038    /// Current resource trend analysis.
1039    pub fn trend_analysis(&self) -> &ResourceTrendAnalysis {
1040        self.predictor.trend_analysis()
1041    }
1042
1043    /// Registers a handler invoked for every raised alert.
1044    pub fn register_alert_handler(&mut self, handler: Box<dyn AlertHandler>) {
1045        self.alert_system.register_handler(handler);
1046    }
1047
1048    /// Currently unresolved alerts.
1049    pub fn active_alerts(&self) -> Vec<ResourceAlert> {
1050        self.alert_system.active_alerts.iter().cloned().collect()
1051    }
1052
1053    /// Resolved alerts, oldest first.
1054    pub fn alert_history(&self) -> Vec<ResourceAlert> {
1055        self.alert_system.alert_history.iter().cloned().collect()
1056    }
1057
1058    /// Applies resource allocation adaptation
1059    pub fn apply_allocation_adaptation(
1060        &mut self,
1061        adaptation: &Adaptation<f32>,
1062    ) -> Result<(), String> {
1063        if adaptation.adaptation_type == AdaptationType::ResourceAllocation {
1064            match adaptation.target_component.as_str() {
1065                "memory_manager" => {
1066                    // Adjust memory allocations
1067                    let factor = (1.0 + adaptation.magnitude).max(0.0);
1068                    let mut allocations = lock_recovered(&self.allocations);
1069
1070                    for allocation in allocations.values_mut() {
1071                        if allocation.priority >= ResourcePriority::Normal {
1072                            allocation.allocated_memory_mb =
1073                                ((allocation.allocated_memory_mb as f32) * factor) as usize;
1074                        }
1075                    }
1076                    drop(allocations);
1077                    self.optimizer.record_applied_change("memory_manager");
1078                }
1079                "cpu_manager" => {
1080                    // Adjust CPU allocations
1081                    let factor = (1.0 + adaptation.magnitude).max(0.0);
1082                    let mut allocations = lock_recovered(&self.allocations);
1083
1084                    for allocation in allocations.values_mut() {
1085                        if allocation.priority >= ResourcePriority::Normal {
1086                            allocation.allocated_cpu_percent *= factor as f64;
1087                        }
1088                    }
1089                    drop(allocations);
1090                    self.optimizer.record_applied_change("cpu_manager");
1091                }
1092                other => {
1093                    return Err(format!(
1094                        "no resource adaptation is defined for target component '{other}'"
1095                    ));
1096                }
1097            }
1098        }
1099
1100        Ok(())
1101    }
1102
1103    /// Gets current resource usage
1104    pub fn current_usage(&self) -> Result<ResourceUsage, String> {
1105        Ok(lock_recovered(&self.current_usage).clone())
1106    }
1107
1108    /// Gets resource usage history
1109    pub fn get_usage_history(&self, count: usize) -> Vec<ResourceUsage> {
1110        let history = lock_recovered(&self.usage_history);
1111        history.iter().rev().take(count).cloned().collect()
1112    }
1113
1114    /// Gets diagnostic information
1115    pub fn get_diagnostics(&self) -> ResourceDiagnostics {
1116        let current_usage = lock_recovered(&self.current_usage);
1117        let allocations = lock_recovered(&self.allocations);
1118
1119        ResourceDiagnostics {
1120            current_usage: current_usage.clone(),
1121            total_allocations: allocations.len(),
1122            // Per-process footprint against the per-process allocation budget.
1123            memory_utilization: current_usage.process_memory().map(|process_mb| {
1124                (process_mb as f64 / self.budget.memory_budget.max_allocation_mb.max(1) as f64)
1125                    * 100.0
1126            }),
1127            // System-wide pressure, which is what the memory alerts watch.
1128            system_memory_percent: current_usage.memory_usage_percent(),
1129            cpu_utilization: current_usage.cpu_usage(),
1130            active_alerts: self.alert_system.active_alerts.len(),
1131            // R7: a real count, not the hardcoded zero it used to be.
1132            budget_violations: self.budget_violations.load(Ordering::Relaxed) as usize,
1133            budget_penalty: self.budget_penalty,
1134            // Accumulated magnitude of the resource changes actually applied
1135            // per component. The optimizer tracked this from the start but
1136            // nothing surfaced it, so callers had no way to see whether the
1137            // optimization engine was doing anything at all.
1138            applied_change_magnitude: self.optimizer.performance_impact().clone(),
1139        }
1140    }
1141}
1142
1143impl Drop for ResourceManager {
1144    /// R4: joins the monitoring thread so it cannot outlive the manager.
1145    fn drop(&mut self) {
1146        self.shutdown.store(true, Ordering::SeqCst);
1147        if let Some(handle) = self.monitoring_handle.take() {
1148            let _ = handle.join();
1149        }
1150    }
1151}
1152
1153/// Diagnostic information for resource management
1154#[derive(Debug, Clone)]
1155pub struct ResourceDiagnostics {
1156    pub current_usage: ResourceUsage,
1157    pub total_allocations: usize,
1158    /// This process's footprint as a percentage of its allocation budget, or
1159    /// `None` when the process counters are unavailable.
1160    pub memory_utilization: Option<f64>,
1161    /// System-wide memory pressure as a percentage, or `None` when the system
1162    /// total is unknown.
1163    pub system_memory_percent: Option<f64>,
1164    /// Real CPU utilization, or `None` when the probe has not produced a
1165    /// measurement yet (R1).
1166    pub cpu_utilization: Option<f64>,
1167    pub active_alerts: usize,
1168    /// Real budget-violation count (R7).
1169    pub budget_violations: usize,
1170    /// Accumulated budget-violation penalty.
1171    pub budget_penalty: f64,
1172    /// Total absolute resource change applied per component by the
1173    /// optimization engine, keyed by component name.
1174    pub applied_change_magnitude: HashMap<String, f64>,
1175}
1176
1177impl Default for ResourceUsage {
1178    fn default() -> Self {
1179        Self {
1180            memory_usage_mb: 0,
1181            total_memory_mb: 0,
1182            process_memory_mb: None,
1183            cpu_usage_percent: 0.0,
1184            cpu_usage_percent_valid: false,
1185            gpu_usage_percent: None,
1186            network_io_mbps: None,
1187            disk_io_mbps: None,
1188            active_threads: 0,
1189            timestamp: Instant::now(),
1190        }
1191    }
1192}
1193
1194#[cfg(test)]
1195mod r2_memory_percent_tests {
1196    use super::*;
1197
1198    fn usage_with(memory_usage_mb: usize, total_memory_mb: usize) -> ResourceUsage {
1199        ResourceUsage {
1200            memory_usage_mb,
1201            total_memory_mb,
1202            ..Default::default()
1203        }
1204    }
1205
1206    /// R2: on a realistic machine (far more than 1 GB total memory), a
1207    /// moderate absolute memory usage must NOT be reported as a >100%
1208    /// (and therefore permanently `Emergency`-severity) utilization. The
1209    /// previous `(memory_usage_mb / 1024.0) * 100.0` assumed exactly 1 GB
1210    /// of total system memory.
1211    #[test]
1212    fn memory_percent_is_correct_on_realistic_machine() {
1213        // 4 GB used out of 32 GB total: a real, moderate 12.5% utilization.
1214        let usage = usage_with(4096, 32768);
1215        let percent = usage
1216            .memory_usage_percent()
1217            .expect("total_memory_mb is set, so this must be Some");
1218        assert!(
1219            (percent - 12.5).abs() < 1e-9,
1220            "R2 regression: expected ~12.5%, got {percent}%"
1221        );
1222        assert!(
1223            percent < 100.0,
1224            "R2 regression: realistic usage reported as over 100% (got {percent}%), \
1225             which pins alert severity at Emergency regardless of real pressure"
1226        );
1227    }
1228
1229    /// R2: `memory_usage_percent` must return `None` (not a fabricated
1230    /// value) when the real total is unknown, so callers can skip the
1231    /// check instead of manufacturing a false alert.
1232    #[test]
1233    fn memory_percent_is_none_when_total_unknown() {
1234        let usage = usage_with(4096, 0);
1235        assert_eq!(usage.memory_usage_percent(), None);
1236    }
1237
1238    /// R2 (via `ResourceAlertSystem::check_thresholds`): a moderate,
1239    /// realistic memory load on a large machine must not raise a memory
1240    /// alert at all, since it is nowhere near any real threshold. Before
1241    /// the fix, this scenario would compute `(4096 / 1024.0) * 100.0 =
1242    /// 400%`, which is above every threshold including `emergency: 95.0`.
1243    #[test]
1244    fn realistic_memory_load_raises_no_alert() {
1245        let mut alert_system = ResourceAlertSystem::new();
1246        let usage = usage_with(4096, 32768); // 12.5%, far below `warning: 70.0`
1247        let alerts = alert_system
1248            .check_thresholds(&usage)
1249            .expect("check_thresholds");
1250        assert!(
1251            alerts.is_empty(),
1252            "R2 regression: realistic 12.5% memory usage raised alert(s): {alerts:?}"
1253        );
1254    }
1255
1256    /// R2: a genuinely high memory load (relative to the real total) must
1257    /// still raise an alert — the fix must not make the detector blind to
1258    /// real pressure, only correct about what "high" means.
1259    #[test]
1260    fn genuinely_high_memory_load_raises_alert() {
1261        let mut alert_system = ResourceAlertSystem::new();
1262        let usage = usage_with(31000, 32768); // ~94.6%, above `critical: 85.0`
1263        let alerts = alert_system
1264            .check_thresholds(&usage)
1265            .expect("check_thresholds");
1266        assert!(
1267            !alerts.is_empty(),
1268            "genuinely high memory usage (~94.6%) should raise an alert"
1269        );
1270        // The exact band depends on the configured `cleanup_threshold` (R8:
1271        // the thresholds are derived from configuration now, not hardcoded), so
1272        // assert on the severity floor rather than one specific level.
1273        assert!(alerts
1274            .iter()
1275            .any(|a| a.resource_type == "memory" && a.severity >= AlertSeverity::Critical));
1276    }
1277}