use armature_core::{Error, HttpRequest, HttpResponse, Router};
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
thread_local! {
static ALLOCS: Cell<u64> = const { Cell::new(0) };
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
fn tick() {
if COUNTING.try_with(Cell::get).unwrap_or(false) {
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 count<T>(f: impl FnOnce() -> T) -> u64 {
ALLOCS.with(|c| c.set(0));
COUNTING.with(|c| c.set(true));
let out = f();
COUNTING.with(|c| c.set(false));
drop(out);
ALLOCS.with(Cell::get)
}
const BUDGET_CONSTRUCT: u64 = 1;
#[test]
fn constructing_a_request_costs_only_its_target() {
let n = count(|| HttpRequest::new("GET", "/users/42"));
println!("construct: {n} allocations");
assert!(
n <= BUDGET_CONSTRUCT,
"constructing a request cost {n} allocations, budget is {BUDGET_CONSTRUCT}"
);
}
#[test]
fn an_unread_query_string_costs_nothing() {
let req = HttpRequest::new("GET", "/s?a=1&b=2&c=hello%20world");
let n = count(|| req.headers.len());
println!("unread query: {n} allocations");
assert_eq!(
n, 0,
"a query string no handler reads must not be parsed or decoded"
);
}
const BUDGET_SIX_HEADERS: u64 = 6;
#[test]
fn well_known_header_names_cost_no_allocation() {
let mut req = HttpRequest::new("GET", "/");
let n = count(|| {
req.headers.insert("host", "a.example");
req.headers.insert("accept", "*/*");
req.headers.insert("accept-encoding", "gzip");
req.headers.insert("user-agent", "curl/8");
req.headers.insert("connection", "keep-alive");
req.headers.insert("content-length", "0");
});
println!("six headers: {n} allocations");
assert!(
n <= BUDGET_SIX_HEADERS,
"six headers cost {n} allocations, budget is {BUDGET_SIX_HEADERS}: \
interning a well-known name must not allocate"
);
}
#[test]
fn cloning_a_request_does_not_copy_its_body_or_target() {
let mut req = HttpRequest::new("POST", "/upload");
req.set_body_bytes(bytes::Bytes::from(vec![0u8; 1024 * 1024]));
let first = req.clone();
assert_eq!(
first.body.as_ptr(),
req.body.as_ptr(),
"the promotion must share the buffer, not copy it"
);
let n = count(|| req.clone());
println!("clone: {n} allocations");
assert_eq!(n, 0, "cloning a request cost {n} allocations");
}
const BUDGET_DISPATCH: u64 = 4;
#[test]
fn dispatch_allocates_only_for_captured_params() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("runtime");
let mut router = Router::new();
router.get("/users/:id", |_req: HttpRequest| async {
Ok::<_, Error>(HttpResponse::new(200))
});
rt.block_on(router.route(HttpRequest::new("GET", "/users/1")))
.expect("warm-up dispatch");
let n = count(|| {
rt.block_on(router.route(HttpRequest::new("GET", "/users/42")))
.expect("dispatch")
});
println!("dispatch: {n} allocations");
assert!(
n <= BUDGET_DISPATCH,
"dispatch cost {n} allocations, budget is {BUDGET_DISPATCH}"
);
}