#![deny(
clippy::all,
clippy::cargo,
clippy::nursery,
clippy::must_use_candidate,
// 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::ExecutableCommand;
use crossterm::style::{Attribute, SetAttribute};
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(
version,
about = "CLI utility to measure the TTFB (time to first byte) of HTTP(S) \
requests. This includes data of intermediate steps, such as the relative \
and absolute timings of DNS lookup, TCP connect, and TLS handshake. \
\n\n\
For issues or merge requests, please visit https://github.com/phip1611/ttfb."
)]
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 let Some(duration_pair) = ttfb.dns_lookup_duration() {
let duration = duration_pair.relative().as_secs_f64() * 1000.0;
print!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "DNS Lookup",
rel_time = duration,
abs_time = duration,
);
if duration < 2.0 {
print!(" (probably cached)");
}
println!();
}
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "TCP connect",
rel_time = ttfb.tcp_connect_duration().relative().as_secs_f64() * 1000.0,
abs_time = ttfb.tcp_connect_duration().total().as_secs_f64() * 1000.0,
);
if let Some(duration_pair) = ttfb.tls_handshake_duration() {
println!(
"{property:<14}: {rel_time:>13.3} {abs_time:>13.3}",
property = "TLS Handshake",
rel_time = duration_pair.relative().as_secs_f64() * 1000.0,
abs_time = duration_pair.total().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().relative().as_secs_f64() * 1000.0,
abs_time = ttfb.http_get_send_duration().total().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.ttfb_duration().relative().as_secs_f64() * 1000.0,
abs_time = ttfb.ttfb_duration().total().as_secs_f64() * 1000.0,
);
stdout()
.execute(SetAttribute(Attribute::Reset))
.map_err(|err| err.to_string())?;
Ok(())
}