openentropy_core/sources/frontier/
nvme_latency.rs1use std::io::{Read, Seek, SeekFrom, Write};
15use std::time::Instant;
16
17use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
18use crate::sources::helpers::extract_timing_entropy;
19
20const N_OFFSETS: usize = 8;
22
23const BLOCK_SIZE: usize = 4096;
25
26static NVME_LATENCY_INFO: SourceInfo = SourceInfo {
27 name: "nvme_latency",
28 description: "NVMe I/O stack timing jitter from storage controller scheduling",
29 physics: "Reads a file at multiple offsets with OS buffer cache bypassed (F_NOCACHE). \
30 Each read traverses: filesystem metadata lookup \u{2192} NVMe command queue \
31 submission \u{2192} SSD controller DRAM cache \u{2192} completion interrupt. \
32 Timing jitter arises from NVMe command queue arbitration, SSD controller \
33 firmware scheduling (garbage collection, wear leveling background tasks), \
34 and interrupt delivery latency. Note: freshly-written data typically resides \
35 in SSD DRAM cache, not NAND cells.",
36 category: SourceCategory::IO,
37 platform: Platform::Any,
38 requirements: &[],
39 entropy_rate_estimate: 1000.0,
40 composite: false,
41};
42
43pub struct NVMeLatencySource;
45
46impl EntropySource for NVMeLatencySource {
47 fn info(&self) -> &SourceInfo {
48 &NVME_LATENCY_INFO
49 }
50
51 fn is_available(&self) -> bool {
52 true
53 }
54
55 fn collect(&self, n_samples: usize) -> Vec<u8> {
56 let mut tmpfile = match tempfile::NamedTempFile::new() {
58 Ok(f) => f,
59 Err(_) => return Vec::new(),
60 };
61
62 let total_size = BLOCK_SIZE * N_OFFSETS;
63 let mut fill = vec![0u8; total_size];
64 let mut lcg: u64 = 0xDEAD_BEEF_CAFE_1234;
65 for chunk in fill.chunks_mut(8) {
66 lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
67 let bytes = lcg.to_le_bytes();
68 for (i, b) in chunk.iter_mut().enumerate() {
69 *b = bytes[i % 8];
70 }
71 }
72 if tmpfile.write_all(&fill).is_err() {
73 return Vec::new();
74 }
75 if tmpfile.flush().is_err() {
76 return Vec::new();
77 }
78
79 #[cfg(target_os = "macos")]
81 {
82 use std::os::unix::io::AsRawFd;
83 unsafe {
86 libc::fcntl(tmpfile.as_raw_fd(), libc::F_NOCACHE, 1);
87 }
88 }
89
90 let raw_count = n_samples * 4 + 64;
91 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
92 let mut read_buf = vec![0u8; BLOCK_SIZE];
93
94 for i in 0..raw_count {
95 let offset = (i % N_OFFSETS) as u64 * BLOCK_SIZE as u64;
96 if tmpfile.seek(SeekFrom::Start(offset)).is_err() {
97 continue;
98 }
99 let t0 = Instant::now();
100 let _ = tmpfile.read(&mut read_buf);
101 let elapsed = t0.elapsed();
102 timings.push(elapsed.as_nanos() as u64);
103 }
104
105 extract_timing_entropy(&timings, n_samples)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn info() {
115 let src = NVMeLatencySource;
116 assert_eq!(src.name(), "nvme_latency");
117 assert_eq!(src.info().category, SourceCategory::IO);
118 assert!(!src.info().composite);
119 }
120
121 #[test]
122 #[ignore] fn collects_bytes() {
124 let src = NVMeLatencySource;
125 assert!(src.is_available());
126 let data = src.collect(64);
127 assert!(!data.is_empty());
128 assert!(data.len() <= 64);
129 }
130}