zv 0.14.0

Ziglang Version Manager and Project Starter
use crate::{
    App, Result, ZvError,
    app::{
        CacheStrategy,
        network::mirror::{MirrorBenchmarkResult, MirrorManager, RankApplyPolicy},
        utils::{host_target, zig_tarball},
    },
};
use color_eyre::eyre::eyre;
use dialoguer::{Select, theme::ColorfulTheme};
use yansi::Paint;

const DEFAULT_SAMPLE_SIZE_MIB: u64 = 4;
const DEFAULT_CONCURRENCY: usize = 4;

pub async fn speed(
    mut app: App,
    refresh: bool,
    concurrency: usize,
    sample_size: u64,
    json: bool,
) -> Result<()> {
    let cache_strategy = if refresh {
        CacheStrategy::AlwaysRefresh
    } else {
        CacheStrategy::PreferCache
    };

    tokio::fs::create_dir_all(&app.paths.cache_dir)
        .await
        .map_err(ZvError::Io)?;
    let mut mirror_manager =
        MirrorManager::init_and_load(app.paths.mirrors_file.clone(), cache_strategy)
            .await
            .map_err(ZvError::NetworkError)?;

    let release = app.fetch_latest_version(cache_strategy).await?;
    let semver_version = release.resolved_version().version().clone();
    let host_target = host_target().ok_or_else(|| eyre!("Could not determine host target"))?;
    let artifact = release.target_artifact(&host_target).ok_or_else(|| {
        eyre!(
            "No download artifact found for target <{}> in release {}",
            host_target,
            release.version_string()
        )
    })?;
    let zig_tarball = zig_tarball(&semver_version, None).ok_or_else(|| {
        eyre!(
            "Could not determine tarball name for Zig version {}",
            semver_version
        )
    })?;
    let requested_sample_size = sample_size.max(1).saturating_mul(1024 * 1024);
    let sample_size = requested_sample_size.min(artifact.size);
    let concurrency = concurrency.max(1);

    if !json {
        println!(
            "{} {} using up to {:.1} MiB per mirror",
            Paint::new("Benchmarking mirrors for").italic(),
            Paint::cyan(&zig_tarball),
            sample_size as f64 / 1_048_576.0
        );
    }

    let results = mirror_manager
        .benchmark_mirrors(&semver_version, &zig_tarball, sample_size, concurrency)
        .await
        .map_err(ZvError::NetworkError)?;

    if json {
        println!("{}", serde_json::to_string_pretty(&results)?);
        return Ok(());
    }

    print_results(&results);

    let Some(policy) = prompt_application_policy()? else {
        println!("{}", Paint::yellow("No changes saved.").italic());
        return Ok(());
    };

    let applied_label = match policy {
        RankApplyPolicy::Overwrite => "overwritten",
        RankApplyPolicy::Blend => "blended",
    };

    mirror_manager
        .apply_benchmark_results(&results, policy)
        .await
        .map_err(ZvError::NetworkError)?;

    println!(
        "{}",
        Paint::green(&format!("Mirror ranks {applied_label} and saved.")).bold()
    );

    Ok(())
}

pub fn default_sample_size() -> u64 {
    DEFAULT_SAMPLE_SIZE_MIB
}

pub fn default_concurrency() -> usize {
    DEFAULT_CONCURRENCY
}

fn print_results(results: &[MirrorBenchmarkResult]) {
    println!();
    println!("{}", "Mirror benchmark results:".italic());
    println!();

    for (idx, result) in results.iter().enumerate() {
        let rank = format!("#{}", idx + 1);
        let rank_display = if result.is_success() {
            Paint::green(&rank).bold().to_string()
        } else {
            Paint::red(&rank).to_string()
        };

        match result.bytes_per_second {
            Some(bytes_per_second) => {
                let mib_per_second = bytes_per_second / 1_048_576.0;
                let layout = result
                    .measured_layout
                    .map(|layout| format!("{layout:?}").to_lowercase())
                    .unwrap_or_else(|| "unknown".to_string());
                println!(
                    "  {} {:>8.2} MiB/s  old rank #{:<3} {} ({})",
                    rank_display,
                    mib_per_second,
                    result.old_rank,
                    result.base_url,
                    Paint::cyan(&layout).italic()
                );
            }
            None => {
                println!(
                    "  {} {:>8}        old rank #{:<3} {} ({})",
                    rank_display,
                    "failed",
                    result.old_rank,
                    result.base_url,
                    Paint::red(result.error.as_deref().unwrap_or("unknown error")).italic()
                );
            }
        }
    }

    println!();
}

fn prompt_application_policy() -> Result<Option<RankApplyPolicy>> {
    if !crate::tools::supports_interactive_prompts() {
        println!(
            "{}",
            Paint::yellow("Interactive prompt unavailable; leaving mirrors.toml unchanged.")
                .italic()
        );
        return Ok(None);
    }

    let options = [
        "Overwrite all ranks with benchmark order (default)",
        "Blend benchmark order with existing ranks",
        "Do nothing and exit",
    ];
    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Apply benchmark results?")
        .items(options)
        .default(0)
        .interact()
        .map_err(|e| ZvError::General(eyre!(e)))?;

    Ok(match selection {
        0 => Some(RankApplyPolicy::Overwrite),
        1 => Some(RankApplyPolicy::Blend),
        2 => None,
        _ => unreachable!("invalid dialoguer selection"),
    })
}