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(())
}