use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::engine::Progress;
use crate::stats::{BenchResult, Percentiles, PhaseDetails};
const PROGRESS_LINES: usize = 6;
pub async fn render_progress(
progress: Arc<Progress>,
total: usize,
is_duration_mode: bool,
stop: Arc<AtomicBool>,
) {
if !std::io::IsTerminal::is_terminal(&std::io::stderr()) {
loop {
tokio::time::sleep(Duration::from_millis(100)).await;
if stop.load(Ordering::Relaxed) {
return;
}
}
}
let start = Instant::now();
let mut interval = tokio::time::interval(Duration::from_millis(100));
let mut first = true;
eprint!("\x1b[?25l");
let _ = std::io::stderr().flush();
loop {
interval.tick().await;
if stop.load(Ordering::Relaxed) {
break;
}
let completed = progress.completed.load(Ordering::Relaxed) as usize;
let errors = progress.errors.load(Ordering::Relaxed);
let total_ns = progress.total_ns.load(Ordering::Relaxed);
let elapsed = start.elapsed();
if !first {
eprint!("\x1b[{}A", PROGRESS_LINES);
}
first = false;
let rps = if elapsed.as_secs_f64() > 0.001 {
completed as f64 / elapsed.as_secs_f64()
} else {
0.0
};
let avg_ms = if completed > 0 {
total_ns as f64 / completed as f64 / 1_000_000.0
} else {
0.0
};
eprintln!("\x1b[2K vastar -- running");
eprintln!("\x1b[2K");
if is_duration_mode {
eprintln!(
"\x1b[2K Requests: {} Errors: {}",
completed, errors
);
} else {
let pct = if total > 0 { completed * 100 / total } else { 0 };
let bar_width = 40;
let filled = (pct * bar_width / 100).min(bar_width);
let bar = "=".repeat(filled);
let arrow = if filled < bar_width { ">" } else { "" };
let space = " ".repeat(bar_width.saturating_sub(filled + if filled < bar_width { 1 } else { 0 }));
eprintln!(
"\x1b[2K [{}{}{}] {}% {}/{}",
bar, arrow, space, pct, completed, total
);
}
eprintln!(
"\x1b[2K Elapsed {:.1}s RPS {:.0}/s Avg {:.2}ms",
elapsed.as_secs_f64(),
rps,
avg_ms
);
if errors > 0 {
eprintln!("\x1b[2K Errors: {}", errors);
} else {
eprintln!("\x1b[2K");
}
eprintln!("\x1b[2K");
let _ = std::io::stderr().flush();
}
if !first {
eprint!("\x1b[{}A", PROGRESS_LINES);
for _ in 0..PROGRESS_LINES {
eprintln!("\x1b[2K");
}
eprint!("\x1b[{}A", PROGRESS_LINES);
}
eprint!("\x1b[?25h");
let _ = std::io::stderr().flush();
}
pub fn print_report(r: &BenchResult) {
println!();
println!("Summary:");
println!(" Total: {:.4} secs", r.total_duration.as_secs_f64());
println!(" Slowest: {:.4} secs", r.max_latency);
println!(" Fastest: {:.4} secs", r.min_latency);
println!(" Average: {:.4} secs", r.avg_latency);
println!(" Requests/sec: {:.2}", r.rps);
if r.total_bytes > 0 {
let size_per = if r.total_requests > 0 {
r.total_bytes / r.total_requests as u64
} else {
0
};
println!(" Total data: {} bytes", r.total_bytes);
println!(" Size/request: {} bytes", size_per);
}
println!();
let use_color = std::io::IsTerminal::is_terminal(&std::io::stdout());
println!("Response time distribution:");
let pcts: &[(&str, f64, bool)] = &[
("10.00%", r.percentiles.p10, false),
("25.00%", r.percentiles.p25, false),
("50.00%", r.percentiles.p50, true), ("75.00%", r.percentiles.p75, false),
("90.00%", r.percentiles.p90, false),
("95.00%", r.percentiles.p95, true), ("99.00%", r.percentiles.p99, true), ("99.90%", r.percentiles.p999, true), ("99.99%", r.percentiles.p9999, false),
];
for (pct, val, key) in pcts {
if use_color {
let (color, _) = slo_color(*val, &r.percentiles);
if *key {
println!(" {} in {}{:.4}{} secs ({}{:.2}ms{})", pct, color, val, RESET, color, val * 1000.0, RESET);
} else {
println!(" {} in {}{:.4}{} secs", pct, color, val, RESET);
}
} else if *key {
println!(" {} in {:.4} secs ({:.2}ms)", pct, val, val * 1000.0);
} else {
println!(" {} in {:.4} secs", pct, val);
}
}
println!();
if !r.histogram.is_empty() {
let use_color = std::io::IsTerminal::is_terminal(&std::io::stdout());
println!("Response time histogram:");
let max_count = r.histogram.iter().map(|b| b.count).max().unwrap_or(1).max(1);
let bar_max = 48;
for bucket in &r.histogram {
let bar_len = bucket.count * bar_max / max_count;
if use_color {
let (color, _) = slo_color(bucket.mark, &r.percentiles);
let bar = "\u{2588}".repeat(bar_len);
println!(" {:.4} [{}]\t{}{}{}", bucket.mark, bucket.count, color, bar, RESET);
} else {
let bar = "#".repeat(bar_len);
println!(" {:.4} [{}]\t{}", bucket.mark, bucket.count, bar);
}
}
if use_color {
println!();
print_slo_legend(&r.percentiles);
}
println!();
}
if !r.status_dist.is_empty() {
println!("Status code distribution:");
let mut codes: Vec<_> = r.status_dist.iter().collect();
codes.sort_by_key(|(k, _)| **k);
for (code, count) in codes {
println!(" [{}] {} responses", code, count);
}
println!();
}
print_details(&r.details);
if r.total_errors > 0 {
println!("Errors: {} total", r.total_errors);
println!();
}
let use_color = std::io::IsTerminal::is_terminal(&std::io::stdout());
if use_color {
print_insight(&r.percentiles, r.rps, r.concurrency, r.avg_latency);
}
println!();
}
fn print_insight(p: &Percentiles, rps: f64, concurrency: usize, avg_latency: f64) {
let p99_p95 = if p.p95 > 0.0 { p.p99 / p.p95 } else { 1.0 };
let p999_p99 = if p.p99 > 0.0 { p.p999 / p.p99 } else { 1.0 };
let spread = if p.p50 > 0.0 { p.p99 / p.p50 } else { 1.0 };
let p95_p50 = if p.p50 > 0.0 { p.p95 / p.p50 } else { 1.0 };
println!("Insight:");
if spread <= 1.5 {
println!(" Latency spread p99/p50 = {:.1}x -- excellent consistency", spread);
} else if spread <= 3.0 {
println!(" Latency spread p99/p50 = {:.1}x -- good consistency", spread);
} else if spread <= 5.0 {
println!(" Latency spread p99/p50 = {:.1}x -- moderate variance", spread);
} else {
println!(" Latency spread p99/p50 = {:.1}x -- high variance, investigate slow path", spread);
}
if p99_p95 > 2.0 {
println!(" Tail ratio p99/p95 = {:.1}x -- tail latency problem (>2x)", p99_p95);
} else if p99_p95 > 1.5 {
println!(" Tail ratio p99/p95 = {:.1}x -- mild tail latency", p99_p95);
} else {
println!(" Tail ratio p99/p95 = {:.1}x -- clean tail", p99_p95);
}
if p999_p99 > 3.0 {
println!(" Outlier ratio p99.9/p99 = {:.1}x -- severe outliers (>3x), check GC/infra", p999_p99);
} else if p999_p99 > 2.0 {
println!(" Outlier ratio p99.9/p99 = {:.1}x -- outliers present", p999_p99);
} else {
println!(" Outlier ratio p99.9/p99 = {:.1}x -- no significant outliers", p999_p99);
}
if p95_p50 > 3.0 {
println!(" Queue ratio p95/p50 = {:.1}x -- queuing/contention detected (>3x)", p95_p50);
} else if p95_p50 > 2.0 {
println!(" Queue ratio p95/p50 = {:.1}x -- mild queuing", p95_p50);
}
if rps > 0.0 && avg_latency > 0.0 {
let base = (rps * avg_latency).round() as usize;
let lo = (base as f64 * 0.8) as usize;
let hi = (base as f64 * 1.3) as usize;
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let c = concurrency;
let saturated = p95_p50 > 2.0;
println!();
println!(" Sweet spot concurrency (Little's Law):");
println!(" C = RPS x avg_latency = {:.0} x {:.4}s = ~{}", rps, avg_latency, base);
println!(" Range: ~{}..{} (client, {} cores)", lo, hi, cpus);
if saturated {
println!(" Status: SATURATED -- queuing detected (p95/p50={:.1}x), reduce below c={}", p95_p50, c);
} else if c > hi {
println!(" Status: HIGH -- current c={}, consider reducing to ~{}..{}", c, lo, hi);
} else if c < lo {
println!(" Status: HEADROOM -- current c={}, can increase to ~{}..{}", c, lo, hi);
} else {
println!(" Status: OPTIMAL (c={} within {}..{})", c, lo, hi);
}
}
}
fn print_details(d: &PhaseDetails) {
println!("Details (average, fastest, slowest):");
println!(" req write:\t{:.4} secs, {:.4} secs, {:.4} secs",
d.req_write.avg, d.req_write.min, d.req_write.max);
println!(" resp wait:\t{:.4} secs, {:.4} secs, {:.4} secs",
d.resp_wait.avg, d.resp_wait.min, d.resp_wait.max);
println!(" resp read:\t{:.4} secs, {:.4} secs, {:.4} secs",
d.resp_read.avg, d.resp_read.min, d.resp_read.max);
println!();
}
const RESET: &str = "\x1b[0m";
const SLO_LEVELS: [(&str, &str); 11] = [
("\x1b[38;5;22m", "elite"), ("\x1b[38;5;28m", "excellent"), ("\x1b[38;5;34m", "good"), ("\x1b[38;5;40m", "normal"), ("\x1b[38;5;82m", "acceptable"), ("\x1b[38;5;154m", "degraded"), ("\x1b[38;5;226m", "slow"), ("\x1b[38;5;214m", "very slow"), ("\x1b[38;5;202m", "critical"), ("\x1b[38;5;160m", "severe"), ("\x1b[38;5;88m", "violation"), ];
fn slo_color(latency: f64, p: &Percentiles) -> (&'static str, &'static str) {
let level = if latency <= p.p25 * 0.5 {
0
} else if latency <= p.p25 {
1
} else if latency <= p.p50 {
2
} else if latency <= p.p75 {
3
} else if latency <= p.p90 {
4
} else if latency <= p.p95 {
5
} else if latency <= p.p99 {
6
} else if latency <= p.p99 * 1.5 {
7
} else if latency <= p.p99 * 2.0 {
8
} else if latency <= p.p99 * 3.0 {
9
} else {
10
};
(SLO_LEVELS[level].0, SLO_LEVELS[level].1)
}
fn print_slo_legend(p: &Percentiles) {
let fmt_ms = |v: f64| -> String {
let ms = v * 1000.0;
if ms < 1.0 { format!("{:.2}ms", ms) }
else if ms < 100.0 { format!("{:.1}ms", ms) }
else { format!("{:.0}ms", ms) }
};
let thresholds: [String; 11] = [
format!("<={}", fmt_ms(p.p25 * 0.5)),
format!("<={}", fmt_ms(p.p25)),
format!("<={}", fmt_ms(p.p50)),
format!("<={}", fmt_ms(p.p75)),
format!("<={}", fmt_ms(p.p90)),
format!("<={}", fmt_ms(p.p95)),
format!("<={}", fmt_ms(p.p99)),
format!("<={}", fmt_ms(p.p99 * 1.5)),
format!("<={}", fmt_ms(p.p99 * 2.0)),
format!("<={}", fmt_ms(p.p99 * 3.0)),
format!(">{}", fmt_ms(p.p99 * 3.0)),
];
const CELL: usize = 26;
let items: Vec<String> = (0..11)
.map(|i| format!("{}\u{2588}\u{2588}{} {:<11}{}", SLO_LEVELS[i].0, RESET, SLO_LEVELS[i].1, thresholds[i]))
.collect();
println!(" SLO:");
for row in items.chunks(3) {
print!(" ");
for item in row {
let visible_len = strip_ansi_len(item);
let pad = if CELL > visible_len { CELL - visible_len } else { 0 };
print!("{}{}", item, " ".repeat(pad));
}
println!();
}
}
fn strip_ansi_len(s: &str) -> usize {
let mut len = 0;
let mut in_esc = false;
for c in s.chars() {
if in_esc {
if c == 'm' { in_esc = false; }
} else if c == '\x1b' {
in_esc = true;
} else {
len += 1;
}
}
len
}