xocomil 0.3.0

A lightweight, zero-allocation HTTP/1.1 request parser and response writer
Documentation
//! Twin of `perf_parse` for the `httparse` crate, used to compare
//! parser speed under the same clean (non-Criterion) harness.
//!
//! Same fixtures, same iteration count, same `black_box` discipline.
//! Differences in `ns/iter` between this and `perf_parse` are the
//! true parser-speed gap on this machine — Criterion's per-iter
//! overhead drops out because both runs share the same harness.

use std::hint::black_box;

const MINIMAL: &[u8] = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";

const HTMX: &[u8] = b"GET /about HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Accept: text/html\r\n\
HX-Request: true\r\n\
HX-Target: #content\r\n\
HX-Current-URL: http://localhost:8080/\r\n\
User-Agent: Mozilla/5.0\r\n\
Accept-Language: en-US,en;q=0.9\r\n\
Connection: keep-alive\r\n\
\r\n";

const BROWSER: &[u8] = b"GET /articles/digital-signatures-explained HTTP/1.1\r\n\
Host: authentidoc.com\r\n\
Connection: keep-alive\r\n\
Cache-Control: max-age=0\r\n\
Upgrade-Insecure-Requests: 1\r\n\
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n\
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\r\n\
Accept-Encoding: gzip, deflate, br\r\n\
Accept-Language: en-US,en;q=0.9,es;q=0.8\r\n\
Cookie: session=abc123; theme=dark\r\n\
If-None-Match: \"etag-value\"\r\n\
HX-Request: true\r\n\
HX-Target: #content\r\n\
HX-Current-URL: https://authentidoc.com/\r\n\
HX-Trigger: nav-link\r\n\
\r\n";

fn many_headers() -> Vec<u8> {
    let mut raw = b"GET /features HTTP/1.1\r\nHost: localhost\r\n".to_vec();
    for i in 0..30 {
        raw.extend_from_slice(format!("X-Header-{i}: value-{i}\r\n").as_bytes());
    }
    raw.extend_from_slice(b"\r\n");
    raw
}

fn main() {
    let mut args = std::env::args().skip(1);
    let fixture = args.next().unwrap_or_else(|| "htmx".to_string());
    let iters: u64 = args
        .next()
        .as_deref()
        .and_then(|s| s.parse().ok())
        .unwrap_or(100_000_000);

    let many = many_headers();
    let raw: &[u8] = match fixture.as_str() {
        "minimal" => MINIMAL,
        "htmx" => HTMX,
        "browser" => BROWSER,
        "many" => &many,
        other => {
            eprintln!("unknown fixture: {other}. expected one of: minimal htmx browser many");
            std::process::exit(2);
        }
    };

    eprintln!(
        "fixture={fixture} bytes={} iters={iters} parser=httparse",
        raw.len()
    );
    let start = std::time::Instant::now();

    // httparse mirrors xocomil's no-alloc parse: stack-allocated header
    // array + parse-into. 32 slots matches our default MAX_HDRS.
    for _ in 0..iters {
        let mut headers = [httparse::EMPTY_HEADER; 32];
        let mut req = httparse::Request::new(&mut headers);
        let s = req.parse(black_box(raw)).unwrap();
        black_box(s);
    }

    let elapsed = start.elapsed();
    #[allow(clippy::cast_precision_loss)]
    let ns_per = elapsed.as_secs_f64() * 1e9 / iters as f64;
    eprintln!("elapsed={elapsed:?} ns/iter={ns_per:.2}");
}