#![deny(
clippy::all,
clippy::cargo,
clippy::nursery,
// clippy::restriction,
// clippy::pedantic
)]
#![allow(
clippy::suboptimal_flops,
clippy::redundant_pub_crate,
clippy::fallible_impl_from
)]
#![allow(clippy::multiple_crate_versions)]
#![allow(clippy::use_self)]
#![deny(missing_debug_implementations)]
#![deny(rustdoc::all)]
use clap::Parser;
use crossterm::style::{Attribute, SetAttribute};
use crossterm::ExecutableCommand;
use std::io::stdout;
use std::process::exit;
use ttfb::TtfbError;
use ttfb::TtfbOutcome;
const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
macro_rules! unwrap_or_exit {
($ident:ident) => {
if let Err(err) = $ident {
$crate::exit_error(err);
} else {
$ident.unwrap()
}
};
}
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "CLI utility to measure the TTFB (time to first byte) of HTTP(S) requests. \
Additionally, this crate measures the relative and absolute times of DNS \
lookup, TCP connect, and TLS handshake."
)]
struct TtfbArgs {
host: String,
#[arg(short = 'k', long = "insecure")]
allow_insecure_certificates: bool,
}
fn main() {
let input: TtfbArgs = TtfbArgs::parse();
let res = ttfb::ttfb(input.host, input.allow_insecure_certificates);
let ttfb = unwrap_or_exit!(res);
print_outcome(&ttfb).unwrap();
}
fn exit_error(err: TtfbError) -> ! {
eprint!("\u{1b}[31m");
eprint!("\u{1b}[1m");
eprint!("ERROR: ",);
eprint!("\u{1b}[0m");
eprint!("{}", err);
eprintln!();
exit(-1)
}
fn print_outcome(ttfb: &TtfbOutcome) -> Result<(), String> {
stdout()
.execute(SetAttribute(Attribute::Bold))
.map_err(|err| err.to_string())?;
println!(
"TTFB for {url} (by ttfb@v{crate_version})",
url = ttfb.user_input(),
crate_version = CRATE_VERSION
);
println!("PROPERTY REL TIME (ms) ABS TIME (ms)");
stdout()
.execute(SetAttribute(Attribute::Reset))
.map_err(|err| err.to_string())?;
if ttfb.dns_duration_rel().is_some() {
print!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "DNS Lookup",
rel_time = ttfb.dns_duration_rel().unwrap().as_secs_f64() * 1000.0,
abs_time = ttfb.dns_duration_rel().unwrap().as_secs_f64() * 1000.0,
);
if ttfb.dns_duration_rel().unwrap().as_millis() < 2 {
print!(" (probably cached)");
}
println!();
}
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "TCP connect",
rel_time = ttfb.tcp_connect_duration_rel().as_secs_f64() * 1000.0,
abs_time = ttfb.tcp_connect_duration_abs().as_secs_f64() * 1000.0,
);
if ttfb.tls_handshake_duration_rel().is_some() {
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "TLS Handshake",
rel_time = ttfb.tls_handshake_duration_rel().unwrap().as_secs_f64() * 1000.0,
abs_time = ttfb.tls_handshake_duration_abs().unwrap().as_secs_f64() * 1000.0,
);
}
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "HTTP GET Req",
rel_time = ttfb.http_get_send_duration_rel().as_secs_f64() * 1000.0,
abs_time = ttfb.http_get_send_duration_abs().as_secs_f64() * 1000.0,
);
stdout()
.execute(SetAttribute(Attribute::Bold))
.map_err(|err| err.to_string())?;
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "HTTP Resp TTFB",
rel_time = ttfb.http_ttfb_duration_rel().as_secs_f64() * 1000.0,
abs_time = ttfb.http_ttfb_duration_abs().as_secs_f64() * 1000.0,
);
stdout()
.execute(SetAttribute(Attribute::Reset))
.map_err(|err| err.to_string())?;
Ok(())
}