use arcature::App;
use arcature::axum::routing::get;
use arcature::prelude::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
const REQUESTS: usize = 5_000;
async fn one_request(addr: std::net::SocketAddr) -> std::io::Result<()> {
let mut stream = TcpStream::connect(addr).await?;
stream
.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.await?;
let mut buf = Vec::with_capacity(128);
stream.read_to_end(&mut buf).await?;
if !buf.starts_with(b"HTTP/1.1 200 OK") {
return Err(std::io::Error::other("unexpected response"));
}
Ok(())
}
async fn time_requests(addr: std::net::SocketAddr, n: usize) -> std::time::Duration {
let _ = one_request(addr).await;
let start = std::time::Instant::now();
for _ in 0..n {
one_request(addr)
.await
.expect("request succeeded in warmup; benchmark request should too");
}
start.elapsed()
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("Arcature Engine Phase — pipeline overhead benchmark");
println!("==================================================");
println!("Requests per implementation: {REQUESTS} (sequential, real TCP)");
println!(
"Build: {} (run with --release for meaningful numbers)",
build_mode()
);
println!();
let raw_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let raw_addr = raw_listener.local_addr().unwrap();
let (raw_tx, raw_rx) = tokio::sync::oneshot::channel::<()>();
let raw_server = tokio::spawn(async move {
let router = axum::Router::new().route("/", get(|| async { "hello" }));
axum::serve(raw_listener, router)
.with_graceful_shutdown(async {
let _ = raw_rx.await;
})
.await
});
let app_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let app_addr = app_listener.local_addr().unwrap();
let (app_tx, app_rx) = tokio::sync::oneshot::channel::<()>();
let app_server = tokio::spawn(async move {
let app = App::new().route("/", get(|| async { "hello" }));
app.serve_with_shutdown(app_listener, async {
let _ = app_rx.await;
})
.await
});
let engine_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let engine_addr = engine_listener.local_addr().unwrap();
let (engine_tx, engine_rx) = tokio::sync::oneshot::channel::<()>();
let engine_server = tokio::spawn(async move {
let app = Application::new()
.routes(Routes::new().route("/", get(|| async { "hello" })))
.build();
app.serve_with_shutdown(engine_listener, async {
let _ = engine_rx.await;
})
.await
.expect("engine serve");
});
let raw_time = time_requests(raw_addr, REQUESTS).await;
let app_time = time_requests(app_addr, REQUESTS).await;
let engine_time = time_requests(engine_addr, REQUESTS).await;
let _ = raw_tx.send(());
let _ = app_tx.send(());
let _ = engine_tx.send(());
let _ = raw_server.await;
let _ = app_server.await;
let _ = engine_server.await;
#[cfg(all(feature = "observe", feature = "pages"))]
let full_us = {
let full_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let full_addr = full_listener.local_addr().unwrap();
let (full_tx, full_rx) = tokio::sync::oneshot::channel::<()>();
let full_server = tokio::spawn(async move {
let app = Application::new()
.routes(Routes::new().route("/", get(|| async { "hello" })))
.proxy(|_req| ProxyAction::continue_default())
.pages(arcature::pages::Pages::new())
.build();
app.serve_with_shutdown(full_listener, async {
let _ = full_rx.await;
})
.await
.expect("full engine serve");
});
let full_time = time_requests(full_addr, REQUESTS).await;
let _ = full_tx.send(());
let _ = full_server.await;
full_time.as_secs_f64() * 1_000_000.0 / REQUESTS as f64
};
#[cfg(not(all(feature = "observe", feature = "pages")))]
println!("(skipping full-stack case: build with --features \"observe,pages\" to include)");
let raw_us = raw_time.as_secs_f64() * 1_000_000.0 / REQUESTS as f64;
let app_us = app_time.as_secs_f64() * 1_000_000.0 / REQUESTS as f64;
let engine_us = engine_time.as_secs_f64() * 1_000_000.0 / REQUESTS as f64;
println!("--- Results (μs per request, sequential, end-to-end TCP) ---");
println!(" raw Axum: {raw_us:>10.2} μs/req (total {raw_time:?})");
println!(" App (kernel): {app_us:>10.2} μs/req (total {app_time:?})");
println!(
" Application (min): {engine_us:>10.2} μs/req (total {engine_time:?}) — ProxyLayer pass-through"
);
#[cfg(all(feature = "observe", feature = "pages"))]
println!(" Application (full): {full_us:>10.2} μs/req — proxy + observe + pages");
println!();
println!("--- Overhead vs raw Axum ---");
let app_overhead = (app_us - raw_us) / raw_us * 100.0;
let engine_overhead = (engine_us - raw_us) / raw_us * 100.0;
println!(" App (kernel): {app_overhead:+.1}%");
println!(" Application (min): {engine_overhead:+.1}%");
#[cfg(all(feature = "observe", feature = "pages"))]
{
let full_overhead = (full_us - raw_us) / raw_us * 100.0;
println!(" Application (full): {full_overhead:+.1}%");
}
println!();
println!("--- Interpretation ---");
println!(" - App (kernel) forwards to axum::serve with the same Router, so");
println!(" per-request overhead is expected to be ~0 (within timing noise).");
println!(" - Application (min) always applies ProxyLayer (a zero-overhead");
println!(" pass-through when no proxy fn is installed) — the cost is one");
println!(" `Option::is_none` check per request plus a `Box::pin` allocation");
println!(" shared with axum::serve's per-connection setup.");
println!(" - Application (full) adds RequestIdLayer (pre-routing), the");
println!(" MatchedRoute adapter + HttpLayer (route-layers), and the pages");
println!(" 404 fallback. Each adds a small per-request cost; the benchmark");
println!(" shows the cumulative engine pipeline overhead.");
println!(" - Sequential requests over loopback TCP; the *difference* is the");
println!(" relevant signal (TCP/HTTP overhead is common to all).");
}
fn build_mode() -> &'static str {
if cfg!(debug_assertions) {
"debug"
} else {
"release"
}
}