1use std::fs::{self, File};
2use std::os::unix::process::CommandExt;
3use std::process::{Command, Stdio};
4use std::time::{Duration, Instant};
5
6use anyhow::{bail, Result};
7use clap::ValueEnum;
8
9use super::child_guard::ChildGuard;
10use super::report::{format_delta, format_latency_delta, mean_stdev, percentile, save_report};
11use super::{binary_path, is_scx_active, self_exe, wait_for_activation, LOG_DIR, TARGET_DIR};
12
13#[derive(Clone, ValueEnum)]
14pub enum BenchMode {
15 #[value(name = "self")]
17 SelfBuild,
18 Contention,
20 Mixed,
22 Cmd,
24}
25
26pub fn run_bench_run(
28 mode: BenchMode,
29 cmd: Option<&str>,
30 iterations: usize,
31 clean_cmd: Option<&str>,
32 sched_args: &[String],
33) -> Result<()> {
34 fs::create_dir_all(LOG_DIR)?;
35
36 let build_out = File::create(format!("{}/build.out", LOG_DIR))?;
37 let build_err = File::create(format!("{}/build.err", LOG_DIR))?;
38 let status = Command::new("cargo")
39 .args(["build", "--release"])
40 .env("CARGO_TARGET_DIR", TARGET_DIR)
41 .stdout(Stdio::from(build_out))
42 .stderr(Stdio::from(build_err))
43 .status()?;
44 if !status.success() {
45 bail!("BUILD FAILED. SEE {}/build.err", LOG_DIR);
46 }
47
48 let extra_args: Vec<String> = sched_args.to_vec();
49
50 let bench_out = File::create(format!("{}/bench.out", LOG_DIR))?;
51 let bench_err = File::create(format!("{}/bench.err", LOG_DIR))?;
52 let mut bench_cmd = Command::new(binary_path());
53 let mode_name = mode
54 .to_possible_value()
55 .ok_or_else(|| anyhow::anyhow!("INVALID BENCH MODE"))?
56 .get_name()
57 .to_string();
58 bench_cmd
59 .arg("bench")
60 .arg("--mode")
61 .arg(mode_name)
62 .arg("--iterations")
63 .arg(iterations.to_string());
64 if let Some(c) = cmd {
65 bench_cmd.arg("--cmd").arg(c);
66 }
67 if let Some(cc) = clean_cmd {
68 bench_cmd.arg("--clean-cmd").arg(cc);
69 }
70 if !extra_args.is_empty() {
71 bench_cmd.arg("--").args(extra_args);
72 }
73
74 let status = bench_cmd
75 .stdout(Stdio::from(bench_out))
76 .stderr(Stdio::from(bench_err))
77 .status()?;
78 if !status.success() {
79 bail!("BENCH FAILED. SEE {}/bench.err", LOG_DIR);
80 }
81
82 log_info!("Build logs: {}/build.out {}/build.err", LOG_DIR, LOG_DIR);
83 log_info!("Bench logs: {}/bench.out {}/bench.err", LOG_DIR, LOG_DIR);
84 Ok(())
85}
86
87fn timed_run(cmd: &str) -> Option<f64> {
88 log_info!("Running: {}", cmd);
89 let start = Instant::now();
90 let result = Command::new("sh")
91 .args(["-c", cmd])
92 .stdout(Stdio::null())
93 .stderr(Stdio::piped())
94 .output();
95 let elapsed = start.elapsed().as_secs_f64();
96 match result {
97 Ok(r) if r.status.success() => {
98 log_info!("Completed in {:.2}s", elapsed);
99 Some(elapsed)
100 }
101 Ok(r) => {
102 let stderr = String::from_utf8_lossy(&r.stderr);
103 log_error!(
104 "Command failed (exit {}): {}",
105 r.status.code().unwrap_or(-1),
106 &stderr[..stderr.len().min(500)]
107 );
108 None
109 }
110 Err(e) => {
111 log_error!("Command failed: {}", e);
112 None
113 }
114 }
115}
116
117fn start_scheduler(sched_args: &[String]) -> Result<ChildGuard> {
118 let bin = binary_path();
119 let mut args = Vec::new();
120 args.extend(sched_args.iter().cloned());
121
122 let child = Command::new("sudo")
123 .arg(&bin)
124 .args(&args)
125 .process_group(0)
126 .stdout(Stdio::piped())
127 .stderr(Stdio::piped())
128 .spawn()?;
129 Ok(ChildGuard::new(child))
130}
131
132fn stop_scheduler(guard: &mut ChildGuard) {
133 guard.stop();
134}
135
136fn ensure_scheduler_started(sched_args: &[String]) -> Result<ChildGuard> {
137 let guard = start_scheduler(sched_args)?;
138 if !wait_for_activation(10) {
139 bail!("PANDEMONIUM DID NOT ACTIVATE WITHIN 10S");
140 }
141 log_info!("PANDEMONIUM is active");
142 std::thread::sleep(Duration::from_secs(2));
143 Ok(guard)
144}
145
146pub fn run_bench(
147 mode: BenchMode,
148 cmd: Option<&str>,
149 iterations: usize,
150 clean_cmd: Option<&str>,
151 sched_args: &[String],
152) -> Result<()> {
153 match mode {
154 BenchMode::SelfBuild => bench_general(
155 &format!("CARGO_TARGET_DIR={} cargo build --release", TARGET_DIR),
156 iterations,
157 Some(&format!("cargo clean --target-dir {}", TARGET_DIR)),
158 sched_args,
159 ),
160 BenchMode::Cmd => {
161 let cmd = cmd.ok_or_else(|| anyhow::anyhow!("--cmd required for --mode cmd"))?;
162 bench_general(cmd, iterations, clean_cmd, sched_args)
163 }
164 BenchMode::Mixed => bench_mixed(sched_args),
165 BenchMode::Contention => bench_contention(sched_args),
166 }
167}
168
169fn bench_general(
171 cmd: &str,
172 iterations: usize,
173 clean_cmd: Option<&str>,
174 sched_args: &[String],
175) -> Result<()> {
176 let sep = "=".repeat(60);
177 log_info!("PANDEMONIUM A/B benchmark");
178 log_info!("Command: {}", cmd);
179 log_info!("Iterations: {}", iterations);
180 if let Some(cc) = clean_cmd {
181 log_info!("Clean cmd: {}", cc);
182 }
183
184 if is_scx_active() {
185 bail!("SCHED_EXT IS ALREADY ACTIVE. STOP IT BEFORE BENCHMARKING.");
186 }
187
188 log_info!("Phase 1: EEVDF baseline");
190 let mut eevdf_times = Vec::new();
191 for i in 0..iterations {
192 log_info!("Iteration {}/{}", i + 1, iterations);
193 if let Some(cc) = clean_cmd {
194 let _ = Command::new("sh").args(["-c", cc]).output();
195 }
196 match timed_run(cmd) {
197 Some(t) => eevdf_times.push(t),
198 None => bail!("ABORTING BENCHMARK: COMMAND FAILED"),
199 }
200 }
201
202 log_info!("Phase 2: starting PANDEMONIUM");
204 let mut pand_proc = ensure_scheduler_started(sched_args)?;
205
206 log_info!("Phase 3: PANDEMONIUM benchmark");
208 let mut pand_times = Vec::new();
209 for i in 0..iterations {
210 log_info!("Iteration {}/{}", i + 1, iterations);
211 if let Some(cc) = clean_cmd {
212 let _ = Command::new("sh").args(["-c", cc]).output();
213 }
214 match timed_run(cmd) {
215 Some(t) => pand_times.push(t),
216 None => {
217 stop_scheduler(&mut pand_proc);
218 bail!("ABORTING BENCHMARK: COMMAND FAILED");
219 }
220 }
221 }
222
223 log_info!("Phase 4: stopping PANDEMONIUM");
225 stop_scheduler(&mut pand_proc);
226 log_info!("PANDEMONIUM stopped");
227
228 let (eevdf_mean, eevdf_std) = mean_stdev(&eevdf_times);
230 let (pand_mean, pand_std) = mean_stdev(&pand_times);
231 let delta_pct = if eevdf_mean > 0.0 {
232 ((pand_mean - eevdf_mean) / eevdf_mean) * 100.0
233 } else {
234 0.0
235 };
236
237 let mut report = Vec::new();
238 report.push(sep.clone());
239 report.push("BENCHMARK RESULTS".to_string());
240 report.push(sep.clone());
241 report.push(format!("COMMAND: {}", cmd));
242 report.push(format!("ITERATIONS: {}", iterations));
243 report.push(String::new());
244 report.push(format!(
245 "EEVDF: {:.2}s +/- {:.2}s",
246 eevdf_mean, eevdf_std
247 ));
248 report.push(format!(
249 " RUNS: {}",
250 eevdf_times
251 .iter()
252 .map(|t| format!("{:.2}s", t))
253 .collect::<Vec<_>>()
254 .join(", ")
255 ));
256 report.push(format!(
257 "PANDEMONIUM: {:.2}s +/- {:.2}s",
258 pand_mean, pand_std
259 ));
260 report.push(format!(
261 " RUNS: {}",
262 pand_times
263 .iter()
264 .map(|t| format!("{:.2}s", t))
265 .collect::<Vec<_>>()
266 .join(", ")
267 ));
268 report.push(String::new());
269 report.push(format_delta(delta_pct, "BUILD"));
270 report.push(sep.clone());
271
272 let report_text = report.join("\n") + "\n";
273 for line in &report {
274 println!("{}", line);
275 }
276
277 let path = save_report(&report_text, "benchmark")?;
278 println!("\nSAVED TO {}", path);
279 Ok(())
280}
281
282fn pw_top_snapshot() -> Vec<(String, i64)> {
284 let mut child = match Command::new("pw-top")
285 .arg("-b")
286 .stdout(Stdio::piped())
287 .stderr(Stdio::null())
288 .spawn()
289 {
290 Ok(c) => c,
291 Err(_) => return Vec::new(),
292 };
293
294 std::thread::sleep(Duration::from_millis(1500));
295 let _ = child.kill();
296 let output = match child.wait_with_output() {
297 Ok(o) => o,
298 Err(_) => return Vec::new(),
299 };
300
301 let stdout = String::from_utf8_lossy(&output.stdout);
302 let mut entries = Vec::new();
303 for line in stdout.lines() {
304 let line = line.trim();
305 if line.is_empty() || line.starts_with("S ") {
306 continue;
307 }
308 let parts: Vec<&str> = line.split_whitespace().collect();
309 if parts.len() < 10 {
310 continue;
311 }
312 if !matches!(parts[0], "R" | "S" | "C") {
313 continue;
314 }
315 if let Ok(err) = parts[8].parse::<i64>() {
316 let name = parts[9..]
317 .join(" ")
318 .trim_start_matches(['+', ' '])
319 .to_string();
320 entries.push((name, err));
321 }
322 }
323 entries
324}
325
326fn pw_audio_playing() -> bool {
327 Command::new("pactl")
328 .args(["list", "sink-inputs", "short"])
329 .output()
330 .map(|o| o.status.success() && !o.stdout.is_empty())
331 .unwrap_or(false)
332}
333
334fn pw_get_xruns() -> i64 {
335 pw_top_snapshot().iter().map(|(_, err)| err).sum()
336}
337
338fn bench_mixed(sched_args: &[String]) -> Result<()> {
340 let sep = "=".repeat(60);
341 log_info!("PANDEMONIUM mixed workload benchmark");
342
343 if !pw_audio_playing() {
344 bail!("NO AUDIO PLAYING. START AUDIO PLAYBACK FIRST.");
345 }
346
347 let entries = pw_top_snapshot();
348 log_info!("Active PipeWire nodes:");
349 for (name, err) in &entries {
350 log_info!(" {} (xruns: {})", name, err);
351 }
352
353 if is_scx_active() {
354 bail!("SCHED_EXT IS ALREADY ACTIVE. STOP IT BEFORE BENCHMARKING.");
355 }
356
357 let build_cmd = format!("CARGO_TARGET_DIR={} cargo build --release", TARGET_DIR);
358 let clean_cmd = format!("cargo clean --target-dir {}", TARGET_DIR);
359
360 let sched_args = sched_args.to_vec();
361
362 log_info!("Phase 1: EEVDF (default scheduler)");
364 let _ = Command::new("sh").args(["-c", &clean_cmd]).output();
365 let xruns_before = pw_get_xruns();
366 log_info!("Xruns before: {}", xruns_before);
367 let eevdf_time = timed_run(&build_cmd).ok_or_else(|| anyhow::anyhow!("BUILD FAILED"))?;
368 let xruns_after = pw_get_xruns();
369 let eevdf_xruns = xruns_after - xruns_before;
370 log_info!("Xruns after: {} (delta: {})", xruns_after, eevdf_xruns);
371
372 log_info!("Phase 2: starting PANDEMONIUM");
374 let mut pand_proc = ensure_scheduler_started(&sched_args)?;
375
376 log_info!("Phase 3: PANDEMONIUM");
378 let _ = Command::new("sh").args(["-c", &clean_cmd]).output();
379 let xruns_before = pw_get_xruns();
380 log_info!("Xruns before: {}", xruns_before);
381 let pand_time = match timed_run(&build_cmd) {
382 Some(t) => t,
383 None => {
384 stop_scheduler(&mut pand_proc);
385 bail!("BUILD FAILED");
386 }
387 };
388 let xruns_after = pw_get_xruns();
389 let pand_xruns = xruns_after - xruns_before;
390 log_info!("Xruns after: {} (delta: {})", xruns_after, pand_xruns);
391
392 log_info!("Phase 4: stopping PANDEMONIUM");
394 stop_scheduler(&mut pand_proc);
395 log_info!("PANDEMONIUM stopped");
396
397 let delta_pct = if eevdf_time > 0.0 {
399 ((pand_time - eevdf_time) / eevdf_time) * 100.0
400 } else {
401 0.0
402 };
403 let xrun_delta = pand_xruns - eevdf_xruns;
404
405 let mut report = Vec::new();
406 report.push(sep.clone());
407 report.push("MIXED WORKLOAD BENCHMARK RESULTS".to_string());
408 report.push(sep.clone());
409 report.push("WORKLOAD: CARGO BUILD --RELEASE + AUDIO PLAYBACK".to_string());
410 report.push(String::new());
411 report.push(format!(
412 "{:<16} {:>12} {:>12}",
413 "SCHEDULER", "BUILD TIME", "AUDIO XRUNS"
414 ));
415 report.push(format!(
416 "{} {} {}",
417 "-".repeat(16),
418 "-".repeat(12),
419 "-".repeat(12)
420 ));
421 report.push(format!(
422 "{:<16} {:>11.2}s {:>12}",
423 "EEVDF", eevdf_time, eevdf_xruns
424 ));
425 report.push(format!(
426 "{:<16} {:>11.2}s {:>12}",
427 "PANDEMONIUM", pand_time, pand_xruns
428 ));
429 report.push(String::new());
430 report.push(format_delta(delta_pct, "BUILD"));
431 if xrun_delta < 0 {
432 report.push(format!(
433 "XRUN DELTA: {:+} (PANDEMONIUM HAS FEWER AUDIO GLITCHES)",
434 xrun_delta
435 ));
436 } else if xrun_delta > 0 {
437 report.push(format!(
438 "XRUN DELTA: {:+} (PANDEMONIUM HAS MORE AUDIO GLITCHES)",
439 xrun_delta
440 ));
441 } else {
442 report.push("XRUN DELTA: 0 (SAME AUDIO QUALITY)".to_string());
443 }
444 report.push(sep.clone());
445
446 let report_text = report.join("\n") + "\n";
447 for line in &report {
448 println!("{}", line);
449 }
450
451 let path = save_report(&report_text, "mixed")?;
452 println!("\nSAVED TO {}", path);
453 Ok(())
454}
455
456fn bench_contention(sched_args: &[String]) -> Result<()> {
458 let sep = "=".repeat(60);
459 log_info!("PANDEMONIUM contention benchmark");
460 log_info!("Workload: cargo build --release + interactive probe (10ms sleep/wake)");
461
462 if is_scx_active() {
463 bail!("SCHED_EXT IS ALREADY ACTIVE. STOP IT BEFORE BENCHMARKING.");
464 }
465
466 let build_cmd = format!("CARGO_TARGET_DIR={} cargo build --release", TARGET_DIR);
467 let clean_cmd = format!("cargo clean --target-dir {}", TARGET_DIR);
468
469 std::fs::create_dir_all(super::LOG_DIR)?;
472 let probe_exe = format!("{}/probe", super::LOG_DIR);
473 std::fs::copy(self_exe(), &probe_exe)?;
474
475 let sched_args = sched_args.to_vec();
476
477 struct PhaseResult {
478 name: String,
479 build_time: f64,
480 samples: usize,
481 median: f64,
482 p99: f64,
483 worst: f64,
484 }
485
486 let phases: Vec<(&str, bool)> = vec![("EEVDF (DEFAULT)", false), ("PANDEMONIUM", true)];
487
488 let mut results = Vec::new();
489
490 for (phase_name, use_scheduler) in &phases {
491 log_info!("Phase: {}", phase_name);
492
493 let mut pand_proc = if *use_scheduler {
494 Some(ensure_scheduler_started(&sched_args)?)
495 } else {
496 None
497 };
498
499 let _ = Command::new("sh").args(["-c", &clean_cmd]).output();
501
502 let (death_read, death_write) = super::death_pipe::create_death_pipe()
504 .map_err(|e| anyhow::anyhow!("DEATH PIPE: {}", e))?;
505 let death_write_copy = death_write;
506 let probe_proc = unsafe {
507 Command::new(&probe_exe)
508 .arg("probe")
509 .arg("--death-pipe-fd")
510 .arg(death_read.to_string())
511 .process_group(0)
512 .stdout(Stdio::piped())
513 .stderr(Stdio::null())
514 .pre_exec(move || {
515 libc::close(death_write_copy);
516 libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM as libc::c_ulong);
517 Ok(())
518 })
519 .spawn()?
520 };
521 super::death_pipe::close_fd(death_read);
522 let probe_guard = ChildGuard::new(probe_proc);
523
524 log_info!("Building...");
526 let build_start = Instant::now();
527 let build_result = Command::new("sh")
528 .args(["-c", &build_cmd])
529 .stdout(Stdio::null())
530 .stderr(Stdio::piped())
531 .output()?;
532 let build_time = build_start.elapsed().as_secs_f64();
533
534 if !build_result.status.success() {
535 log_error!(
536 "Build failed (exit {})",
537 build_result.status.code().unwrap_or(-1)
538 );
539 drop(probe_guard);
540 super::death_pipe::close_fd(death_write);
541 if let Some(ref mut p) = pand_proc {
542 stop_scheduler(p);
543 }
544 bail!("BUILD FAILED");
545 }
546
547 std::thread::sleep(Duration::from_secs(1));
549
550 unsafe {
552 libc::killpg(probe_guard.id() as i32, libc::SIGTERM);
553 }
554 let probe_child = probe_guard.into_child();
555 let probe_output = probe_child.wait_with_output()?;
556 super::death_pipe::close_fd(death_write);
557 let probe_stdout = String::from_utf8_lossy(&probe_output.stdout);
558
559 if let Some(ref mut p) = pand_proc {
561 stop_scheduler(p);
562 log_info!("PANDEMONIUM stopped");
563 }
564
565 let mut overshoots: Vec<f64> = probe_stdout
567 .lines()
568 .filter_map(|line| line.trim().parse::<f64>().ok())
569 .collect();
570 overshoots.sort_by(|a, b| a.partial_cmp(b).unwrap());
571
572 let n = overshoots.len();
573 let med = percentile(&overshoots, 50.0);
574 let p99 = percentile(&overshoots, 99.0);
575 let worst = overshoots.last().copied().unwrap_or(0.0);
576
577 log_info!("Build time: {:.2}s", build_time);
578 log_info!("Probe samples: {}", n);
579 log_info!("Median overshoot: {:.0}us", med);
580 log_info!("P99 overshoot: {:.0}us", p99);
581 log_info!("Worst overshoot: {:.0}us", worst);
582
583 results.push(PhaseResult {
584 name: phase_name.to_string(),
585 build_time,
586 samples: n,
587 median: med,
588 p99,
589 worst,
590 });
591 }
592
593 let eevdf = &results[0];
595 let pand = &results[1];
596
597 let build_delta = if eevdf.build_time > 0.0 {
598 ((pand.build_time - eevdf.build_time) / eevdf.build_time) * 100.0
599 } else {
600 0.0
601 };
602 let med_delta = pand.median - eevdf.median;
603 let p99_delta = pand.p99 - eevdf.p99;
604
605 let mut report = Vec::new();
606 report.push(sep.clone());
607 report.push("CONTENTION BENCHMARK RESULTS".to_string());
608 report.push(sep.clone());
609 report
610 .push("WORKLOAD: CARGO BUILD --RELEASE + INTERACTIVE PROBE (10MS SLEEP/WAKE)".to_string());
611 report.push(String::new());
612 report.push(format!(
613 "{:<24} {:>8} {:>8} {:>8} {:>8} {:>8}",
614 "SCHEDULER", "BUILD", "SAMPLES", "MEDIAN", "P99", "WORST"
615 ));
616 report.push(format!(
617 "{} {} {} {} {} {}",
618 "-".repeat(24),
619 "-".repeat(8),
620 "-".repeat(8),
621 "-".repeat(8),
622 "-".repeat(8),
623 "-".repeat(8),
624 ));
625 for r in &results {
626 report.push(format!(
627 "{:<24} {:>7.2}s {:>8} {:>7.0}us {:>7.0}us {:>7.0}us",
628 r.name, r.build_time, r.samples, r.median, r.p99, r.worst,
629 ));
630 }
631 report.push(String::new());
632 report.push(format_delta(build_delta, "BUILD"));
633 report.push(format_latency_delta(med_delta, "MEDIAN"));
634 report.push(format_latency_delta(p99_delta, "P99"));
635 report.push(sep.clone());
636
637 let report_text = report.join("\n") + "\n";
638 for line in &report {
639 println!("{}", line);
640 }
641
642 let path = save_report(&report_text, "contention")?;
643 println!("\nSAVED TO {}", path);
644 Ok(())
645}