use std::hint::black_box;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::Duration;
use apimock::{App, EnvArgs};
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use tokio::runtime::Runtime;
struct BenchServer {
base_url: String,
fallback_dir: PathBuf,
rt: Runtime,
}
static SERVER: OnceLock<BenchServer> = OnceLock::new();
fn server() -> &'static BenchServer {
SERVER.get_or_init(|| {
let _ = log::set_boxed_logger(Box::new(NullLogger));
log::set_max_level(log::LevelFilter::Off);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("build tokio runtime for bench server");
let (port, fallback_dir, config_path) = rt.block_on(async { prepare_fixtures().await });
let env_args = EnvArgs {
config_file_path: Some(config_path.to_string_lossy().into_owned()),
port: Some(port),
fallback_respond_dir_path: None,
};
rt.spawn(async move {
let app = App::new(&env_args, None, false)
.await
.expect("bench server App::new");
app.server.start().await;
});
std::thread::sleep(Duration::from_millis(400));
BenchServer {
base_url: format!("http://127.0.0.1:{}", port),
fallback_dir,
rt,
}
})
}
async fn prepare_fixtures() -> (u16, PathBuf, PathBuf) {
let dir = Box::leak(Box::new(
tempfile::tempdir().expect("tempdir for bench fixtures"),
));
let fallback_dir = dir.path().join("fallback");
std::fs::create_dir_all(&fallback_dir).expect("mkdir fallback");
std::fs::write(
fallback_dir.join("hello.json"),
"{\"greeting\":\"hello\",\"items\":[1,2,3]}",
)
.expect("write hello.json");
let rule_set_path = dir.path().join("rules.toml");
std::fs::write(
&rule_set_path,
concat!(
"[[rules]]\n",
"when.request.url_path = \"/text\"\n",
"respond = { text = \"hello from text rule\" }\n",
"\n",
"[[rules]]\n",
"when.request.url_path = \"/status\"\n",
"respond = { status = 204 }\n",
"\n",
"[[rules]]\n",
"when.request.url_path = \"/file\"\n",
"respond = { file_path = \"hello.json\" }\n",
),
)
.expect("write rules.toml");
let config_path = dir.path().join("apimock.toml");
std::fs::write(
&config_path,
format!(
"[listener]\n\
ip_address = \"127.0.0.1\"\n\
port = 0\n\
\n\
[log]\n\
verbose = {{ header = false, body = false }}\n\
\n\
[service]\n\
rule_sets = [\"{}\"]\n\
fallback_respond_dir = \"{}\"\n",
rule_set_path
.file_name()
.unwrap()
.to_string_lossy(),
fallback_dir
.file_name()
.unwrap()
.to_string_lossy(),
),
)
.expect("write apimock.toml");
let fallback_abs = fallback_dir
.canonicalize()
.expect("canonicalize fallback");
let rule_set_body = std::fs::read_to_string(&rule_set_path).unwrap();
let rule_set_with_prefix = format!(
"[prefix]\nrespond_dir = \"{}\"\n\n{}",
fallback_abs.to_string_lossy(),
rule_set_body,
);
std::fs::write(&rule_set_path, rule_set_with_prefix).unwrap();
(pick_port(), fallback_dir, config_path)
}
fn pick_port() -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral");
let port = listener
.local_addr()
.expect("local_addr")
.port();
drop(listener);
port
}
fn bench_response_latency(c: &mut Criterion) {
let server = server();
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.expect("reqwest client");
let mut group = c.benchmark_group("response_latency");
group.throughput(Throughput::Elements(1));
group.sample_size(50);
group.measurement_time(Duration::from_secs(3));
group.bench_function("text_rule", |b| {
b.to_async(&server.rt).iter(|| async {
let resp = client
.get(format!("{}/text", server.base_url))
.send()
.await
.expect("GET /text");
let bytes = resp.bytes().await.expect("body");
black_box(bytes);
});
});
group.bench_function("status_rule", |b| {
b.to_async(&server.rt).iter(|| async {
let resp = client
.get(format!("{}/status", server.base_url))
.send()
.await
.expect("GET /status");
let _ = resp.bytes().await;
});
});
group.bench_function("file_rule_warm", |b| {
b.to_async(&server.rt).iter(|| async {
let resp = client
.get(format!("{}/file", server.base_url))
.send()
.await
.expect("GET /file");
let bytes = resp.bytes().await.expect("body");
black_box(bytes);
});
});
group.bench_function("dyn_route_fallback", |b| {
b.to_async(&server.rt).iter(|| async {
let resp = client
.get(format!("{}/hello", server.base_url))
.send()
.await
.expect("GET /hello");
let bytes = resp.bytes().await.expect("body");
black_box(bytes);
});
});
group.bench_function("not_found", |b| {
b.to_async(&server.rt).iter(|| async {
let resp = client
.get(format!("{}/does-not-exist", server.base_url))
.send()
.await
.expect("GET /does-not-exist");
let _ = resp.bytes().await;
});
});
let _ = &server.fallback_dir;
group.finish();
}
criterion_group!(benches, bench_response_latency);
criterion_main!(benches);
struct NullLogger;
impl log::Log for NullLogger {
fn enabled(&self, _: &log::Metadata) -> bool {
false
}
fn log(&self, _: &log::Record) {}
fn flush(&self) {}
}