cli_denoiser/bench/
mod.rs1mod corpus;
2
3use std::time::Instant;
4
5use crate::filters::ansi::AnsiFilter;
6use crate::filters::cargo::CargoFilter;
7use crate::filters::dedup::DedupFilter;
8use crate::filters::docker::DockerFilter;
9use crate::filters::generic::GenericFilter;
10use crate::filters::git::GitFilter;
11use crate::filters::kubectl::KubectlFilter;
12use crate::filters::npm::NpmFilter;
13use crate::filters::progress::ProgressFilter;
14use crate::filters::{CommandKind, Filter};
15use crate::pipeline::Pipeline;
16
17#[derive(Debug, serde::Serialize)]
19pub struct BenchResults {
20 pub version: String,
21 pub timestamp: String,
22 pub scenarios: Vec<ScenarioResult>,
23 pub totals: Totals,
24}
25
26#[derive(Debug, serde::Serialize)]
28pub struct ScenarioResult {
29 pub name: String,
30 pub command_kind: String,
31 pub original_tokens: usize,
32 pub filtered_tokens: usize,
33 pub savings_tokens: usize,
34 #[serde(serialize_with = "serialize_f64")]
35 pub savings_percent: f64,
36 #[serde(serialize_with = "serialize_f64")]
37 pub latency_us: f64,
38 pub original_lines: usize,
39 pub filtered_lines: usize,
40 pub signal_preserved: bool,
41}
42
43#[derive(Debug, serde::Serialize)]
45pub struct Totals {
46 pub total_original_tokens: usize,
47 pub total_filtered_tokens: usize,
48 pub total_savings: usize,
49 #[serde(serialize_with = "serialize_f64")]
50 pub overall_savings_percent: f64,
51 #[serde(serialize_with = "serialize_f64")]
52 pub avg_latency_us: f64,
53 pub zero_false_positives: bool,
54}
55
56#[allow(clippy::trivially_copy_pass_by_ref)]
57fn serialize_f64<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
58where
59 S: serde::Serializer,
60{
61 serializer.serialize_f64((value * 10.0).round() / 10.0)
62}
63
64#[must_use]
66pub fn run_all() -> BenchResults {
67 let scenarios_def = corpus::all_scenarios();
68 let mut results = Vec::with_capacity(scenarios_def.len());
69
70 for scenario in &scenarios_def {
71 let pipeline = build_bench_pipeline(&scenario.kind);
72
73 let start = Instant::now();
74 let pipe_result = pipeline.process(&scenario.input);
75 let elapsed = start.elapsed();
76
77 let original_lines = scenario.input.lines().count();
78 let filtered_lines = pipe_result.output.lines().count();
79
80 let signal_preserved = scenario
82 .required_signals
83 .iter()
84 .all(|sig| pipe_result.output.contains(sig));
85
86 results.push(ScenarioResult {
87 name: scenario.name.clone(),
88 command_kind: format!("{:?}", scenario.kind),
89 original_tokens: pipe_result.original_tokens,
90 filtered_tokens: pipe_result.filtered_tokens,
91 savings_tokens: pipe_result.savings,
92 savings_percent: pipe_result.savings_percent(),
93 #[allow(clippy::cast_precision_loss)]
94 latency_us: elapsed.as_micros() as f64,
95 original_lines,
96 filtered_lines,
97 signal_preserved,
98 });
99 }
100
101 let total_original: usize = results.iter().map(|r| r.original_tokens).sum();
102 let total_filtered: usize = results.iter().map(|r| r.filtered_tokens).sum();
103 let total_savings = total_original.saturating_sub(total_filtered);
104 let avg_latency = if results.is_empty() {
105 0.0
106 } else {
107 #[allow(clippy::cast_precision_loss)]
108 {
109 results.iter().map(|r| r.latency_us).sum::<f64>() / results.len() as f64
110 }
111 };
112
113 #[allow(clippy::cast_precision_loss)]
114 let overall_pct = if total_original == 0 {
115 0.0
116 } else {
117 (total_savings as f64 / total_original as f64) * 100.0
118 };
119
120 BenchResults {
121 version: env!("CARGO_PKG_VERSION").to_string(),
122 timestamp: chrono::Utc::now().to_rfc3339(),
123 scenarios: results,
124 totals: Totals {
125 total_original_tokens: total_original,
126 total_filtered_tokens: total_filtered,
127 total_savings,
128 overall_savings_percent: overall_pct,
129 avg_latency_us: avg_latency,
130 zero_false_positives: scenarios_def
131 .iter()
132 .zip(
133 scenarios_def.iter().map(|s| {
135 let p = build_bench_pipeline(&s.kind);
136 let r = p.process(&s.input);
137 s.required_signals.iter().all(|sig| r.output.contains(sig))
138 }),
139 )
140 .all(|(_, preserved)| preserved),
141 },
142 }
143}
144
145pub fn print_summary(results: &BenchResults) {
147 println!("cli-denoiser benchmark v{}\n", results.version);
148 println!(
149 " {:<30} {:>8} {:>8} {:>7} {:>8} {:>6}",
150 "Scenario", "Original", "Filtered", "Saved", "Savings", "Signal"
151 );
152 println!(" {}", "-".repeat(75));
153
154 for s in &results.scenarios {
155 let signal = if s.signal_preserved { "OK" } else { "FAIL" };
156 println!(
157 " {:<30} {:>8} {:>8} {:>7} {:>7.1}% {:>6}",
158 s.name,
159 s.original_tokens,
160 s.filtered_tokens,
161 s.savings_tokens,
162 s.savings_percent,
163 signal
164 );
165 }
166
167 println!(" {}", "-".repeat(75));
168 println!(
169 " {:<30} {:>8} {:>8} {:>7} {:>7.1}% {:>6}",
170 "TOTAL",
171 results.totals.total_original_tokens,
172 results.totals.total_filtered_tokens,
173 results.totals.total_savings,
174 results.totals.overall_savings_percent,
175 if results.totals.zero_false_positives {
176 "OK"
177 } else {
178 "FAIL"
179 }
180 );
181 println!(
182 "\n Avg latency: {:.0}us | Zero false positives: {}",
183 results.totals.avg_latency_us, results.totals.zero_false_positives
184 );
185}
186
187fn build_bench_pipeline(kind: &CommandKind) -> Pipeline {
188 let mut pipeline = Pipeline::new();
189 pipeline.add_filter(Box::new(AnsiFilter));
190 pipeline.add_filter(Box::new(ProgressFilter));
191 pipeline.add_filter(Box::new(DedupFilter::new()));
192
193 let specific: Box<dyn Filter> = match kind {
194 CommandKind::Git => Box::new(GitFilter),
195 CommandKind::Npm => Box::new(NpmFilter),
196 CommandKind::Cargo => Box::new(CargoFilter),
197 CommandKind::Docker => Box::new(DockerFilter),
198 CommandKind::Kubectl => Box::new(KubectlFilter),
199 CommandKind::Unknown => Box::new(GenericFilter),
200 };
201 pipeline.add_filter(specific);
202 pipeline
203}