use armature_h1::{ConnConfig, Connection, DateCache, Limits, Request, Response};
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::{Cell, RefCell};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::rc::Rc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
thread_local! {
static ALLOCS: Cell<u64> = const { Cell::new(0) };
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
fn tick() {
let armed = COUNTING.try_with(Cell::get).unwrap_or(false);
if armed {
let _ = ALLOCS.try_with(|c| c.set(c.get() + 1));
}
}
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
tick();
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
tick();
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
tick();
unsafe { System.alloc_zeroed(layout) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn arm() {
ALLOCS.with(|c| c.set(0));
COUNTING.with(|c| c.set(true));
}
fn disarm() -> u64 {
COUNTING.with(|c| c.set(false));
ALLOCS.with(Cell::get)
}
async fn hello(_req: Request) -> Response {
Response::text("hi")
}
const MEASURED_PEER: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 54321);
async fn hello_with_peer(req: Request) -> Response {
assert_eq!(
req.peer,
Some(MEASURED_PEER),
"the connection was built with a peer address and the handler must see \
it; a zero-allocation result means nothing if the field never arrived"
);
Response::text("hi")
}
async fn drain(mut req: Request) -> Response {
let _ = req.body.collect(64 * 1024).await;
Response::text("hi")
}
fn limits() -> Limits {
Limits {
idle_timeout: Duration::from_millis(200),
header_timeout: Duration::from_millis(200),
..Default::default()
}
}
fn steady_state_allocs<S, Fut>(request: &'static [u8], service: S, warm: usize, count: usize) -> u64
where
S: Fn(Request) -> Fut + Copy + 'static,
Fut: std::future::Future<Output = Response> + 'static,
{
steady_state_allocs_with_peer(request, service, warm, count, None)
}
fn steady_state_allocs_with_peer<S, Fut>(
request: &'static [u8],
service: S,
warm: usize,
count: usize,
peer: Option<SocketAddr>,
) -> u64
where
S: Fn(Request) -> Fut + Copy + 'static,
Fut: std::future::Future<Output = Response> + 'static,
{
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let _guard = rt.enter();
let local = tokio::task::LocalSet::new();
let (mut client, server) = tokio::io::duplex(1024 * 1024);
let conn = Connection::new(
server,
service,
Rc::new(ConnConfig {
limits: limits(),
tick: Duration::from_millis(50),
server_name: None,
}),
Rc::new(RefCell::new(DateCache::new())),
)
.with_peer(peer);
let task = local.spawn_local(conn.serve());
rt.block_on(local.run_until(async move {
let mut scratch = vec![0u8; 64 * 1024];
for i in 0..warm {
client.write_all(request).await.expect("write");
match tokio::time::timeout(
Duration::from_secs(5),
read_one_response(&mut client, &mut scratch),
)
.await
{
Ok(()) => {}
Err(_) => panic!("warm-up request {i} never got a response"),
}
}
arm();
for _ in 0..count {
client.write_all(request).await.expect("write");
read_one_response(&mut client, &mut scratch).await;
}
let allocs = disarm();
drop(client);
let _ = task.await;
allocs
}))
}
async fn read_one_response(client: &mut tokio::io::DuplexStream, scratch: &mut [u8]) {
let n = client.read(scratch).await.expect("read");
assert!(n > 0, "server closed unexpectedly");
assert!(
scratch[..n].starts_with(b"HTTP/1.1 200 OK"),
"unexpected response: {}",
String::from_utf8_lossy(&scratch[..n.min(120)])
);
}
const KEEPALIVE_GET: &[u8] = b"GET / HTTP/1.1\r\nHost: a.example\r\n\r\n";
const BROWSER_GET: &[u8] = b"GET /index.html HTTP/1.1\r\n\
Host: a.example\r\n\
User-Agent: Mozilla/5.0\r\n\
Accept: text/html,application/xhtml+xml\r\n\
Accept-Language: en-US,en;q=0.9\r\n\
Accept-Encoding: gzip, deflate, br\r\n\
Connection: keep-alive\r\n\
Cache-Control: max-age=0\r\n\
\r\n";
const BROWSER_GET_MIXED_CASE: &[u8] = b"GET /index.html HTTP/1.1\r\n\
Host: a.example\r\n\
User-Agent: Mozilla/5.0\r\n\
Accept: text/html,application/xhtml+xml\r\n\
Accept-Language: en-US,en;q=0.9\r\n\
Accept-Encoding: gzip, deflate, br\r\n\
Connection: keep-alive\r\n\
Sec-Fetch-Mode: navigate\r\n\
Sec-Fetch-Site: none\r\n\
Sec-Fetch-Dest: document\r\n\
Sec-Ch-Ua: \"Not.A/Brand\";v=\"8\"\r\n\
Upgrade-Insecure-Requests: 1\r\n\
DNT: 1\r\n\
\r\n";
const FIXED_BODY_POST: &[u8] =
b"POST / HTTP/1.1\r\nHost: a.example\r\nContent-Length: 5\r\n\r\nhello";
const CHUNKED_POST: &[u8] = b"POST / HTTP/1.1\r\nHost: a.example\r\n\
Transfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
const BUDGET_PER_GET: u64 = 0;
#[test]
fn steady_state_keepalive_get_stays_within_budget() {
let n = 100;
let allocs = steady_state_allocs(KEEPALIVE_GET, hello, 50, n);
let per_request = allocs as f64 / n as f64;
println!("keep-alive GET: {allocs} allocations over {n} requests ({per_request:.2}/request)");
assert!(
allocs <= BUDGET_PER_GET * n as u64,
"keep-alive GET budget exceeded: {per_request:.2} allocations per request, \
budget is {BUDGET_PER_GET}"
);
}
#[test]
fn steady_state_keepalive_get_with_peer_stays_within_budget() {
let n = 100;
let allocs =
steady_state_allocs_with_peer(KEEPALIVE_GET, hello_with_peer, 50, n, Some(MEASURED_PEER));
let per_request = allocs as f64 / n as f64;
println!(
"keep-alive GET with peer: {allocs} allocations over {n} requests ({per_request:.2}/request)"
);
assert!(
allocs <= BUDGET_PER_GET * n as u64,
"a populated peer address must not cost an allocation: {per_request:.2} per request, \
budget is {BUDGET_PER_GET}"
);
}
#[test]
fn browser_sized_get_stays_within_budget() {
let n = 100;
let allocs = steady_state_allocs(BROWSER_GET, hello, 50, n);
let per_request = allocs as f64 / n as f64;
println!("browser GET (7 headers): {allocs} over {n} ({per_request:.2}/request)");
assert!(
allocs <= BUDGET_PER_GET * n as u64,
"header count must not drive allocations: {per_request:.2} per request"
);
}
const BUDGET_PER_MIXED_CASE_HEADER_GET: u64 = 6;
#[test]
fn browser_sized_get_with_mixed_case_unknown_headers_stays_within_budget() {
let n = 100;
let allocs = steady_state_allocs(BROWSER_GET_MIXED_CASE, hello, 50, n);
let per_request = allocs as f64 / n as f64;
println!(
"browser GET (7 well-known + 6 mixed-case unknown headers): {allocs} over {n} ({per_request:.2}/request)"
);
assert_eq!(
allocs,
BUDGET_PER_MIXED_CASE_HEADER_GET * n as u64,
"mixed-case unknown headers must cost exactly one allocation each per request \
(the lowercasing-copy branch in src/parse.rs): {per_request:.2} per request, \
budget is {BUDGET_PER_MIXED_CASE_HEADER_GET}"
);
}
#[test]
fn fixed_body_post_stays_within_budget() {
let n = 100;
let allocs = steady_state_allocs(FIXED_BODY_POST, drain, 50, n);
let per_request = allocs as f64 / n as f64;
println!("Content-Length POST: {allocs} over {n} ({per_request:.2}/request)");
assert!(
allocs <= BUDGET_PER_GET * n as u64,
"fixed-body POST budget exceeded: {per_request:.2} per request"
);
}
#[test]
fn chunked_post_stays_within_budget() {
let n = 100;
let allocs = steady_state_allocs(CHUNKED_POST, drain, 50, n);
let per_request = allocs as f64 / n as f64;
println!("chunked POST: {allocs} over {n} ({per_request:.2}/request)");
assert!(
allocs <= BUDGET_PER_GET * n as u64,
"chunked POST budget exceeded: {per_request:.2} per request"
);
}
#[test]
fn per_request_cost_does_not_grow_with_connection_age() {
let early = steady_state_allocs(KEEPALIVE_GET, hello, 20, 50) as f64 / 50.0;
let late = steady_state_allocs(KEEPALIVE_GET, hello, 500, 50) as f64 / 50.0;
println!("early: {early:.2}/request, after 500 requests: {late:.2}/request");
assert!(
late <= early,
"per-request cost grew with connection age: {early:.2} -> {late:.2}"
);
}