/home/noah/src/trueno/src/brick/profiling.rs
Line | Count | Source |
1 | | //! High-Performance Profiling Patterns |
2 | | //! |
3 | | //! CPU cycle counters, cached time service, and page fault detection. |
4 | | //! Based on Phase 11: E.9 patterns. |
5 | | |
6 | | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
7 | | use std::time::Instant; |
8 | | |
9 | | // ============================================================================ |
10 | | // CPU Cycle Counters |
11 | | // ============================================================================ |
12 | | |
13 | | /// CPU cycle counter using RDTSCP (x86_64) or CNTVCT_EL0 (ARM64). |
14 | | /// |
15 | | /// Returns actual CPU cycles for frequency-invariant performance analysis. |
16 | | /// Use with `elapsed_ns` to calculate IPC (Instructions Per Cycle). |
17 | | /// |
18 | | /// # Example |
19 | | /// ```rust,ignore |
20 | | /// let start_cycles = cpu_cycles(); |
21 | | /// // ... operation ... |
22 | | /// let end_cycles = cpu_cycles(); |
23 | | /// let cycles_per_element = (end_cycles - start_cycles) / num_elements; |
24 | | /// ``` |
25 | | #[cfg(target_arch = "x86_64")] |
26 | | #[inline] |
27 | 0 | pub fn cpu_cycles() -> u64 { |
28 | | unsafe { |
29 | 0 | let mut _aux: u32 = 0; |
30 | 0 | core::arch::x86_64::__rdtscp(&mut _aux) |
31 | | } |
32 | 0 | } |
33 | | |
34 | | /// CPU cycle counter for ARM64 using CNTVCT_EL0 register. |
35 | | #[cfg(target_arch = "aarch64")] |
36 | | #[inline] |
37 | | pub fn cpu_cycles() -> u64 { |
38 | | let cycles: u64; |
39 | | unsafe { |
40 | | core::arch::asm!("mrs {}, cntvct_el0", out(reg) cycles); |
41 | | } |
42 | | cycles |
43 | | } |
44 | | |
45 | | /// Fallback for unsupported architectures (returns 0). |
46 | | #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] |
47 | | #[inline] |
48 | | pub fn cpu_cycles() -> u64 { |
49 | | 0 |
50 | | } |
51 | | |
52 | | // ============================================================================ |
53 | | // Cached Time Service (Pattern 2 from actix-web) |
54 | | // ============================================================================ |
55 | | |
56 | | /// Global cached instant in nanoseconds, updated by background thread. |
57 | | static CACHED_NANOS: AtomicU64 = AtomicU64::new(0); |
58 | | |
59 | | /// Epoch instant for cached time calculation. |
60 | | static EPOCH: std::sync::OnceLock<Instant> = std::sync::OnceLock::new(); |
61 | | |
62 | | /// Flag to track if time service is initialized. |
63 | | static TIME_SERVICE_INIT: AtomicBool = AtomicBool::new(false); |
64 | | |
65 | | /// Initialize the cached time service (call once at startup). |
66 | | /// |
67 | | /// Spawns a background thread that updates cached time every 100µs. |
68 | | /// This avoids syscall overhead when profiling high-frequency operations. |
69 | | /// |
70 | | /// # Example |
71 | | /// ```rust,ignore |
72 | | /// trueno::brick::init_time_service(); |
73 | | /// // Later... |
74 | | /// let ns = trueno::brick::cached_nanos(); |
75 | | /// ``` |
76 | 0 | pub fn init_time_service() { |
77 | 0 | if TIME_SERVICE_INIT.swap(true, Ordering::SeqCst) { |
78 | 0 | return; // Already initialized |
79 | 0 | } |
80 | | |
81 | 0 | let epoch = *EPOCH.get_or_init(Instant::now); |
82 | 0 | CACHED_NANOS.store(0, Ordering::Relaxed); |
83 | | |
84 | 0 | std::thread::Builder::new() |
85 | 0 | .name("trueno-time-service".into()) |
86 | 0 | .spawn(move || loop { |
87 | 0 | std::thread::sleep(std::time::Duration::from_micros(100)); // 100µs precision |
88 | 0 | let elapsed = epoch.elapsed().as_nanos() as u64; |
89 | 0 | CACHED_NANOS.store(elapsed, Ordering::Relaxed); |
90 | 0 | }) |
91 | 0 | .expect("Failed to spawn time service thread"); |
92 | 0 | } |
93 | | |
94 | | /// Get cached time in nanoseconds since epoch (NO SYSCALL, ~1ns overhead). |
95 | | /// |
96 | | /// Returns 0 if time service is not initialized. For accurate timing, |
97 | | /// call `init_time_service()` at application startup. |
98 | | #[inline] |
99 | 0 | pub fn cached_nanos() -> u64 { |
100 | 0 | CACHED_NANOS.load(Ordering::Relaxed) |
101 | 0 | } |
102 | | |
103 | | /// Get cached time or fall back to Instant::now() if service not initialized. |
104 | | #[inline] |
105 | 0 | pub fn cached_nanos_or_now() -> u64 { |
106 | 0 | let cached = CACHED_NANOS.load(Ordering::Relaxed); |
107 | 0 | if cached == 0 && !TIME_SERVICE_INIT.load(Ordering::Relaxed) { |
108 | | // Fall back to syscall if time service not initialized |
109 | 0 | EPOCH |
110 | 0 | .get_or_init(Instant::now) |
111 | 0 | .elapsed() |
112 | 0 | .as_nanos() as u64 |
113 | | } else { |
114 | 0 | cached |
115 | | } |
116 | 0 | } |
117 | | |
118 | | // ============================================================================ |
119 | | // Page Fault Detection (Pattern from B4 Investigation) |
120 | | // ============================================================================ |
121 | | |
122 | | /// Get current minor and major page fault counts (Linux only). |
123 | | /// |
124 | | /// Returns (minor_faults, major_faults). |
125 | | /// - Minor faults: Page in memory but not mapped (soft fault) |
126 | | /// - Major faults: Page on disk, requires I/O (hard fault) |
127 | | #[cfg(target_os = "linux")] |
128 | 0 | pub fn get_page_faults() -> (u64, u64) { |
129 | | use std::fs; |
130 | 0 | let stat = fs::read_to_string("/proc/self/stat").unwrap_or_default(); |
131 | 0 | let fields: Vec<&str> = stat.split_whitespace().collect(); |
132 | 0 | if fields.len() > 12 { |
133 | 0 | let minor = fields[9].parse().unwrap_or(0); |
134 | 0 | let major = fields[11].parse().unwrap_or(0); |
135 | 0 | (minor, major) |
136 | | } else { |
137 | 0 | (0, 0) |
138 | | } |
139 | 0 | } |
140 | | |
141 | | /// Fallback for non-Linux platforms. |
142 | | #[cfg(not(target_os = "linux"))] |
143 | | pub fn get_page_faults() -> (u64, u64) { |
144 | | (0, 0) |
145 | | } |
146 | | |
147 | | /// Execute a closure while tracking page faults. |
148 | | /// |
149 | | /// Logs a warning if more than 1000 minor faults or any major faults occur. |
150 | | /// |
151 | | /// # Example |
152 | | /// ```rust,ignore |
153 | | /// let result = with_page_fault_tracking("mmap_copy", || { |
154 | | /// data.copy_from_slice(&mmap_region); |
155 | | /// }); |
156 | | /// ``` |
157 | 0 | pub fn with_page_fault_tracking<T, F: FnOnce() -> T>(name: &str, f: F) -> (T, u64, u64) { |
158 | 0 | let (minor_before, major_before) = get_page_faults(); |
159 | 0 | let result = f(); |
160 | 0 | let (minor_after, major_after) = get_page_faults(); |
161 | | |
162 | 0 | let minor_delta = minor_after.saturating_sub(minor_before); |
163 | 0 | let major_delta = major_after.saturating_sub(major_before); |
164 | | |
165 | | #[cfg(feature = "tracing")] |
166 | | if minor_delta > 1000 || major_delta > 0 { |
167 | | tracing::warn!( |
168 | | operation = name, |
169 | | minor_faults = minor_delta, |
170 | | major_faults = major_delta, |
171 | | "High page fault count detected" |
172 | | ); |
173 | | } |
174 | | |
175 | 0 | let _ = name; // Suppress unused warning when tracing disabled |
176 | 0 | (result, minor_delta, major_delta) |
177 | 0 | } |
178 | | |
179 | | #[cfg(test)] |
180 | | mod tests { |
181 | | use super::*; |
182 | | |
183 | | #[test] |
184 | | fn test_cpu_cycles_returns_value() { |
185 | | let cycles = cpu_cycles(); |
186 | | // On x86_64/aarch64, should return non-zero |
187 | | // On other architectures, returns 0 |
188 | | #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] |
189 | | assert!(cycles > 0); |
190 | | #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] |
191 | | assert_eq!(cycles, 0); |
192 | | } |
193 | | |
194 | | #[test] |
195 | | fn test_cached_nanos_or_now_returns_value() { |
196 | | let nanos = cached_nanos_or_now(); |
197 | | // Should return a valid nanosecond count (non-zero on most systems) |
198 | | // Note: u64 is always >= 0, so just verify we got a value |
199 | | let _ = nanos; // Value is always valid for u64 |
200 | | } |
201 | | |
202 | | #[test] |
203 | | fn test_page_fault_tracking() { |
204 | | let (result, minor, major) = with_page_fault_tracking("test", || 42); |
205 | | assert_eq!(result, 42); |
206 | | // Page faults are u64, always non-negative by type |
207 | | let _ = (minor, major); // Values are always valid for u64 |
208 | | } |
209 | | |
210 | | #[test] |
211 | | fn test_get_page_faults() { |
212 | | let (minor, major) = get_page_faults(); |
213 | | // Page faults are u64, always non-negative by type (may be 0 on non-Linux) |
214 | | let _ = (minor, major); // Values are always valid for u64 |
215 | | } |
216 | | } |