use std::{
io::Write,
process::{Command, ExitStatus},
sync::LazyLock,
};
static CURRENT: LazyLock<Isolation> = LazyLock::new(Isolation::from_env);
pub const PROCESS_ISOLATION_ENV: &str = "CODSPEED_PROCESS_ISOLATION";
const ISOLATED_BENCHMARK_ENV: &str = "CODSPEED_ISOLATED_BENCHMARK";
const GENERIC_FAILURE_EXIT_CODE: i32 = 1;
#[cfg(unix)]
const SIGNAL_EXIT_CODE_OFFSET: i32 = 128;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Isolation {
SharedProcess,
ProcessPerBenchmark,
IsolatedBenchmark(String),
}
impl Isolation {
pub fn current() -> &'static Self {
&CURRENT
}
fn from_env() -> Self {
if let Ok(uri) = std::env::var(ISOLATED_BENCHMARK_ENV) {
return Self::IsolatedBenchmark(uri);
}
match std::env::var(PROCESS_ISOLATION_ENV).as_deref() {
Err(_) | Ok("1" | "true") => Self::ProcessPerBenchmark,
Ok("0" | "false") => Self::SharedProcess,
Ok(other) => {
panic!("{PROCESS_ISOLATION_ENV} must be `true` or `false`, got `{other}`")
}
}
}
pub fn only_benchmark(&self) -> Option<&str> {
match self {
Self::IsolatedBenchmark(uri) => Some(uri),
Self::SharedProcess | Self::ProcessPerBenchmark => None,
}
}
pub fn run(&self, uri: String, bench: impl FnOnce(String)) {
match self {
Self::SharedProcess | Self::IsolatedBenchmark(_) => bench(uri),
Self::ProcessPerBenchmark => run_in_child_process(&uri),
}
}
}
fn run_in_child_process(uri: &str) {
if let Err(error) = std::io::stdout().flush() {
eprintln!("failed to flush stdout before running {uri}: {error}");
}
let exe = std::env::current_exe().expect("failed to locate the benchmark executable");
let status = Command::new(exe)
.args(std::env::args_os().skip(1))
.env(ISOLATED_BENCHMARK_ENV, uri)
.status()
.unwrap_or_else(|error| panic!("failed to start the process for {uri}: {error}"));
if !status.success() {
std::process::exit(exit_code(status));
}
}
fn exit_code(status: ExitStatus) -> i32 {
#[cfg(unix)]
if let Some(signal) = std::os::unix::process::ExitStatusExt::signal(&status) {
return SIGNAL_EXIT_CODE_OFFSET + signal;
}
status.code().unwrap_or(GENERIC_FAILURE_EXIT_CODE)
}