use std::path::{Path, PathBuf};
use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use tempfile::TempDir;
use zccache::core::NormalizedPath;
use zccache::daemon::DaemonServer;
use zccache::protocol::{ExecCachePolicy, ExecOutputStreams, Request, Response};
#[cfg(unix)]
type ClientConn = zccache::ipc::IpcConnection;
#[cfg(windows)]
type ClientConn = zccache::ipc::IpcClientConnection;
fn target_bin_dir() -> PathBuf {
let mut p = std::env::current_exe().expect("current_exe");
p.pop();
p.pop();
p
}
fn binary_path(stem: &str) -> PathBuf {
let mut p = target_bin_dir();
if cfg!(windows) {
p.push(format!("{stem}.exe"));
} else {
p.push(stem);
}
p
}
fn find_test_tool() -> Option<PathBuf> {
let p = binary_path("exec_test_tool");
if p.is_file() {
Some(p)
} else {
eprintln!("exec_test_tool not found at {p:?} — skipping exec benches");
None
}
}
struct BenchHarness {
_cache: TempDir,
work: TempDir,
_server: tokio::task::JoinHandle<()>,
rt: tokio::runtime::Runtime,
client: ClientConn,
tool: PathBuf,
}
impl BenchHarness {
fn new() -> Option<Self> {
let tool = find_test_tool()?;
let cache = tempfile::tempdir().ok()?;
let work = tempfile::tempdir().ok()?;
std::env::set_var("ZCCACHE_CACHE_DIR", cache.path());
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.ok()?;
let (server, client) = rt.block_on(async {
let endpoint = zccache::ipc::unique_test_endpoint();
let mut server = DaemonServer::bind(&endpoint).expect("bind");
let _shutdown = server.shutdown_handle();
let server_task = tokio::spawn(async move {
server.run(0).await.expect("daemon run");
});
let client = zccache::ipc::connect(&endpoint).await.expect("connect");
(server_task, client)
});
Some(Self {
_cache: cache,
work,
_server: server,
rt,
client,
tool,
})
}
fn build_request(&self, args: Vec<String>, input_files: Vec<NormalizedPath>) -> Request {
Request::GenericToolExec {
tool: NormalizedPath::from(self.tool.as_path()),
args,
cwd: NormalizedPath::from(self.work.path()),
env: vec![],
input_files,
input_extra: Arc::new(Vec::new()),
output_streams: ExecOutputStreams::default(),
output_files: vec![],
tool_hash: None,
cache_policy: ExecCachePolicy::Normal,
cwd_in_key: true,
include_scan_files: vec![],
include_dirs: vec![],
system_include_dirs: vec![],
iquote_dirs: vec![],
depfile: None,
non_deterministic: false,
key_args_filter: vec![],
}
}
fn run(&mut self, req: &Request) -> Response {
let client = &mut self.client;
self.rt.block_on(async move {
client.send(req).await.unwrap();
client.recv::<Response>().await.unwrap().unwrap()
})
}
}
fn write_input(path: &Path, content: &[u8]) {
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).ok();
}
std::fs::write(path, content).unwrap();
}
fn bench_warm_hit(c: &mut Criterion) {
let Some(mut h) = BenchHarness::new() else {
return;
};
let input = h.work.path().join("warm_input.txt");
write_input(&input, b"warm-hit-bytes");
let req = h.build_request(
vec![
"0".into(),
input.to_string_lossy().into(),
"-".into(),
"-".into(),
],
vec![NormalizedPath::from(input.as_path())],
);
let primed = h.run(&req);
if let Response::GenericToolExecResult { cached, .. } = &primed {
assert!(!cached, "first call primes the cache");
}
let warm = h.run(&req);
if let Response::GenericToolExecResult { cached, .. } = &warm {
assert!(cached, "second call must be a warm hit");
}
c.bench_function("exec_warm_hit", |b| {
b.iter(|| {
let r = h.run(&req);
match r {
Response::GenericToolExecResult { cached: true, .. } => {}
other => panic!("expected warm hit, got {other:?}"),
}
});
});
}
fn bench_cold_miss(c: &mut Criterion) {
let Some(mut h) = BenchHarness::new() else {
return;
};
let work_root: PathBuf = h.work.path().to_path_buf();
let mut counter: u64 = 0;
c.bench_function("exec_cold_miss", |b| {
b.iter_batched(
|| {
counter = counter.wrapping_add(1);
let path = work_root.join(format!("cold_{counter}.txt"));
write_input(&path, format!("cold-{counter}").as_bytes());
path
},
|input| {
let req = h.build_request(
vec![
"0".into(),
input.to_string_lossy().into(),
"-".into(),
"-".into(),
],
vec![NormalizedPath::from(input.as_path())],
);
let r = h.run(&req);
match r {
Response::GenericToolExecResult { cached: false, .. } => {}
other => panic!("expected cold miss, got {other:?}"),
}
},
BatchSize::SmallInput,
);
});
}
fn bench_one_input_changed(c: &mut Criterion) {
let Some(mut h) = BenchHarness::new() else {
return;
};
let stable: PathBuf = h.work.path().join("stable.txt");
write_input(&stable, b"stable-content-stays-cached");
let churn: PathBuf = h.work.path().join("churn.txt");
let churn_for_setup = churn.clone();
let mut counter: u64 = 0;
c.bench_function("exec_one_input_changed", |b| {
b.iter_batched(
|| {
counter = counter.wrapping_add(1);
write_input(&churn_for_setup, format!("v{counter}").as_bytes());
},
|()| {
let req = h.build_request(
vec![
"0".into(),
churn.to_string_lossy().into(),
"-".into(),
"-".into(),
],
vec![
NormalizedPath::from(stable.as_path()),
NormalizedPath::from(churn.as_path()),
],
);
let _ = h.run(&req);
},
BatchSize::SmallInput,
);
});
}
criterion_group!(
name = exec;
config = Criterion::default()
.sample_size(20)
.warm_up_time(std::time::Duration::from_millis(500))
.measurement_time(std::time::Duration::from_secs(3));
targets = bench_warm_hit, bench_cold_miss, bench_one_input_changed
);
criterion_main!(exec);