plumbrs 0.24.2

A high-performance HTTP/1.1 and HTTP/2 benchmarking tool
pub mod client;
pub mod engine;
pub mod metrics;
pub mod options;
pub mod stats;

use anyhow::{Result, anyhow};
use clap::Parser;
use client::ClientType;

use crossterm::{cursor, execute};

#[cfg(feature = "mimalloc")]
use mimalloc::MiMalloc;

use crate::options::Options;
use ctor::dtor;

#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

#[dtor]
fn cleanup() {
    _ = execute!(std::io::stdout(), cursor::Show);
}

fn main() -> Result<()> {
    // Hide cursor and ensure it's restored on exit
    _ = execute!(std::io::stdout(), cursor::Hide);

    #[cfg(feature = "mimalloc")]
    eprintln!("using allocator: mimalloc");
    #[cfg(not(feature = "mimalloc"))]
    eprintln!("using allocator: system");

    pretty_env_logger::init();
    let mut opts = Options::parse();
    check_options(&mut opts)?;
    engine::run_tokio_engines(opts)
}

fn check_options(opts: &mut Options) -> Result<()> {
    if matches!(opts.method, Some(http::Method::TRACE)) && opts.body.len() > 1 {
        return Err(anyhow!("TRACE method cannot have a body!"));
    }

    if !opts.trailers.is_empty() && opts.body.is_empty() {
        opts.body.push(String::new());
    }

    if opts.method.is_none() {
        if opts.body.is_empty() {
            opts.method = Some(http::Method::GET);
        } else {
            opts.method = Some(http::Method::POST);
        }
    }

    #[cfg(feature = "mcp")]
    if !matches!(opts.client_type, ClientType::Auto)
        && !matches!(opts.client_type, ClientType::HyperMcp)
        && (opts.mcp || opts.mcp_sse)
    {
        return Err(anyhow!("MCP not supported with this client!"));
    }

    if !opts.trailers.is_empty() {
        match opts.client_type {
            ClientType::Auto => opts.client_type = ClientType::HyperChunked,
            ClientType::HyperChunked | ClientType::HyperH2 => {}
            _ => {
                return Err(anyhow!(
                    "Trailers are only supported with hyper-chunked or hyper-h2 clients!"
                ));
            }
        }
    }

    match opts.client_type {
        #[cfg(all(target_os = "linux", feature = "tokio_uring"))]
        ClientType::TokioUring if opts.http2 => {
            return Err(anyhow!("HTTP/2 not supported with tokio-uring client!"));
        }
        #[cfg(all(target_os = "linux", feature = "tokio_uring"))]
        ClientType::TokioUring if opts.multithreaded.unwrap_or(1) > 1 => {
            return Err(anyhow!(
                "Multithreaded runtime not supported with io-uring client!"
            ));
        }
        #[cfg(all(target_os = "linux", feature = "tokio_uring"))]
        ClientType::TokioUring if opts.uri.is_empty() => {
            println!("Missing URI. Try --help");
            std::process::exit(1);
        }
        ClientType::Auto
        | ClientType::HyperLegacy
        | ClientType::Hyper
        | ClientType::HyperRt1
        | ClientType::HyperH2
            if opts.uri.is_empty() =>
        {
            println!("Missing URI. Try --help");
            std::process::exit(1);
        }
        ClientType::HyperLegacy | ClientType::HyperRt1 if opts.host.is_some() => {
            return Err(anyhow!("Host option not available with this client!"));
        }
        ClientType::Help => {
            println!("Available client types:");
            println!(
                "  hyper             - Hyper client, one per connection. Both HTTP/1 and HTTP/2"
            );
            println!(
                "  hyper-chunked  - Hyper client, one per connection, with multi-chunked body. Both HTTP/1 and HTTP/2"
            );
            println!(
                "  hyper-h2          - Hyper client, one per connection. Use h2 package, HTTP/2 only"
            );
            println!(
                "  hyper-legacy      - Hyper client (legacy), one per connection. Both HTTP/1 and HTTP/2"
            );
            println!(
                "  hyper-rt1         - Hyper client (legacy), one per runtime. Both HTTP/1 and HTTP/2"
            );
            println!(
                "  reqwest           - Reqwest client, one per runtime. Both HTTP/1 and HTTP/2"
            );
            println!("  tokio-uring       - Tokio-uring client, one per thread. Only HTTP/1");
            println!("  monoio            - Monoio client, one per thread. Only HTTP/1");
            std::process::exit(0);
        }
        _ => (),
    }

    if let Some(nt) = opts.multithreaded
        && !opts.threads.is_multiple_of(nt)
    {
        return Err(anyhow!(
            "The number of threads must be an exact multiple of the thread count for each individual runtime"
        ));
    }

    Ok(())
}