system_profile/
lib.rs

1//! System Profile Crate
2//!
3//! Provides cached system information and resource profiles for the entire application.
4//! All values are computed once on first access and cached for the program lifetime.
5//!
6//! Uses std::sync::LazyLock (Rust 1.80+) for lazy initialization.
7
8use std::sync::{Arc, LazyLock};
9
10/// Global system profile instance - computed once, cached forever
11pub static SYSTEM: LazyLock<Arc<SystemProfile>> =
12    LazyLock::new(|| Arc::new(SystemProfile::detect()));
13
14/// System profile containing hardware and resource information
15#[derive(Debug, Clone)]
16pub struct SystemProfile {
17    /// Total CPU cores (including hyperthreading)
18    pub cpu_count: usize,
19
20    /// Physical CPU cores (excluding hyperthreading)
21    pub physical_cpu_count: usize,
22
23    /// Total system memory in bytes
24    pub total_memory: u64,
25
26    /// Available system memory in bytes at startup
27    pub available_memory: u64,
28
29    /// Operating system name
30    pub os_name: String,
31
32    /// Operating system version
33    pub os_version: String,
34
35    /// System hostname
36    pub hostname: String,
37
38    /// Is this a Mac?
39    pub is_macos: bool,
40
41    /// Is this Windows?
42    pub is_windows: bool,
43
44    /// Is this Linux?
45    pub is_linux: bool,
46
47    /// Recommended worker count for I/O-bound tasks
48    pub recommended_io_workers: usize,
49
50    /// Recommended worker count for CPU-bound tasks
51    pub recommended_cpu_workers: usize,
52}
53
54impl SystemProfile {
55    /// Detect system profile (called once via LazyLock)
56    fn detect() -> Self {
57        use sysinfo::System;
58
59        let cpu_count = num_cpus::get();
60        let physical_cpu_count = num_cpus::get_physical();
61
62        // Get system information
63        let mut sys = System::new_with_specifics(
64            sysinfo::RefreshKind::new().with_memory(sysinfo::MemoryRefreshKind::everything()),
65        );
66        sys.refresh_memory();
67
68        let total_memory = sys.total_memory();
69        let available_memory = sys.available_memory();
70
71        // Get OS information
72        let os_name = System::name().unwrap_or_else(|| "Unknown".to_string());
73        let os_version = System::os_version().unwrap_or_else(|| "Unknown".to_string());
74        let hostname = System::host_name().unwrap_or_else(|| "Unknown".to_string());
75
76        // Detect OS type
77        let is_macos = cfg!(target_os = "macos");
78        let is_windows = cfg!(target_os = "windows");
79        let is_linux = cfg!(target_os = "linux");
80
81        // Calculate recommended worker counts
82        // I/O-bound: can use more threads than cores (2x is common for file I/O)
83        let recommended_io_workers = cpu_count * 2;
84
85        // CPU-bound: typically matches physical cores
86        let recommended_cpu_workers = physical_cpu_count;
87
88        Self {
89            cpu_count,
90            physical_cpu_count,
91            total_memory,
92            available_memory,
93            os_name,
94            os_version,
95            hostname,
96            is_macos,
97            is_windows,
98            is_linux,
99            recommended_io_workers,
100            recommended_cpu_workers,
101        }
102    }
103
104    /// Get the global system profile instance
105    pub fn get() -> Arc<SystemProfile> {
106        SYSTEM.clone()
107    }
108
109    /// Get optimal worker count based on percentage of available CPUs
110    pub fn calculate_workers(&self, percentage: usize) -> usize {
111        let percentage = percentage.min(100) as f32 / 100.0;
112        ((self.cpu_count as f32 * percentage).ceil() as usize).max(1)
113    }
114
115    /// Get worker count with a maximum limit
116    pub fn calculate_workers_with_limit(&self, percentage: usize, max_threads: usize) -> usize {
117        if max_threads > 0 {
118            self.calculate_workers(percentage).min(max_threads)
119        } else {
120            self.calculate_workers(percentage)
121        }
122    }
123
124    /// Adapt worker count based on workload size (for file processing tasks)
125    pub fn adapt_workers_for_workload(&self, item_count: usize, max_workers: usize) -> usize {
126        match item_count {
127            0..=10 => 1.min(max_workers),             // Minimal parallelism
128            11..=50 => (max_workers / 2).max(1),      // Conservative parallelism
129            51..=100 => (max_workers * 3 / 4).max(1), // Moderate parallelism
130            _ => max_workers,                         // Full parallelism
131        }
132    }
133
134    /// Check if system has sufficient resources for parallel processing
135    pub fn should_use_parallel(&self, min_memory_mb: u64) -> bool {
136        self.cpu_count > 1 && self.available_memory > (min_memory_mb * 1024 * 1024)
137    }
138
139    /// Get a human-readable summary of system resources
140    pub fn summary(&self) -> String {
141        format!(
142            "System: {} {} on {}\nCPUs: {} ({} physical)\nMemory: {:.2} GB ({:.2} GB \
143             available)\nHost: {}",
144            self.os_name,
145            self.os_version,
146            if self.is_macos {
147                "macOS"
148            } else if self.is_windows {
149                "Windows"
150            } else if self.is_linux {
151                "Linux"
152            } else {
153                "Other"
154            },
155            self.cpu_count,
156            self.physical_cpu_count,
157            self.total_memory as f64 / (1024.0 * 1024.0 * 1024.0),
158            self.available_memory as f64 / (1024.0 * 1024.0 * 1024.0),
159            self.hostname
160        )
161    }
162
163    /// Get memory in GB
164    pub fn total_memory_gb(&self) -> f64 {
165        self.total_memory as f64 / (1024.0 * 1024.0 * 1024.0)
166    }
167
168    /// Get available memory in GB
169    pub fn available_memory_gb(&self) -> f64 {
170        self.available_memory as f64 / (1024.0 * 1024.0 * 1024.0)
171    }
172}
173
174/// Quick access functions
175impl SystemProfile {
176    /// Get CPU count directly
177    pub fn cpu_count() -> usize {
178        SYSTEM.cpu_count
179    }
180
181    /// Get physical CPU count directly
182    pub fn physical_cpu_count() -> usize {
183        SYSTEM.physical_cpu_count
184    }
185
186    /// Check if running on a multi-core system
187    pub fn is_multicore() -> bool {
188        SYSTEM.cpu_count > 1
189    }
190
191    /// Check OS type quickly
192    pub fn is_macos() -> bool {
193        SYSTEM.is_macos
194    }
195
196    pub fn is_windows() -> bool {
197        SYSTEM.is_windows
198    }
199
200    pub fn is_linux() -> bool {
201        SYSTEM.is_linux
202    }
203}
204
205#[cfg(feature = "gpu")]
206pub mod gpu {
207    /// GPU information (if available)
208    #[derive(Debug, Clone)]
209    pub struct GpuInfo {
210        pub name: String,
211        pub memory_mb: u64,
212        pub cuda_cores: Option<u32>,
213    }
214
215    /// Detect NVIDIA GPUs (requires 'gpu' feature)
216    pub fn detect_nvidia_gpus() -> Vec<GpuInfo> {
217        // TODO: Implement using nvml-wrapper
218        vec![]
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_system_profile_initialization() {
228        let profile = SystemProfile::get();
229        assert!(profile.cpu_count > 0);
230        assert!(profile.physical_cpu_count > 0);
231        assert!(profile.total_memory > 0);
232
233        // Test OS detection - at least one should be true
234        assert!(profile.is_macos || profile.is_windows || profile.is_linux);
235    }
236
237    #[test]
238    fn test_worker_calculation() {
239        let profile = SystemProfile::get();
240
241        // Test 50% workers
242        let half_workers = profile.calculate_workers(50);
243        assert!(half_workers >= 1);
244        assert!(half_workers <= profile.cpu_count);
245
246        // Test 100% workers
247        let full_workers = profile.calculate_workers(100);
248        assert_eq!(full_workers, profile.cpu_count);
249
250        // Test with limit
251        let limited = profile.calculate_workers_with_limit(100, 4);
252        assert!(limited <= 4);
253    }
254
255    #[test]
256    fn test_workload_adaptation() {
257        let profile = SystemProfile::get();
258        let max_workers = 8;
259
260        // Small workload should use minimal workers
261        assert_eq!(profile.adapt_workers_for_workload(5, max_workers), 1);
262
263        // Medium workload should use conservative workers
264        let medium = profile.adapt_workers_for_workload(30, max_workers);
265        assert!(medium <= max_workers / 2);
266
267        // Large workload should use all workers
268        assert_eq!(
269            profile.adapt_workers_for_workload(200, max_workers),
270            max_workers
271        );
272    }
273
274    #[test]
275    fn test_static_access() {
276        // Ensure multiple accesses return the same instance
277        let profile1 = SystemProfile::get();
278        let profile2 = SystemProfile::get();
279        assert_eq!(profile1.cpu_count, profile2.cpu_count);
280
281        // Test direct access methods
282        assert_eq!(SystemProfile::cpu_count(), profile1.cpu_count);
283        assert_eq!(
284            SystemProfile::physical_cpu_count(),
285            profile1.physical_cpu_count
286        );
287    }
288
289    #[test]
290    fn test_summary() {
291        let profile = SystemProfile::get();
292        let summary = profile.summary();
293
294        // Summary should contain key information
295        assert!(summary.contains("CPUs:"));
296        assert!(summary.contains("Memory:"));
297        assert!(summary.contains("System:"));
298    }
299}