use std::collections::HashMap;
use console::style;
use eyre::Result;
use super::{
CompareArgs, CompareSection,
model::{
CompareOutput, CompareReport, CompareSummary, ImprovementEntry, LogDiffEntry, PnlDiff,
RegressionEntry,
},
symbols::resolve_symbol,
};
const DEFAULT_LIMIT: usize = 10;
pub(super) fn print_json(
args: &CompareArgs,
mut report: CompareReport,
symbols: &HashMap<String, String>,
) -> Result<()> {
report
.pnl_diffs
.iter_mut()
.flat_map(|d| d.tokens.iter_mut())
.for_each(|t| t.symbol = resolve_symbol(&t.mint, symbols).to_string());
let value = match args.section {
Some(CompareSection::Regressions) => serde_json::to_value(&report.regressions)?,
Some(CompareSection::Improvements) => serde_json::to_value(&report.improvements)?,
Some(CompareSection::Logs) => serde_json::to_value(&report.log_diffs)?,
Some(CompareSection::Balances) => serde_json::to_value(&report.pnl_diffs)?,
None => {
let summary = CompareSummary {
regressions: report.regressions.len(),
improvements: report.improvements.len(),
log_diffs: report.log_diffs.len(),
missing: report.missing.len(),
balance_diffs: report.pnl_diffs.len(),
};
serde_json::to_value(CompareOutput {
baseline: args.baseline.display().to_string(),
experiment: args.experiment.display().to_string(),
baseline_transactions: report.baseline_transactions,
experiment_transactions: report.experiment_transactions,
common_transactions: report.common,
balance_diffs: report.pnl_diffs,
regressions: report.regressions,
improvements: report.improvements,
log_diffs: report.log_diffs,
missing: report.missing,
summary,
})?
}
};
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(())
}
pub(super) fn print_text(
args: &CompareArgs,
report: &CompareReport,
symbols: &HashMap<String, String>,
) {
println!(
"Baseline ({}) versus Experiment ({})",
args.baseline.display(),
args.experiment.display(),
);
println!("Baseline transactions: {}", report.baseline_transactions);
println!(
"Experiment transactions: {}",
report.experiment_transactions
);
println!("Common transactions: {}\n", report.common);
match args.section {
Some(CompareSection::Regressions) => {
print_regressions(&report.regressions, None);
}
Some(CompareSection::Improvements) => {
print_improvements(&report.improvements, None);
}
Some(CompareSection::Logs) => {
print_log_diffs(&report.log_diffs, None);
}
Some(CompareSection::Balances) => {
print_pnl_diffs(&report.pnl_diffs, None, symbols);
}
None => {
if report.regressions.is_empty()
&& report.improvements.is_empty()
&& report.log_diffs.is_empty()
&& report.missing.is_empty()
&& report.pnl_diffs.is_empty()
{
println!("{} No differences found", style("✔").green());
}
print_regressions(&report.regressions, Some(DEFAULT_LIMIT));
print_improvements(&report.improvements, Some(DEFAULT_LIMIT));
print_log_diffs(&report.log_diffs, Some(DEFAULT_LIMIT));
print_missing(&report.missing, Some(DEFAULT_LIMIT));
print_pnl_diffs(&report.pnl_diffs, Some(DEFAULT_LIMIT), symbols);
println!(
"Summary: {} regressions, {} improvements, {} log diffs, {} missing, {} balance diffs",
report.regressions.len(),
report.improvements.len(),
report.log_diffs.len(),
report.missing.len(),
report.pnl_diffs.len(),
);
}
}
}
fn print_capped<T>(
items: &[T],
limit: Option<usize>,
header: impl FnOnce(usize),
render: impl Fn(&T),
more_suffix: &str,
) {
if items.is_empty() {
return;
}
header(items.len());
let shown = limit.map_or(items.len(), |l| l.min(items.len()));
items[..shown].iter().for_each(render);
if let Some(limit) = limit
&& items.len() > limit
{
println!(" … and {} more{more_suffix}", items.len() - limit);
}
println!();
}
fn print_regressions(regressions: &[RegressionEntry], limit: Option<usize>) {
print_capped(
regressions,
limit,
|count| {
println!(
"{} Regressions (success → failure): {count}",
style("✖").red()
)
},
|entry| {
println!(" slot {} {}", entry.slot, format(&entry.signature));
if let Some(err) = &entry.error {
println!(" error: {err}");
}
},
" (use `sim compare regressions` to view all)",
);
}
fn print_improvements(improvements: &[ImprovementEntry], limit: Option<usize>) {
print_capped(
improvements,
limit,
|count| {
println!(
"{} Improvements (failure → success): {count}",
style("✔").green()
)
},
|entry| {
println!(" slot {} {}", entry.slot, format(&entry.signature));
if let Some(err) = &entry.was {
println!(" was: {err}");
}
},
" (use `sim compare improvements` to view all)",
);
}
fn print_log_diffs(log_diffs: &[LogDiffEntry], limit: Option<usize>) {
print_capped(
log_diffs,
limit,
|count| println!("{} Log differences: {count}", style("~").yellow()),
|entry| {
println!(
" slot {} {} ({} baseline logs → {} modified logs)",
entry.slot,
format(&entry.signature),
entry.baseline_log_count,
entry.experiment_log_count,
);
},
" (use `sim compare logs` to view all)",
);
}
fn print_missing(missing: &[String], limit: Option<usize>) {
print_capped(
missing,
limit,
|count| println!("{} Missing from experiment: {count}", style("?").dim()),
|signature| println!(" {}", format(signature)),
"",
);
}
pub(super) fn format_pct_change(baseline: i64, experiment: i64) -> String {
if baseline == 0 {
return "new".to_string();
}
format!(
"{:+.4}%",
(experiment - baseline) as f64 / baseline.unsigned_abs() as f64 * 100.0
)
}
fn print_pnl_diffs(pnl_diffs: &[PnlDiff], limit: Option<usize>, symbols: &HashMap<String, String>) {
print_capped(
pnl_diffs,
limit,
|count| println!("{} Balance differences: {count}", style("$").cyan()),
|diff| {
println!(" slot {} {}", diff.slot, format(&diff.signature));
for d in &diff.sol {
let pct = format_pct_change(d.baseline_delta, d.experiment_delta);
println!(
" SOL {} {:+.9} → {:+.9} ({pct})",
format(&d.pubkey),
d.baseline_delta as f64 / 1e9,
d.experiment_delta as f64 / 1e9,
);
}
for d in &diff.tokens {
let scale = 10f64.powi(d.decimals as i32);
let prec = d.decimals as usize;
let ticker = resolve_symbol(&d.mint, symbols);
let pct = format_pct_change(d.baseline_delta, d.experiment_delta);
println!(
" {ticker} {} {:+.prec$} → {:+.prec$} ({pct})",
format(&d.pubkey),
d.baseline_delta as f64 / scale,
d.experiment_delta as f64 / scale,
prec = prec,
);
}
println!();
},
" (use `sim compare balances` to view all)",
);
}
fn format(signature: &str) -> String {
let n = signature.len();
if n <= 16 {
return signature.to_string();
}
format!("{}…{}", &signature[..8], &signature[n - 8..])
}