http-handle 0.0.5

A fast and lightweight Rust library for handling HTTP requests and responses.
Documentation
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (c) 2023 - 2026 HTTP Handle

#![allow(missing_docs)]

//! Criterion benchmarks for the synchronous `Server` hot path and a
//! micro-bench for `Response::send`. The harness:
//!
//! - Binds to `127.0.0.1:0` via a probe listener to discover a free port,
//!   then hands the port off to `Server::start()`. Safe for concurrent runs
//!   (no hard-coded `8082` collision).
//! - Reads every response to EOF before dropping the client socket, so the
//!   previous `Connection reset by peer` noise in stderr is eliminated.
//! - Covers both the blocking accept loop (`Server::start`) and the
//!   shutdown-aware loop (`Server::start_with_shutdown_signal_and_ready`).
//!   The shutdown-aware loop now uses adaptive 100µs–5ms backoff instead
//!   of a fixed 100ms sleep-poll, so single-client latency is no longer
//!   dominated by accept-poll cadence.
//!
//! The server thread is leaked at bench end; the process exits immediately
//! after Criterion, so the OS reclaims it.

use criterion::{Criterion, criterion_group, criterion_main};
use http_handle::response::Response;
use http_handle::{Server, ShutdownSignal};
use std::hint::black_box;
use std::io::{Cursor, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;

/// Reserves a free TCP port on the loopback interface.
fn reserve_port() -> (String, TempDir) {
    let probe = TcpListener::bind("127.0.0.1:0").expect("probe bind");
    let addr = probe.local_addr().expect("addr").to_string();
    drop(probe);
    let root = TempDir::new().expect("tempdir");
    (addr, root)
}

enum AcceptMode {
    Basic,
    ThreadPool(usize),
}

fn spawn_sync_server_with(body: &[u8], mode: AcceptMode) -> String {
    let (addr, root) = reserve_port();
    std::fs::write(root.path().join("test.html"), body)
        .expect("write body");
    std::fs::create_dir(root.path().join("404")).expect("404 dir");
    std::fs::write(root.path().join("404/index.html"), b"404")
        .expect("write 404");
    let document_root =
        root.path().to_str().expect("utf8 path").to_string();
    let addr_for_server = addr.clone();
    // Keep the TempDir alive for the life of the server thread.
    let _ = thread::spawn(move || {
        let _root_keepalive = root;
        let server = Server::new(&addr_for_server, &document_root);
        match mode {
            AcceptMode::Basic => {
                let _ = server.start();
            }
            AcceptMode::ThreadPool(size) => {
                let _ = server.start_with_thread_pool(size);
            }
        }
    });
    // Retry until the kernel reports the port as bound.
    for _ in 0..100 {
        if TcpStream::connect(&addr).is_ok() {
            return addr;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("server never bound on {addr}");
}

fn spawn_sync_server(body: &[u8]) -> String {
    spawn_sync_server_with(body, AcceptMode::Basic)
}

fn spawn_sync_server_with_rate_limit(
    body: &[u8],
    rate_per_minute: usize,
) -> String {
    let (addr, root) = reserve_port();
    std::fs::write(root.path().join("test.html"), body)
        .expect("write body");
    std::fs::create_dir(root.path().join("404")).expect("404 dir");
    std::fs::write(root.path().join("404/index.html"), b"404")
        .expect("write 404");
    let document_root =
        root.path().to_str().expect("utf8 path").to_string();
    let addr_for_server = addr.clone();
    let _ = thread::spawn(move || {
        let _root_keepalive = root;
        let server = Server::builder()
            .address(&addr_for_server)
            .document_root(&document_root)
            .rate_limit_per_minute(rate_per_minute)
            .build()
            .expect("server build");
        let _ = server.start();
    });
    for _ in 0..100 {
        if TcpStream::connect(&addr).is_ok() {
            return addr;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("server never bound on {addr}");
}

fn roundtrip(addr: &str) {
    let mut stream = TcpStream::connect(addr).expect("connect");
    stream
        .write_all(b"GET /test.html HTTP/1.1\r\nHost: b\r\n\r\n")
        .expect("write");
    let mut sink = Vec::with_capacity(256);
    let _ = stream.read_to_end(&mut sink).expect("read");
    let _ = black_box(sink);
}

fn bench_sync_server_small_body(c: &mut Criterion) {
    let addr =
        spawn_sync_server(b"<html><body>Test Content</body></html>");
    let _ = c.bench_function("sync_server_small_body_38B", |b| {
        b.iter(|| roundtrip(&addr));
    });
}

fn drive_concurrent(addr: &str, parallelism: usize) {
    // Use scoped threads so the bench loop borrows `addr` without cloning.
    thread::scope(|s| {
        let mut handles = Vec::with_capacity(parallelism);
        for _ in 0..parallelism {
            handles.push(s.spawn(|| roundtrip(addr)));
        }
        for h in handles {
            let _ = h.join();
        }
    });
}

fn bench_sync_server_basic_concurrent_8(c: &mut Criterion) {
    let addr =
        spawn_sync_server(b"<html><body>Test Content</body></html>");
    let _ = c.bench_function("sync_server_basic_concurrent_8", |b| {
        b.iter(|| drive_concurrent(&addr, 8));
    });
}

fn bench_sync_server_rate_limit_concurrent_8(c: &mut Criterion) {
    // Rate limit set high enough that requests don't get 429'd during
    // the bench window; the goal is to exercise the contended path
    // (every request still hits the sharded mutex), not to trigger
    // limiting. usize::MAX/2 keeps headroom while staying well within
    // the rate-limit guard's expected range.
    let addr = spawn_sync_server_with_rate_limit(
        b"<html><body>Test Content</body></html>",
        usize::MAX / 2,
    );
    let _ =
        c.bench_function("sync_server_rate_limit_concurrent_8", |b| {
            b.iter(|| drive_concurrent(&addr, 8));
        });
}

fn bench_sync_server_thread_pool_concurrent_8(c: &mut Criterion) {
    let addr = spawn_sync_server_with(
        b"<html><body>Test Content</body></html>",
        AcceptMode::ThreadPool(8),
    );
    let _ =
        c.bench_function("sync_server_thread_pool_concurrent_8", |b| {
            b.iter(|| drive_concurrent(&addr, 8));
        });
}

fn bench_sync_server_thread_pool_concurrent_32(c: &mut Criterion) {
    // Pool size deliberately under fan-out (16 workers, 32 inflight) so
    // requests actually queue through the crossbeam-channel and the
    // lock-free advantage shows up.
    let addr = spawn_sync_server_with(
        b"<html><body>Test Content</body></html>",
        AcceptMode::ThreadPool(16),
    );
    let _ = c.bench_function(
        "sync_server_thread_pool_concurrent_32",
        |b| {
            b.iter(|| drive_concurrent(&addr, 32));
        },
    );
}

fn bench_sync_server_thread_pool_concurrent_64(c: &mut Criterion) {
    let addr = spawn_sync_server_with(
        b"<html><body>Test Content</body></html>",
        AcceptMode::ThreadPool(16),
    );
    let _ = c.bench_function(
        "sync_server_thread_pool_concurrent_64",
        |b| {
            b.iter(|| drive_concurrent(&addr, 64));
        },
    );
}

fn spawn_shutdown_aware_server(body: &[u8]) -> String {
    let (probe_addr, root) = reserve_port();
    std::fs::write(root.path().join("test.html"), body)
        .expect("write body");
    std::fs::create_dir(root.path().join("404")).expect("404 dir");
    std::fs::write(root.path().join("404/index.html"), b"404")
        .expect("write 404");
    let document_root =
        root.path().to_str().expect("utf8 path").to_string();
    let shutdown =
        Arc::new(ShutdownSignal::new(Duration::from_secs(2)));
    let _shutdown_keep = shutdown.clone();
    let (ready_tx, ready_rx) = mpsc::channel::<String>();
    let _ = thread::spawn(move || {
        let _root_keep = root;
        let server = Server::builder()
            .address(&probe_addr)
            .document_root(&document_root)
            .build()
            .expect("server build");
        let _ = server.start_with_shutdown_signal_and_ready(
            shutdown,
            move |addr| {
                let _ = ready_tx.send(addr);
            },
        );
    });
    ready_rx
        .recv_timeout(Duration::from_secs(2))
        .expect("server never reported ready")
}

fn bench_sync_server_shutdown_aware_single(c: &mut Criterion) {
    let addr = spawn_shutdown_aware_server(
        b"<html><body>Test Content</body></html>",
    );
    let _ =
        c.bench_function("sync_server_shutdown_aware_single", |b| {
            b.iter(|| roundtrip(&addr));
        });
}

fn bench_response_send_small(c: &mut Criterion) {
    let mut response = Response::new(
        200,
        "OK",
        b"<html><body>hello</body></html>".to_vec(),
    );
    response.add_header("Content-Type", "text/html");
    response.add_header("ETag", "W/\"1f-68a0cf20\"");
    response.add_header("Accept-Ranges", "bytes");
    let _ =
        c.bench_function("response_send_small_body_5_headers", |b| {
            b.iter(|| {
                let mut sink =
                    Cursor::new(Vec::<u8>::with_capacity(256));
                response.send(&mut sink).expect("send");
                let _ = black_box(sink.into_inner());
            });
        });
}

criterion_group! {
    name = benches;
    config = Criterion::default()
        .warm_up_time(Duration::from_secs(2))
        .measurement_time(Duration::from_secs(5))
        .sample_size(60);
    targets =
        bench_sync_server_small_body,
        bench_sync_server_basic_concurrent_8,
        bench_sync_server_thread_pool_concurrent_8,
        bench_sync_server_thread_pool_concurrent_32,
        bench_sync_server_thread_pool_concurrent_64,
        bench_sync_server_rate_limit_concurrent_8,
        bench_sync_server_shutdown_aware_single,
        bench_response_send_small
}
criterion_main!(benches);