openentropy_core/sources/scheduling/
timer_coalescing.rs1use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
36use crate::sources::helpers::extract_timing_entropy;
37
38static TIMER_COALESCING_INFO: SourceInfo = SourceInfo {
39 name: "timer_coalescing",
40 description: "OS timer coalescing wakeup jitter from system-wide timer queue state",
41 physics: "Calls nanosleep(1ns) and measures the actual wakeup latency. The OS \
42 batches timer wakeups across all processes; actual wakeup time depends on \
43 pending timers from every process, daemon, and kernel subsystem on the \
44 machine. Produces bimodal distribution (~3\u{00b5}s / ~13\u{00b5}s clusters on macOS) \
45 with CV >70%. Intra-cluster jitter encodes the phase of the hardware timer \
46 interrupt relative to our wakeup request — a system-wide aggregate noise \
47 source with no per-process equivalent.",
48 category: SourceCategory::Scheduling,
49 platform: Platform::Any,
50 requirements: &[],
51 entropy_rate_estimate: 2.0,
52 composite: false,
53 is_fast: false,
54};
55
56pub struct TimerCoalescingSource;
62
63impl EntropySource for TimerCoalescingSource {
64 fn info(&self) -> &SourceInfo {
65 &TIMER_COALESCING_INFO
66 }
67
68 fn is_available(&self) -> bool {
69 cfg!(unix)
71 }
72
73 fn collect(&self, n_samples: usize) -> Vec<u8> {
74 #[cfg(unix)]
75 {
76 collect_unix(n_samples)
77 }
78 #[cfg(not(unix))]
79 {
80 let _ = n_samples;
81 Vec::new()
82 }
83 }
84}
85
86#[cfg(unix)]
87fn collect_unix(n_samples: usize) -> Vec<u8> {
88 use std::time::Instant;
89
90 let raw_count = n_samples * 12 + 64;
92 let mut timings = Vec::with_capacity(raw_count);
93
94 let warmup_req = libc_timespec(0, 1);
96 for _ in 0..32 {
97 let mut rem = libc_timespec(0, 0);
98 unsafe { libc::nanosleep(&warmup_req, &mut rem) };
100 }
101
102 for _ in 0..raw_count {
103 let req = libc_timespec(0, 1); let mut rem = libc_timespec(0, 0);
105
106 let t0 = Instant::now();
107 unsafe { libc::nanosleep(&req, &mut rem) };
109 let elapsed_ns = t0.elapsed().as_nanos() as u64;
110
111 if elapsed_ns < 500_000_000 {
113 timings.push(elapsed_ns);
114 }
115 }
116
117 extract_timing_entropy(&timings, n_samples)
118}
119
120#[cfg(unix)]
121#[inline]
122fn libc_timespec(secs: i64, nsecs: i64) -> libc::timespec {
123 libc::timespec {
124 tv_sec: secs as libc::time_t,
125 tv_nsec: nsecs as libc::c_long,
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn info() {
135 let src = TimerCoalescingSource;
136 assert_eq!(src.info().name, "timer_coalescing");
137 assert!(matches!(src.info().category, SourceCategory::Scheduling));
138 assert_eq!(src.info().platform, Platform::Any);
139 assert!(!src.info().composite);
140 }
141
142 #[test]
143 #[cfg(unix)]
144 fn is_available_on_unix() {
145 assert!(TimerCoalescingSource.is_available());
146 }
147
148 #[test]
149 #[ignore] fn collects_bytes_with_variation() {
151 let src = TimerCoalescingSource;
152 if !src.is_available() {
153 return;
154 }
155 let data = src.collect(32);
156 assert!(!data.is_empty(), "expected non-empty output");
157 let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
158 assert!(
159 unique.len() > 2,
160 "expected byte variation from coalescing jitter"
161 );
162 }
163}