uhash-cli 0.5.1

UniversalHash proof-of-work miner for Bostrom blockchain
Documentation
use serde::Serialize;
use std::time::{Duration, Instant};
use uhash_prover::cpu::ParallelCpuSolver;
use uhash_prover::Solver;

#[cfg(feature = "gpu-cuda")]
use uhash::cuda_miner;
#[cfg(all(feature = "gpu-metal", target_os = "macos"))]
use uhash::metal_miner;
#[cfg(feature = "gpu-opencl")]
use uhash::opencl_solver;
#[cfg(feature = "gpu-wgpu")]
use uhash::wgpu_solver;

use crate::MiningBackend;

#[derive(Serialize)]
struct JsonBenchmark {
    backend: String,
    threads: usize,
    total_hashes: u32,
    elapsed_s: f64,
    hashrate: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    startup_s: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    steady_elapsed_s: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    steady_hashrate: Option<f64>,
    params: JsonAlgoParams,
    #[serde(skip_serializing_if = "Option::is_none")]
    backend_telemetry: Option<serde_json::Value>,
}

#[derive(Serialize)]
struct JsonAlgoParams {
    chains: usize,
    scratchpad_kb: usize,
    total_mb: usize,
    rounds: usize,
}

pub(crate) fn cmd_benchmark(
    count: u32,
    effective_backend: MiningBackend,
    backend_note: Option<String>,
    threads: Option<usize>,
    json: bool,
) -> anyhow::Result<()> {
    if !json {
        println!(
            "Running benchmark with {} hashes (backend={})...",
            count,
            effective_backend.as_str()
        );
        if let Some(note) = backend_note {
            eprintln!("Warning: {}", note);
        }
    }

    let bench_header = b"benchmark input data for UniversalHash v4";
    let (
        elapsed,
        used_threads,
        backend_telemetry,
        startup_elapsed,
        steady_elapsed,
        steady_hashes,
    ): (
        Duration,
        usize,
        Option<serde_json::Value>,
        Option<Duration>,
        Option<Duration>,
        Option<usize>,
    ) = match effective_backend {
        MiningBackend::Cpu => {
            let worker_threads = threads.filter(|t| *t > 0).unwrap_or_else(num_cpus::get);
            let mut solver = ParallelCpuSolver::new(worker_threads);
            let start = Instant::now();
            solver.benchmark_hashes(bench_header, 0, count as usize)?;
            (
                start.elapsed(),
                worker_threads,
                None,
                None,
                None,
                None,
            )
        }
        MiningBackend::Metal => {
            #[cfg(all(feature = "gpu-metal", target_os = "macos"))]
            {
                let mut miner = metal_miner::MetalMiner::new()?;
                let lanes = miner.recommended_lanes(threads.unwrap_or(0));
                let total_start = Instant::now();
                let mut remaining = count as usize;
                let mut nonce = 0u64;
                let mut warmup_hashes = 0usize;
                if remaining > 0 {
                    let warmup_batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, warmup_batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                    warmup_hashes = done;
                }
                let startup = total_start.elapsed();
                let steady_start = Instant::now();
                while remaining > 0 {
                    let batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                }
                let steady = steady_start.elapsed();
                let total = total_start.elapsed();
                (
                    total,
                    lanes,
                    Some(serde_json::json!(miner.telemetry())),
                    Some(startup),
                    Some(steady),
                    Some((count as usize).saturating_sub(warmup_hashes)),
                )
            }
            #[cfg(not(all(feature = "gpu-metal", target_os = "macos")))]
            {
                anyhow::bail!("metal benchmark requires macOS + --features gpu-metal");
            }
        }
        MiningBackend::Cuda => {
            #[cfg(feature = "gpu-cuda")]
            {
                let mut miner = cuda_miner::CudaMiner::new()?;
                let lanes = miner.recommended_lanes(threads.unwrap_or(0));
                let total_start = Instant::now();
                let mut remaining = count as usize;
                let mut nonce = 0u64;
                let mut warmup_hashes = 0usize;
                if remaining > 0 {
                    let warmup_batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, warmup_batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                    warmup_hashes = done;
                }
                let startup = total_start.elapsed();
                let steady_start = Instant::now();
                while remaining > 0 {
                    let batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                }
                let steady = steady_start.elapsed();
                let total = total_start.elapsed();
                (
                    total,
                    lanes,
                    Some(serde_json::json!(miner.telemetry())),
                    Some(startup),
                    Some(steady),
                    Some((count as usize).saturating_sub(warmup_hashes)),
                )
            }
            #[cfg(not(feature = "gpu-cuda"))]
            {
                anyhow::bail!("cuda benchmark requires --features gpu-cuda");
            }
        }
        MiningBackend::Opencl => {
            #[cfg(feature = "gpu-opencl")]
            {
                let mut miner = opencl_solver::OpenClSolver::new()?;
                let lanes = miner.recommended_lanes(threads.unwrap_or(0));
                let total_start = Instant::now();
                let mut remaining = count as usize;
                let mut nonce = 0u64;
                let mut warmup_hashes = 0usize;
                if remaining > 0 {
                    let warmup_batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, warmup_batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                    warmup_hashes = done;
                }
                let startup = total_start.elapsed();
                let steady_start = Instant::now();
                while remaining > 0 {
                    let batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                }
                let steady = steady_start.elapsed();
                let total = total_start.elapsed();
                (
                    total,
                    lanes,
                    Some(serde_json::json!(miner.telemetry())),
                    Some(startup),
                    Some(steady),
                    Some((count as usize).saturating_sub(warmup_hashes)),
                )
            }
            #[cfg(not(feature = "gpu-opencl"))]
            {
                anyhow::bail!("opencl benchmark requires --features gpu-opencl");
            }
        }
        MiningBackend::Wgpu => {
            #[cfg(feature = "gpu-wgpu")]
            {
                let mut miner = wgpu_solver::WgpuSolver::new()?;
                let lanes = miner.recommended_lanes(threads.unwrap_or(0));
                let total_start = Instant::now();
                let mut remaining = count as usize;
                let mut nonce = 0u64;
                let mut warmup_hashes = 0usize;
                if remaining > 0 {
                    let warmup_batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, warmup_batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                    warmup_hashes = done;
                }
                let startup = total_start.elapsed();
                let steady_start = Instant::now();
                while remaining > 0 {
                    let batch = remaining.min(lanes);
                    let done = miner.benchmark_hashes(bench_header, nonce, batch)?;
                    remaining = remaining.saturating_sub(done);
                    nonce = nonce.saturating_add(done as u64);
                }
                let steady = steady_start.elapsed();
                let total = total_start.elapsed();
                (
                    total,
                    lanes,
                    Some(serde_json::json!(miner.telemetry())),
                    Some(startup),
                    Some(steady),
                    Some((count as usize).saturating_sub(warmup_hashes)),
                )
            }
            #[cfg(not(feature = "gpu-wgpu"))]
            {
                anyhow::bail!("wgpu benchmark requires --features gpu-wgpu");
            }
        }
        MiningBackend::Auto => unreachable!("benchmark backend must be resolved before dispatch"),
    };

    let elapsed_secs = elapsed.as_secs_f64();
    let hashrate = if elapsed_secs > 0.0 {
        count as f64 / elapsed_secs
    } else {
        0.0
    };
    let steady_hashrate = match (steady_elapsed, steady_hashes) {
        (Some(elapsed), Some(hashes)) if elapsed.as_secs_f64() > 0.0 && hashes > 0 => {
            Some(hashes as f64 / elapsed.as_secs_f64())
        }
        _ => None,
    };

    if json {
        let out = JsonBenchmark {
            backend: effective_backend.as_str().to_string(),
            threads: used_threads,
            total_hashes: count,
            elapsed_s: elapsed_secs,
            hashrate,
            startup_s: startup_elapsed.map(|d| d.as_secs_f64()),
            steady_elapsed_s: steady_elapsed.map(|d| d.as_secs_f64()),
            steady_hashrate,
            params: JsonAlgoParams {
                chains: uhash_core::CHAINS,
                scratchpad_kb: uhash_core::SCRATCHPAD_SIZE / 1024,
                total_mb: uhash_core::TOTAL_MEMORY / (1024 * 1024),
                rounds: uhash_core::ROUNDS,
            },
            backend_telemetry,
        };
        println!("{}", serde_json::to_string(&out)?);
    } else {
        println!("\nResults:");
        println!("  Backend: {}", effective_backend.as_str());
        println!("  Threads/batch: {}", used_threads);
        println!("  Total hashes: {}", count);
        println!("  Time elapsed: {:.2}s", elapsed_secs);
        println!("  Hashrate (overall): {:.2} H/s", hashrate);
        if let Some(startup) = startup_elapsed {
            println!("  Startup/warmup: {:.2}s", startup.as_secs_f64());
        }
        if let (Some(steady_elapsed), Some(steady_hps)) = (steady_elapsed, steady_hashrate) {
            println!(
                "  Hashrate (steady-state): {:.2} H/s over {:.2}s",
                steady_hps,
                steady_elapsed.as_secs_f64()
            );
        }
        if let Some(t) = backend_telemetry {
            println!("  Backend telemetry: {}", t);
        }

        println!("\nAlgorithm parameters:");
        println!("  Chains: {}", uhash_core::CHAINS);
        println!(
            "  Memory per chain: {} KB",
            uhash_core::SCRATCHPAD_SIZE / 1024
        );
        println!(
            "  Total memory: {} MB",
            uhash_core::TOTAL_MEMORY / (1024 * 1024)
        );
        println!("  Rounds: {}", uhash_core::ROUNDS);
    }

    Ok(())
}