strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use std::io::{IsTerminal, Write};
use std::time::Duration;

use crossterm::{
    cursor, queue,
    style::{Color, Print, ResetColor, SetForegroundColor},
    terminal::{Clear, ClearType},
};
use tokio::time::Instant;

use crate::args::TesterArgs;
use crate::shutdown::ShutdownSender;

/// Width of the progress bar in characters.
const PROGRESS_BAR_WIDTH: usize = 30;
/// Refresh cadence for the progress indicator.
const PROGRESS_TICK_INTERVAL: Duration = Duration::from_millis(250);
/// Minimum target duration to avoid divide-by-zero.
const MIN_TARGET_SECS: u64 = 1;
/// Minimum progress units for bar sizing.
const MIN_PROGRESS_UNITS: usize = 1;
/// Milliseconds per second for time math.
const MS_PER_SEC: u128 = 1000;
/// Milliseconds per tenth-second.
const TENTH_MS: u128 = 100;
/// Number of tenths in a second.
const TENTHS_PER_SEC: u128 = 10;
/// Percent scaling factor (x100 = 10_000).
const PERCENT_SCALE: u128 = 10_000;
/// Percent divisor to format x100 values.
const PERCENT_DIVISOR: u128 = 100;

pub(crate) fn setup_progress_indicator(
    args: &TesterArgs,
    run_start: Instant,
    shutdown_tx: &ShutdownSender,
) -> tokio::task::JoinHandle<()> {
    let mut shutdown_rx = shutdown_tx.subscribe();
    let target_secs = args.target_duration.get();
    let goal = usize::try_from(target_secs.max(MIN_TARGET_SECS)).unwrap_or(MIN_PROGRESS_UNITS);
    let style = ProgressStyle::new(PROGRESS_BAR_WIDTH);
    let no_color = args.no_color;

    tokio::spawn(async move {
        if !std::io::stderr().is_terminal() {
            return;
        }

        let mut ticker = tokio::time::interval(PROGRESS_TICK_INTERVAL);

        loop {
            tokio::select! {
                _ = shutdown_rx.recv() => {
                    let elapsed_ms = u128::from(target_secs).saturating_mul(MS_PER_SEC);
                    if render_progress_line(&style, goal, elapsed_ms, no_color).is_err() {
                        break;
                    }
                    if finish_progress_line().is_err() {
                        break;
                    }
                    break;
                }
                _ = ticker.tick() => {
                    let elapsed_ms = run_start.elapsed().as_millis();
                    if render_progress_line(&style, goal, elapsed_ms, no_color).is_err() {
                        break;
                    }
                }
            }
        }
    })
}

fn render_progress_line(
    style: &ProgressStyle,
    goal: usize,
    elapsed_ms: u128,
    no_color: bool,
) -> Result<(), std::io::Error> {
    let elapsed_secs = elapsed_ms.checked_div(MS_PER_SEC).unwrap_or(0);
    let current = usize::try_from(elapsed_secs).unwrap_or(goal);
    let current = current.min(goal);
    let line = build_progress_line(style, current, goal, elapsed_ms, no_color);

    let mut out = std::io::stderr();
    queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine))?;
    for segment in line {
        if no_color {
            queue!(out, Print(&segment.text))?;
        } else if let Some(color) = segment.color {
            queue!(
                out,
                SetForegroundColor(color),
                Print(&segment.text),
                ResetColor
            )?;
        } else {
            queue!(out, Print(&segment.text))?;
        }
    }
    out.flush()?;
    Ok(())
}

fn finish_progress_line() -> Result<(), std::io::Error> {
    let mut out = std::io::stderr();
    out.write_all(b"\n")?;
    out.flush()?;
    Ok(())
}

fn build_progress_line(
    style: &ProgressStyle,
    current: usize,
    goal: usize,
    elapsed_ms: u128,
    no_color: bool,
) -> Vec<ProgressSegment> {
    let size = style.size.max(MIN_PROGRESS_UNITS);
    let goal = goal.max(MIN_PROGRESS_UNITS);
    let current = current.min(goal);

    let current_u128 = u128::from(u64::try_from(current).unwrap_or(u64::MAX));
    let size_u128 = u128::from(u64::try_from(size).unwrap_or(u64::MAX));
    let goal_u128 = u128::from(u64::try_from(goal).unwrap_or(u64::MAX));

    let scaled = current_u128
        .saturating_mul(size_u128)
        .checked_div(goal_u128)
        .unwrap_or(0);
    let complete_size = usize::try_from(scaled).unwrap_or(size).min(size);
    let incomplete_size = size.saturating_sub(complete_size);

    let percent_x100 = current_u128
        .saturating_mul(PERCENT_SCALE)
        .checked_div(goal_u128)
        .unwrap_or(0);
    let percent_whole = percent_x100.checked_div(PERCENT_DIVISOR).unwrap_or(0);
    let percent_frac = percent_x100.checked_rem(PERCENT_DIVISOR).unwrap_or(0);
    let percent_text = format!(" {}.{:02}%", percent_whole, percent_frac);

    let elapsed_tenths = elapsed_ms.checked_div(TENTH_MS).unwrap_or(0);
    let secs = elapsed_tenths.checked_div(TENTHS_PER_SEC).unwrap_or(0);
    let tenths = elapsed_tenths.checked_rem(TENTHS_PER_SEC).unwrap_or(0);
    let time_text = format!(" | {}.{}s / {}s", secs, tenths, goal);

    let progress_bar = format!(
        "{}{}{}{}",
        style.begin,
        style.fill.repeat(complete_size),
        style.empty.repeat(incomplete_size),
        style.end
    );

    if no_color {
        vec![
            ProgressSegment::plain(progress_bar),
            ProgressSegment::plain(percent_text),
            ProgressSegment::plain(time_text),
        ]
    } else {
        vec![
            ProgressSegment::plain(progress_bar),
            ProgressSegment::colored(percent_text, Color::Cyan),
            ProgressSegment::colored(time_text, Color::Yellow),
        ]
    }
}

struct ProgressStyle {
    size: usize,
    begin: String,
    end: String,
    fill: String,
    empty: String,
}

impl ProgressStyle {
    fn new(size: usize) -> Self {
        Self {
            size,
            begin: "[".to_owned(),
            end: "]".to_owned(),
            fill: "#".to_owned(),
            empty: "-".to_owned(),
        }
    }
}

struct ProgressSegment {
    text: String,
    color: Option<Color>,
}

impl ProgressSegment {
    const fn plain(text: String) -> Self {
        Self { text, color: None }
    }

    const fn colored(text: String, color: Color) -> Self {
        Self {
            text,
            color: Some(color),
        }
    }
}