use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use pil2_pilout::pilout as pb;
use prost::Message;
use crate::pil::prepare::PrepareOptions;
use crate::types::stark_struct::{generate_stark_struct, StarkStructsConfig};
pub struct StatsOptions {
pub airout_path: String,
pub hash: String,
pub output_path: Option<String>,
pub stark_structs_path: Option<String>,
pub airgroups: Vec<String>,
pub airs: Vec<String>,
pub im_pols_stages: bool,
pub blake3_lanes: usize,
}
pub fn run_stats(opts: &StatsOptions) -> Result<()> {
let pilout_data = fs::read(&opts.airout_path)?;
let pilout = pb::PilOut::decode(pilout_data.as_slice())?;
let settings_map: StarkStructsConfig = if let Some(ref settings_path) = opts.stark_structs_path {
let data = fs::read_to_string(settings_path)?;
StarkStructsConfig::from_json_str(&data)?
} else {
StarkStructsConfig::default()
};
let output_path = opts.output_path.clone().unwrap_or_else(|| "tmp/stats.txt".to_string());
if let Some(parent) = PathBuf::from(&output_path).parent() {
fs::create_dir_all(parent)?;
}
let mut stats_lines: Vec<String> = Vec::new();
let mut summary_lines: Vec<String> = Vec::new();
for (ag_idx, airgroup) in pilout.air_groups.iter().enumerate() {
let airgroup_name = airgroup.name.clone().unwrap_or_else(|| format!("airgroup_{}", ag_idx));
if !opts.airgroups.is_empty() && !opts.airgroups.contains(&airgroup_name) {
tracing::info!("Skipping airgroup '{}'", airgroup_name);
continue;
}
for (air_idx, air) in airgroup.airs.iter().enumerate() {
let air_name = air.name.clone().unwrap_or_else(|| format!("air_{}", air_idx));
if !opts.airs.is_empty() && !opts.airs.contains(&air_name) {
tracing::info!("Skipping air '{}'", air_name);
continue;
}
let num_rows = air.num_rows.unwrap_or(0) as usize;
if num_rows == 0 {
tracing::warn!("Skipping air '{}' with numRows=0", air_name);
continue;
}
let n_bits = log2_usize(num_rows);
let air_settings = settings_map.resolve(&airgroup_name, &air_name);
let stark_struct = generate_stark_struct(&air_settings, n_bits, &opts.hash);
let prepare_opts = PrepareOptions { debug: false, im_pols_stages: opts.im_pols_stages };
tracing::info!("Computing stats for air '{}'", air_name);
let pil_result = crate::pil::info::pil_info(&pilout, ag_idx, air_idx, &stark_struct, &prepare_opts);
let family = opts.hash.as_str();
let geom = crate::verifier_hashes::geometry_for_family(
&stark_struct,
&pil_result.setup,
pil_result.pil_code.ev_map.len(),
);
let counts = crate::verifier_hashes::verifier_hashes(&geom, family);
let fit = (family == "blake3").then(|| {
let f = crate::verifier_hashes::blake3_recursion_fit(&counts, opts.blake3_lanes);
(f, if f.needs_compressor { "yes" } else { "no" })
});
summary_lines.push(format!(
"{} | {} | {} | verifierHashes: {}{}",
airgroup_name,
air_name,
pil_result.summary,
counts.total(),
fit.map(|(_, n)| format!(" | needsCompressor: {n}")).unwrap_or_default(),
));
stats_lines.push(format!("Airgroup: {} Air: {}", airgroup_name, air_name));
stats_lines.push(format!("Summary: {}", pil_result.summary));
stats_lines.push(format!("Verifier hashes ({family}):"));
stats_lines.push(format!(
" total: {:>9} | leaf: {:>8} merkle: {:>8} fri: {:>8} transcript: {:>5} grinding: {} \
(arity {}, {} queries, llv {}, {} grinding bits, standalone transcript)",
counts.total(),
counts.leaf,
counts.merkle,
counts.fri,
counts.transcript,
counts.grinding,
geom.arity,
geom.n_queries,
geom.last_level_verification,
geom.pow_bits,
));
if let Some((f, n)) = fit {
stats_lines.push(format!(
" needsCompressor: {n:>3} ({} of {} hashing blocks at LANES={}; hashing only, a \
plonk-dominated band can still size the air)",
f.blocks, f.capacity, opts.blake3_lanes,
));
}
let (base_field, extended_field) = &pil_result.im_pols_info;
if !base_field.is_empty() {
stats_lines.push("Intermediate polynomials baseField:".to_string());
for pol in base_field {
stats_lines.push(format!(" {}", pol));
}
}
if !extended_field.is_empty() {
stats_lines.push("Intermediate polynomials extendedField:".to_string());
for pol in extended_field {
stats_lines.push(format!(" {}", pol));
}
}
stats_lines.push(String::new());
}
}
println!("-------------------------- SUMMARY -------------------------");
for line in &summary_lines {
println!("{}", line);
}
println!("------------------------------------------------------------");
fs::write(&output_path, stats_lines.join("\n"))?;
println!("Stats written to {}", output_path);
Ok(())
}
fn log2_usize(n: usize) -> usize {
assert!(n > 0, "log2_usize: n must be positive");
(usize::BITS - 1 - n.leading_zeros()) as usize
}