1use std::{
52 fs,
53 hint::black_box,
54 path::{Path, PathBuf},
55 thread,
56 time::{Duration, Instant},
57};
58
59use crypto::{
60 Hasher,
61 blake3::Blake3,
62 sha2::{Sha256, Sha512},
63 sha3::Sha3_256,
64};
65
66const CHUNK_SIZE: usize = 1024 * 1024; const BENCH_SECS: u64 = 10;
68const COOLDOWN_SECS: u64 = 30;
69const IDLE_SECS: u64 = 5;
70
71struct RaplReader {
72 paths: Vec<PathBuf>,
73 names: Vec<String>,
74}
75
76impl RaplReader {
77 fn detect() -> Result<Self, String> {
78 let powercap = Path::new("/sys/class/powercap");
79 if !powercap.exists() {
80 return Err("/sys/class/powercap not found. RAPL is not available on this system.".into());
81 }
82
83 let mut paths = Vec::new();
84 let mut names = Vec::new();
85 let entries = fs::read_dir(powercap).map_err(|e| format!("cannot read /sys/class/powercap: {e}"))?;
86
87 for entry in entries {
88 let entry = entry.map_err(|e| format!("error reading powercap entry: {e}"))?;
89 let fname = entry.file_name().to_string_lossy().to_string();
90
91 if fname.starts_with("intel-rapl:") && fname.matches(':').count() == 1 {
92 let energy_path = entry.path().join("energy_uj");
93 if energy_path.exists() {
94 let name = fs::read_to_string(entry.path().join("name"))
95 .unwrap_or_default()
96 .trim()
97 .to_string();
98 names.push(name);
99 paths.push(energy_path);
100 }
101 }
102 }
103
104 if paths.is_empty() {
105 return Err("no intel-rapl energy counters found in /sys/class/powercap.\n\
106 RAPL is available on Intel CPUs (Sandy Bridge+) and AMD Zen CPUs (kernel 5.4+)."
107 .into());
108 }
109
110 Ok(Self {
111 paths,
112 names,
113 })
114 }
115
116 fn read_energy_uj(&self) -> Result<u64, String> {
117 let mut total: u64 = 0;
118 for path in &self.paths {
119 let s = fs::read_to_string(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
120 let val: u64 = s
121 .trim()
122 .parse()
123 .map_err(|e| format!("cannot parse {}: {e}", path.display()))?;
124 total += val;
125 }
126 Ok(total)
127 }
128}
129
130struct Measurement {
131 algo: &'static str,
132 total_bytes: u64,
133 energy_j: f64,
134 wall_secs: f64,
135}
136
137fn measure_hash<H: Hasher>(
138 rapl: &RaplReader,
139 data: &[u8],
140 duration: Duration,
141 algo: &'static str,
142) -> Result<Measurement, String> {
143 let chunk_bytes = data.len() as u64;
144 let mut iterations: u64 = 0;
145
146 let energy_before = rapl.read_energy_uj()?;
147 let wall_start = Instant::now();
148
149 while wall_start.elapsed() < duration {
150 let h = H::hash(black_box(data));
151 black_box(h);
152 iterations += 1;
153 }
154
155 let wall_elapsed = wall_start.elapsed();
156 let energy_after = rapl.read_energy_uj()?;
157 let total_bytes = chunk_bytes * iterations;
158
159 let energy_uj = if energy_after >= energy_before {
160 energy_after - energy_before
161 } else {
162 u64::MAX - energy_before + energy_after + 1
163 };
164
165 Ok(Measurement {
166 algo,
167 total_bytes,
168 energy_j: energy_uj as f64 / 1_000_000.0,
169 wall_secs: wall_elapsed.as_secs_f64(),
170 })
171}
172
173fn measure_idle(rapl: &RaplReader, secs: u64) -> Result<(f64, f64), String> {
174 let before = rapl.read_energy_uj()?;
175 let start = Instant::now();
176 while start.elapsed().as_secs() < secs {
177 std::hint::spin_loop();
178 }
179 let after = rapl.read_energy_uj()?;
180 let elapsed = start.elapsed().as_secs_f64();
181
182 let energy_uj = if after >= before {
183 after - before
184 } else {
185 u64::MAX - before + after + 1
186 };
187
188 Ok((energy_uj as f64 / 1_000_000.0, elapsed))
189}
190
191fn commafy(n: u64) -> String {
192 let s = n.to_string();
193 let mut result = String::new();
194 for (i, c) in s.chars().rev().enumerate() {
195 if i > 0 && i % 3 == 0 {
196 result.push(',');
197 }
198 result.push(c);
199 }
200 result.chars().rev().collect()
201}
202
203fn main() {
204 let rapl = match RaplReader::detect() {
205 Ok(r) => r,
206 Err(e) => {
207 eprintln!("error: {e}");
208 std::process::exit(1);
209 }
210 };
211
212 let governor = fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
213 .unwrap_or_default()
214 .trim()
215 .to_string();
216
217 if governor != "performance" {
218 eprintln!(
219 "warning: CPU governor is '{governor}', not 'performance'.\n\
220 run 'make run' or set governor manually for stable measurements.\n"
221 );
222 }
223
224 for (n, p) in rapl.names.iter().zip(rapl.paths.iter()) {
225 eprintln!("found RAPL domain: {} ({})", n, p.display());
226 }
227
228 let data = vec![0xA5u8; CHUNK_SIZE];
229 let bench_duration = Duration::from_secs(BENCH_SECS);
230
231 eprintln!("\nmeasuring idle baseline ({IDLE_SECS}s)...");
232 let (idle_j, idle_secs) = measure_idle(&rapl, IDLE_SECS).unwrap_or_else(|e| {
233 eprintln!("warning: idle measurement failed: {e}");
234 (0.0, IDLE_SECS as f64)
235 });
236 let idle_w = idle_j / idle_secs;
237
238 eprintln!("\nbenchmarking SHA-256 ({BENCH_SECS}s)...");
239 let sha = measure_hash::<Sha256>(&rapl, &data, bench_duration, "SHA-256").unwrap_or_else(|e| {
240 eprintln!("error: {e}");
241 std::process::exit(1);
242 });
243
244 eprintln!("cooling down ({COOLDOWN_SECS}s)...");
245 thread::sleep(Duration::from_secs(COOLDOWN_SECS));
246
247 eprintln!("\nbenchmarking BLAKE3 ({BENCH_SECS}s)...");
248 let blake3 = measure_hash::<Blake3>(&rapl, &data, bench_duration, "BLAKE3").unwrap_or_else(|e| {
249 eprintln!("error: {e}");
250 std::process::exit(1);
251 });
252
253 eprintln!("cooling down ({COOLDOWN_SECS}s)...");
254 thread::sleep(Duration::from_secs(COOLDOWN_SECS));
255
256 eprintln!("\nbenchmarking SHA-512 ({BENCH_SECS}s)...");
257 let sha512 = measure_hash::<Sha512>(&rapl, &data, bench_duration, "SHA-512").unwrap_or_else(|e| {
258 eprintln!("error: {e}");
259 std::process::exit(1);
260 });
261
262 eprintln!("cooling down ({COOLDOWN_SECS}s)...");
263 thread::sleep(Duration::from_secs(COOLDOWN_SECS));
264
265 eprintln!("\nbenchmarking SHA3-256 ({BENCH_SECS}s)...");
266 let sha3 = measure_hash::<Sha3_256>(&rapl, &data, bench_duration, "SHA3-256").unwrap_or_else(|e| {
267 eprintln!("error: {e}");
268 std::process::exit(1);
269 });
270
271 println!();
272 println!(
273 "idle power: {:.1} W | governor: {} | bench duration: {}s",
274 idle_w, governor, BENCH_SECS,
275 );
276 println!();
277
278 let mut results = vec![&sha, &sha512, &sha3, &blake3];
279 results.sort_by(|a, b| {
280 let a_ratio = a.total_bytes as f64 / a.energy_j;
281 let b_ratio = b.total_bytes as f64 / b.energy_j;
282 b_ratio.partial_cmp(&a_ratio).unwrap()
283 });
284
285 println!(
286 "{:<12} {:>16} {:>12} {:>16} {:>16} {:>12}",
287 "Algorithm", "Bytes Hashed", "Energy (J)", "bytes/J", "J/byte", "Thruput"
288 );
289 println!("{:-<12} {:-<16} {:-<12} {:-<16} {:-<16} {:-<12}", "", "", "", "", "", "");
290
291 for m in &results {
292 let b_per_j = m.total_bytes as f64 / m.energy_j;
293 let j_per_b = m.energy_j / m.total_bytes as f64;
294 let thr_mb_s = m.total_bytes as f64 / m.wall_secs / 1_048_576.0;
295
296 println!(
297 "{:<12} {:>16} {:>11.1} J {:>16} {:>15.9e} {:>11.1} MB/s",
298 m.algo,
299 commafy(m.total_bytes),
300 m.energy_j,
301 commafy(b_per_j as u64),
302 j_per_b,
303 thr_mb_s,
304 );
305 }
306}