use lobe_core::engine::burst::BurstSummary;
use lobe_core::engine::history::RegressionSummary;
use lobe_core::models::{ProbeResult, ProbeStatus, TlsStatus};
pub fn print_probe_result(result: &ProbeResult) {
println!("Run #{}", result.id);
println!("target: {}", result.target);
println!("status: {}", format_status(result.status));
println!(
"timing: dns={}ms tcp={}ms tls={}ms ttfb={}ms download={}ms total={}ms",
result.report.dns_ms,
result.report.tcp_ms,
result.report.tls_ms,
result.report.ttfb_ms,
result.report.download_ms,
result.report.total_ms
);
if let Some(tls) = &result.tls {
println!(
"tls: {}{}{}",
format_tls_status(tls.status),
tls.version
.as_deref()
.map(|value| format!(" version={value}"))
.unwrap_or_default(),
tls.cipher_suite
.as_deref()
.map(|value| format!(" cipher={value}"))
.unwrap_or_default()
);
}
if let Some(error_message) = &result.error_message {
println!("error: {error_message}");
}
}
pub fn print_history(results: &[ProbeResult]) {
if results.is_empty() {
println!("No saved runs found.");
return;
}
for result in results {
println!(
"#{} [{}] {} total={}ms ttfb={}ms created_at_ms={}",
result.id,
format_status(result.status),
result.target,
result.report.total_ms,
result.report.ttfb_ms,
result.created_at_ms
);
}
}
pub fn print_comparison(current: &ProbeResult, previous: &ProbeResult, summary: &RegressionSummary) {
println!("target: {}", current.target);
println!(
"current: run #{} total={}ms ttfb={}ms",
current.id, current.report.total_ms, current.report.ttfb_ms
);
println!(
"previous: run #{} total={}ms ttfb={}ms",
previous.id, previous.report.total_ms, previous.report.ttfb_ms
);
println!(
"delta: {}ms ({:.1}%) threshold={:.1}% status={}",
summary.delta_ms,
summary.delta_percent,
summary.threshold_percent,
if summary.exceeded_threshold {
"regression"
} else {
"within-threshold"
}
);
}
pub fn print_comparison_unavailable(target: &str) {
println!("Not enough saved runs to compare for target: {target}");
}
pub fn print_zero_baseline_notice(target: &str) {
println!("Cannot compare target {target} because the previous run had 0ms TTFB.");
}
pub fn print_watch_result(
result: &ProbeResult,
comparison: Option<&RegressionSummary>,
comparison_note: Option<&str>,
) {
print!(
"#{} [{}] total={}ms ttfb={}ms download={}ms dns={}ms tcp={}ms tls={}ms",
result.id,
format_status(result.status),
result.report.total_ms,
result.report.ttfb_ms,
result.report.download_ms,
result.report.dns_ms,
result.report.tcp_ms,
result.report.tls_ms
);
if let Some(tls) = &result.tls {
print!(" tls_status={}", format_tls_status(tls.status));
}
if let Some(summary) = comparison {
print!(
" delta_ttfb={}ms ({:.1}%) {}",
summary.delta_ms,
summary.delta_percent,
if summary.exceeded_threshold {
"REGRESSION"
} else {
"ok"
}
);
}
if let Some(note) = comparison_note {
print!(" note={note}");
}
if let Some(error_message) = &result.error_message {
print!(" error={error_message}");
}
println!();
}
pub fn print_burst_summary(summary: &BurstSummary) {
println!(
"── concurrency {} · {} requests · {:.1}s ──",
summary.concurrency,
summary.total_requests,
summary.elapsed_ms as f64 / 1000.0
);
print!("ok: {}", summary.succeeded);
if summary.failed > 0 {
print!(" failed: {}", summary.failed);
if let Some(error) = &summary.first_error {
print!(" (first error: {error})");
}
}
println!();
let phase_line = |label: &str, phase: &lobe_core::engine::burst::PhasePercentiles| {
println!(
"{label:<10} p50 {:>5}ms p95 {:>5}ms",
phase.p50_ms, phase.p95_ms
);
};
phase_line("ttfb", &summary.ttfb);
phase_line("total", &summary.total);
phase_line("dns", &summary.dns);
phase_line("tcp", &summary.tcp);
phase_line("tls", &summary.tls);
if let Some(detection) = &summary.bimodal_ttfb {
println!(
"bimodal ttfb: fast cohort {}ms ×{} ({:.0}%) / slow cohort {}ms ×{} ({:.0}%) — {:.1}× split",
detection.fast.median_ms,
detection.fast.count,
detection.fast.share * 100.0,
detection.slow.median_ms,
detection.slow.count,
detection.slow.share * 100.0,
detection.ratio
);
println!(
" a fast/slow cohort split under contention usually means a bounded resource \
(connection pool, worker pool) is saturated: the fast cohort got a slot, the slow cohort queued."
);
}
println!();
}
pub fn print_sweep_table(summaries: &[BurstSummary]) {
const BAR_WIDTH: usize = 28;
let max_p95 = summaries
.iter()
.map(|summary| summary.ttfb.p95_ms)
.max()
.unwrap_or(0)
.max(1);
println!("── ttfb vs concurrency ──");
println!("{:>6} {:>8} {:>8}", "conc", "p50", "p95");
for summary in summaries {
let filled =
((summary.ttfb.p95_ms as f64 / max_p95 as f64) * BAR_WIDTH as f64).ceil() as usize;
let bar = "█".repeat(filled.min(BAR_WIDTH));
let split = if summary.bimodal_ttfb.is_some() {
" ← bimodal"
} else {
""
};
println!(
"{:>6} {:>6}ms {:>6}ms {bar}{split}",
summary.concurrency, summary.ttfb.p50_ms, summary.ttfb.p95_ms
);
}
if let Some(first_bimodal) = summaries
.iter()
.find(|summary| summary.bimodal_ttfb.is_some())
{
println!(
"ttfb splits into fast/slow cohorts at concurrency {} — likely where a pool saturates.",
first_bimodal.concurrency
);
}
}
fn format_status(status: ProbeStatus) -> &'static str {
match status {
ProbeStatus::Succeeded => "succeeded",
ProbeStatus::Failed => "failed",
}
}
fn format_tls_status(status: TlsStatus) -> &'static str {
match status {
TlsStatus::NotUsed => "not-used",
TlsStatus::HandshakeSucceeded => "handshake-succeeded",
TlsStatus::HandshakeFailed => "handshake-failed",
}
}