use std::{fs, path::Path};
use corsa::Result;
use serde_json::json;
use crate::{args::Cli, dataset::DatasetCase, runner::ToolRow};
pub fn write(path: &Path, cli: &Cli, datasets: &[DatasetCase], rows: &[ToolRow]) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let datasets = datasets
.iter()
.map(|dataset| {
json!({
"label": dataset.label.as_str(),
"configPath": dataset.config_path,
"fileCount": dataset.file_count,
"totalBytes": dataset.total_bytes,
"totalLines": dataset.total_lines,
"primaryFile": dataset.primary_file.as_str(),
})
})
.collect::<Vec<_>>();
let rows_json = rows
.iter()
.map(|row| {
json!({
"workload": row.workload.as_str(),
"dataset": row.dataset.as_str(),
"tool": row.tool.as_str(),
"sampleCount": row.stats.sample_count(),
"medianMs": row.stats.median_ms(),
"p95Ms": row.stats.p95_ms(),
"p99Ms": row.stats.p99_ms(),
"meanMs": row.stats.mean_ms(),
"stddevMs": row.stats.stddev_ms(),
"cvPercent": row.stats.cv_percent(),
"minMs": row.stats.min_ms(),
"maxMs": row.stats.max_ms(),
})
})
.collect::<Vec<_>>();
fs::write(
path,
serde_json::to_vec_pretty(&json!({
"tsgoPath": cli.tsgo_path,
"nodeCommand": cli.node_command.as_str(),
"iterations": cli.iterations,
"warmupIterations": cli.warmup_iterations,
"timeoutMs": cli.timeout_ms,
"datasets": datasets,
"rows": rows_json,
"projectCheckVsTsgo": project_check_vs_tsgo_json(rows),
"workflowVsTsgoCli": workflow_vs_tsgo_json(rows),
}))?,
)?;
Ok(())
}
pub fn project_check_vs_tsgo_lines(rows: &[ToolRow]) -> Vec<String> {
project_check_vs_tsgo(rows)
.into_iter()
.map(|comparison| {
format!(
"{}\t{}\t{:.3}\t{:.3}\t{:.2}",
comparison.dataset,
comparison.tool,
comparison.median_ms,
comparison.baseline_median_ms,
comparison.vs_baseline_x
)
})
.collect()
}
pub fn workflow_vs_tsgo_lines(rows: &[ToolRow]) -> Vec<String> {
workflow_vs_tsgo(rows)
.into_iter()
.map(|comparison| {
format!(
"{}\t{}\t{:.3}\t{:.3}\t{:.2}",
comparison.dataset,
comparison.tool,
comparison.median_ms,
comparison.baseline_median_ms,
comparison.vs_baseline_x
)
})
.collect()
}
fn project_check_vs_tsgo_json(rows: &[ToolRow]) -> Vec<serde_json::Value> {
project_check_vs_tsgo(rows)
.into_iter()
.map(|comparison| {
json!({
"dataset": comparison.dataset,
"tool": comparison.tool,
"medianMs": comparison.median_ms,
"baselineTool": comparison.baseline_tool,
"baselineMedianMs": comparison.baseline_median_ms,
"vsBaselineX": comparison.vs_baseline_x,
})
})
.collect()
}
fn workflow_vs_tsgo_json(rows: &[ToolRow]) -> Vec<serde_json::Value> {
workflow_vs_tsgo(rows)
.into_iter()
.map(|comparison| {
json!({
"dataset": comparison.dataset,
"tool": comparison.tool,
"medianMs": comparison.median_ms,
"baselineTool": comparison.baseline_tool,
"baselineMedianMs": comparison.baseline_median_ms,
"vsBaselineX": comparison.vs_baseline_x,
})
})
.collect()
}
struct Comparison<'a> {
dataset: &'a str,
tool: &'a str,
baseline_tool: &'a str,
median_ms: f64,
baseline_median_ms: f64,
vs_baseline_x: f64,
}
fn project_check_vs_tsgo(rows: &[ToolRow]) -> Vec<Comparison<'_>> {
let mut items = Vec::new();
for row in rows
.iter()
.filter(|row| row.workload.as_str() == "project_check")
{
let Some(tsgo) = rows.iter().find(|candidate| {
candidate.workload.as_str() == "project_check"
&& candidate.dataset == row.dataset
&& candidate.tool.as_str() == "tsgo"
}) else {
continue;
};
items.push(Comparison {
dataset: row.dataset.as_str(),
tool: row.tool.as_str(),
baseline_tool: "tsgo",
median_ms: row.stats.median_ms(),
baseline_median_ms: tsgo.stats.median_ms(),
vs_baseline_x: tsgo.stats.median_ms() / row.stats.median_ms(),
});
}
sort_comparisons(&mut items);
items
}
fn workflow_vs_tsgo(rows: &[ToolRow]) -> Vec<Comparison<'_>> {
let mut items = Vec::new();
for row in rows
.iter()
.filter(|row| row.workload.as_str() == "editor_workflow")
{
let Some(tsgo) = rows.iter().find(|candidate| {
candidate.workload.as_str() == "project_check"
&& candidate.dataset == row.dataset
&& candidate.tool.as_str() == "tsgo"
}) else {
continue;
};
items.push(Comparison {
dataset: row.dataset.as_str(),
tool: row.tool.as_str(),
baseline_tool: "tsgo-cli-project-check",
median_ms: row.stats.median_ms(),
baseline_median_ms: tsgo.stats.median_ms(),
vs_baseline_x: tsgo.stats.median_ms() / row.stats.median_ms(),
});
}
sort_comparisons(&mut items);
items
}
fn sort_comparisons(items: &mut [Comparison<'_>]) {
items.sort_by(|left, right| {
left.dataset
.cmp(right.dataset)
.then_with(|| left.tool.cmp(right.tool))
});
}