Skip to main content

tacet_core/
timer.rs

1//! Platform timer frequency detection.
2//!
3//! Provides automatic detection of system timer frequencies for ARM64 and x86_64.
4//! This module is no-std compatible for basic register reading, with std-dependent
5//! calibration routines available when the `std` feature is enabled.
6
7#[cfg(target_arch = "aarch64")]
8use core::arch::asm;
9
10#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
11use core::arch::asm;
12
13// =============================================================================
14// ARM64 (aarch64) Generic Timer
15// =============================================================================
16
17/// Get ARM64 counter frequency in Hz.
18///
19/// Reads the CNTFRQ_EL0 register and validates the result. Falls back to
20/// platform detection or calibration if the register value is suspicious.
21///
22/// # Platform Notes
23/// - Apple Silicon M1/M2: 24 MHz
24/// - Apple Silicon M3+ (macOS 15+): 1 GHz (kernel-scaled)
25/// - AWS Graviton: 1 GHz
26/// - Raspberry Pi 4: 54 MHz
27///
28/// # Returns
29/// Counter frequency in Hz, or a reasonable fallback if detection fails.
30#[cfg(target_arch = "aarch64")]
31pub fn get_aarch64_counter_freq_hz() -> u64 {
32    // One-time initialization with validation (not thread-local)
33    #[cfg(feature = "std")]
34    {
35        use std::sync::OnceLock;
36        static VALIDATED_FREQ: OnceLock<u64> = OnceLock::new();
37
38        *VALIDATED_FREQ.get_or_init(|| {
39            // Read counter frequency from CNTFRQ_EL0 register
40            let cntfrq: u64;
41            unsafe {
42                asm!("mrs {}, cntfrq_el0", out(reg) cntfrq);
43            }
44
45            // Quick validation: is the frequency value reasonable?
46            if !is_reasonable_aarch64_freq(cntfrq) {
47                eprintln!(
48                    "[tacet-core] WARNING: CNTFRQ_EL0 returned suspicious value: {} Hz",
49                    cntfrq
50                );
51                eprintln!("[tacet-core] Calibrating ARM64 counter frequency...");
52                return calibrate_aarch64_frequency();
53            }
54
55            // Sanity check: does CNTFRQ_EL0 match runtime calibration?
56            // This catches virtualization issues where CNTFRQ_EL0 is incorrectly programmed
57            let calibrated = calibrate_aarch64_frequency();
58            let ratio = calibrated as f64 / cntfrq as f64;
59
60            // Allow 10% tolerance for measurement noise
61            if !(0.9..=1.1).contains(&ratio) {
62                eprintln!(
63                    "[tacet-core] WARNING: CNTFRQ_EL0 ({} Hz / {:.2} MHz) differs from calibrated frequency ({} Hz / {:.2} MHz) by {:.1}%",
64                    cntfrq, cntfrq as f64 / 1e6,
65                    calibrated, calibrated as f64 / 1e6,
66                    (ratio - 1.0) * 100.0
67                );
68                eprintln!(
69                    "[tacet-core] This typically indicates virtualization (VM/CI) with misconfigured timers."
70                );
71                eprintln!(
72                    "[tacet-core] Using calibrated frequency: {} Hz ({:.2} MHz)",
73                    calibrated, calibrated as f64 / 1e6
74                );
75                return calibrated;
76            }
77
78            // CNTFRQ_EL0 matches calibration - trust it
79            cntfrq
80        })
81    }
82
83    // No-std fallback: trust CNTFRQ_EL0 (can't calibrate without std::time)
84    #[cfg(not(feature = "std"))]
85    {
86        let freq: u64;
87        unsafe {
88            asm!("mrs {}, cntfrq_el0", out(reg) freq);
89        }
90
91        if is_reasonable_aarch64_freq(freq) {
92            freq
93        } else {
94            24_000_000 // Assume 24MHz (Apple Silicon M1/M2)
95        }
96    }
97}
98
99/// Check if an ARM64 counter frequency is reasonable (1 MHz to 10 GHz).
100#[cfg(target_arch = "aarch64")]
101#[inline]
102fn is_reasonable_aarch64_freq(freq: u64) -> bool {
103    (1_000_000..=10_000_000_000).contains(&freq)
104}
105
106/// Try to detect ARM64 platform from /proc/cpuinfo and return known frequency.
107///
108/// This is only used as a fallback when CNTFRQ_EL0 returns an unreasonable value.
109#[cfg(all(feature = "std", target_arch = "aarch64", target_os = "linux"))]
110fn detect_aarch64_platform_freq() -> Option<u64> {
111    let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok()?;
112    let cpuinfo_lower = cpuinfo.to_lowercase();
113
114    // AWS Graviton (Neoverse cores) - common default
115    if cpuinfo_lower.contains("neoverse") || cpuinfo_lower.contains("graviton") {
116        return Some(1_000_000_000); // 1 GHz
117    }
118
119    // Raspberry Pi 4 (Cortex-A72)
120    if cpuinfo_lower.contains("raspberry pi 4") || cpuinfo_lower.contains("bcm2711") {
121        return Some(54_000_000); // 54 MHz
122    }
123
124    None
125}
126
127/// Calibrate ARM64 counter frequency against std::time::Instant.
128///
129/// Takes multiple samples and returns the median for robustness.
130#[cfg(all(feature = "std", target_arch = "aarch64"))]
131#[allow(dead_code)]
132fn calibrate_aarch64_frequency() -> u64 {
133    use std::time::{Duration, Instant};
134
135    const SAMPLES: usize = 5;
136    const SLEEP_MS: u64 = 20;
137
138    let mut frequencies = Vec::with_capacity(SAMPLES);
139
140    for _ in 0..SAMPLES {
141        let start_cnt: u64;
142        unsafe {
143            asm!("mrs {}, cntvct_el0", out(reg) start_cnt);
144        }
145        let start_instant = Instant::now();
146
147        std::thread::sleep(Duration::from_millis(SLEEP_MS));
148
149        let end_cnt: u64;
150        unsafe {
151            asm!("mrs {}, cntvct_el0", out(reg) end_cnt);
152        }
153        let elapsed_ns = start_instant.elapsed().as_nanos() as u64;
154
155        let cnt_delta = end_cnt.wrapping_sub(start_cnt);
156        let freq = ((cnt_delta as u128 * 1_000_000_000) / elapsed_ns as u128) as u64;
157
158        if is_reasonable_aarch64_freq(freq) {
159            frequencies.push(freq);
160        }
161    }
162
163    if frequencies.is_empty() {
164        eprintln!("[tacet-core] WARNING: ARM64 calibration failed. Using 24MHz estimate.");
165        return 24_000_000;
166    }
167
168    // Use median for robustness
169    frequencies.sort_unstable();
170    let median = frequencies[frequencies.len() / 2];
171
172    eprintln!(
173        "[tacet-core] ARM64 counter frequency calibrated to {:.2} MHz",
174        median as f64 / 1_000_000.0
175    );
176
177    median
178}
179
180// =============================================================================
181// x86_64 RDTSC
182// =============================================================================
183
184/// Get x86_64 TSC frequency in Hz.
185///
186/// Tries multiple methods in priority order:
187/// 1. Linux: /sys/devices/system/cpu/cpu0/tsc_freq_khz (most reliable)
188/// 2. Linux: CPUID leaf 0x16 base frequency (Skylake+, if invariant TSC)
189/// 3. macOS: sysctl machdep.tsc.frequency
190/// 4. Fallback: calibration against std::time::Instant
191///
192/// # Returns
193/// TSC frequency in Hz, or a reasonable estimate if detection fails.
194#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
195pub fn get_x86_64_tsc_freq_hz() -> u64 {
196    // One-time initialization (not thread-local)
197    #[cfg(feature = "std")]
198    {
199        use std::sync::OnceLock;
200        static VALIDATED_FREQ: OnceLock<u64> = OnceLock::new();
201
202        *VALIDATED_FREQ.get_or_init(|| {
203            // Try platform-specific reliable sources first
204            #[cfg(target_os = "linux")]
205            if let Some(freq) = get_tsc_freq_linux() {
206                return freq;
207            }
208
209            #[cfg(target_os = "macos")]
210            if let Some(freq) = get_tsc_freq_macos() {
211                return freq;
212            }
213
214            // Fallback: runtime calibration
215            calibrate_tsc_frequency()
216        })
217    }
218
219    // No-std fallback: assume 3GHz (common Intel base frequency)
220    #[cfg(not(feature = "std"))]
221    {
222        3_000_000_000
223    }
224}
225
226/// Linux: Try to get TSC frequency from sysfs or CPUID.
227#[cfg(all(feature = "std", target_arch = "x86_64", target_os = "linux"))]
228fn get_tsc_freq_linux() -> Option<u64> {
229    // Method 1: sysfs tsc_freq_khz (most reliable when available)
230    if let Ok(content) = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/tsc_freq_khz") {
231        if let Ok(khz) = content.trim().parse::<u64>() {
232            let freq = khz * 1000;
233            if is_reasonable_tsc_freq(freq) {
234                return Some(freq);
235            }
236        }
237    }
238
239    // Method 2: CPUID leaf 0x16 - Processor Frequency Information (Skylake+)
240    if has_invariant_tsc() {
241        if let Some(freq) = get_cpuid_base_freq() {
242            if is_reasonable_tsc_freq(freq) {
243                return Some(freq);
244            }
245        }
246    }
247
248    None
249}
250
251/// Check if CPU has invariant TSC (constant rate regardless of frequency scaling).
252#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
253fn has_invariant_tsc() -> bool {
254    // Check CPUID.80000007H:EDX[8] - Invariant TSC
255    let result: u32;
256    unsafe {
257        asm!(
258            "push rbx",
259            "mov eax, 0x80000007",
260            "cpuid",
261            "pop rbx",
262            out("edx") result,
263            out("eax") _,
264            out("ecx") _,
265            options(nostack)
266        );
267    }
268    (result & (1 << 8)) != 0
269}
270
271/// Get processor base frequency from CPUID leaf 0x16 (Skylake+).
272#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
273fn get_cpuid_base_freq() -> Option<u64> {
274    // First check if leaf 0x16 is supported
275    let max_leaf: u32;
276    unsafe {
277        asm!(
278            "push rbx",
279            "mov eax, 0",
280            "cpuid",
281            "pop rbx",
282            out("eax") max_leaf,
283            out("ecx") _,
284            out("edx") _,
285            options(nostack)
286        );
287    }
288
289    if max_leaf < 0x16 {
290        return None;
291    }
292
293    // CPUID leaf 0x16: Processor Frequency Information
294    // EAX = Base frequency in MHz
295    let base_mhz: u32;
296    unsafe {
297        asm!(
298            "push rbx",
299            "mov eax, 0x16",
300            "cpuid",
301            "pop rbx",
302            out("eax") base_mhz,
303            out("ecx") _,
304            out("edx") _,
305            options(nostack)
306        );
307    }
308
309    if base_mhz == 0 {
310        return None;
311    }
312
313    Some(base_mhz as u64 * 1_000_000)
314}
315
316/// macOS: Try to get TSC frequency from sysctl.
317#[cfg(all(feature = "std", target_arch = "x86_64", target_os = "macos"))]
318fn get_tsc_freq_macos() -> Option<u64> {
319    // Try machdep.tsc.frequency first (most accurate)
320    if let Some(freq) = sysctl_read_u64("machdep.tsc.frequency") {
321        if is_reasonable_tsc_freq(freq) {
322            return Some(freq);
323        }
324    }
325
326    // Fallback to hw.cpufrequency (base frequency, usually matches TSC)
327    if let Some(freq) = sysctl_read_u64("hw.cpufrequency") {
328        if is_reasonable_tsc_freq(freq) {
329            return Some(freq);
330        }
331    }
332
333    None
334}
335
336/// Read a u64 value from sysctl on macOS.
337#[cfg(all(feature = "std", target_arch = "x86_64", target_os = "macos"))]
338fn sysctl_read_u64(name: &str) -> Option<u64> {
339    use std::process::Command;
340
341    let output = Command::new("sysctl").arg("-n").arg(name).output().ok()?;
342
343    if !output.status.success() {
344        return None;
345    }
346
347    let stdout = String::from_utf8_lossy(&output.stdout);
348    stdout.trim().parse::<u64>().ok()
349}
350
351/// Check if a TSC frequency is reasonable (500 MHz to 10 GHz).
352#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
353#[inline]
354fn is_reasonable_tsc_freq(freq: u64) -> bool {
355    (500_000_000..=10_000_000_000).contains(&freq)
356}
357
358/// Calibrate TSC frequency by measuring against std::time::Instant.
359#[cfg(all(
360    feature = "std",
361    target_arch = "x86_64",
362    any(target_os = "linux", target_os = "macos")
363))]
364fn calibrate_tsc_frequency() -> u64 {
365    use std::time::{Duration, Instant};
366
367    eprintln!("[tacet-core] Calibrating TSC frequency (no sysfs/sysctl available)...");
368
369    const SAMPLES: usize = 5;
370    const SLEEP_MS: u64 = 20;
371
372    let mut frequencies = Vec::with_capacity(SAMPLES);
373
374    for _ in 0..SAMPLES {
375        let start_tsc = rdtsc();
376        let start_instant = Instant::now();
377
378        std::thread::sleep(Duration::from_millis(SLEEP_MS));
379
380        let end_tsc = rdtsc();
381        let elapsed_ns = start_instant.elapsed().as_nanos() as u64;
382
383        let tsc_delta = end_tsc.wrapping_sub(start_tsc);
384        let freq = ((tsc_delta as u128 * 1_000_000_000) / elapsed_ns as u128) as u64;
385
386        if is_reasonable_tsc_freq(freq) {
387            frequencies.push(freq);
388        }
389    }
390
391    if frequencies.is_empty() {
392        eprintln!("[tacet-core] WARNING: TSC calibration failed. Using 3GHz estimate.");
393        return 3_000_000_000;
394    }
395
396    // Use median for robustness
397    frequencies.sort_unstable();
398    let median = frequencies[frequencies.len() / 2];
399
400    eprintln!(
401        "[tacet-core] TSC frequency calibrated to {:.2} GHz",
402        median as f64 / 1_000_000_000.0
403    );
404
405    median
406}
407
408/// Read x86_64 TSC (Time Stamp Counter).
409#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
410#[inline]
411fn rdtsc() -> u64 {
412    let lo: u32;
413    let hi: u32;
414    unsafe {
415        asm!(
416            "rdtsc",
417            out("eax") lo,
418            out("edx") hi,
419            options(nostack, nomem)
420        );
421    }
422    ((hi as u64) << 32) | (lo as u64)
423}
424
425// =============================================================================
426// Public API
427// =============================================================================
428
429/// Automatically detect the system timer frequency in Hz.
430///
431/// This function uses platform-specific detection:
432/// - **ARM64**: Reads CNTFRQ_EL0 with validation and fallbacks
433/// - **x86_64**: Reads TSC frequency from sysfs/CPUID with calibration fallback
434///
435/// # Returns
436/// - Timer frequency in Hz for platforms with cycle counters
437/// - 0 for platforms without cycle counters (fallback timer)
438///
439/// # Examples
440/// ```
441/// let freq = tacet_core::timer::counter_frequency_hz();
442/// println!("Timer frequency: {} Hz ({:.2} MHz)", freq, freq as f64 / 1e6);
443/// ```
444pub fn counter_frequency_hz() -> u64 {
445    #[cfg(target_arch = "aarch64")]
446    {
447        get_aarch64_counter_freq_hz()
448    }
449
450    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
451    {
452        get_x86_64_tsc_freq_hz()
453    }
454
455    #[cfg(not(any(
456        target_arch = "aarch64",
457        all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos"))
458    )))]
459    {
460        0 // Fallback platforms have no meaningful counter frequency
461    }
462}
463
464/// Returns the timer resolution in nanoseconds.
465///
466/// This is the theoretical minimum delay that can be measured,
467/// calculated as 1e9 / frequency.
468///
469/// # Returns
470/// - Resolution in nanoseconds (e.g., 0.33 for 3GHz TSC, 42 for 24MHz Apple Silicon)
471/// - f64::INFINITY for fallback platforms without cycle counters
472///
473/// # Examples
474/// ```
475/// let resolution = tacet_core::timer::timer_resolution_ns();
476/// println!("Timer resolution: {:.2} ns", resolution);
477/// ```
478pub fn timer_resolution_ns() -> f64 {
479    let freq = counter_frequency_hz();
480    if freq == 0 {
481        f64::INFINITY
482    } else {
483        1_000_000_000.0 / freq as f64
484    }
485}