1use crate::config::CliConfig;
21use chrono::Utc;
22use clap::Args;
23use colored::*;
24use ferrum_bench_core::{
25 compute_metrics, BenchReport, Env, ItlEvidenceSource, OutputTokenCountSource,
26 QualityIssueCounts, RequestItlEvidence, RequestRecord, RunRecord, Scenario, Slo,
27};
28use ferrum_types::{InferenceRequest, Priority, RequestId, Result, SamplingParams};
29use futures::StreamExt;
30use std::collections::HashMap;
31use std::path::PathBuf;
32use std::time::Instant;
33use uuid::Uuid;
34
35#[derive(Args)]
36pub struct BenchCommand {
37 #[arg(default_value = "qwen3:0.6b")]
39 pub model: String,
40
41 #[arg(long, default_value = "5")]
43 pub rounds: usize,
44
45 #[arg(long, default_value = "128")]
47 pub max_tokens: u32,
48
49 #[arg(long, default_value = "auto")]
51 pub backend: String,
52
53 #[arg(long, value_name = "PROFILE")]
55 pub numerical_profile: Option<ferrum_types::NumericalExecutionPolicy>,
56
57 #[arg(long, default_value = "Explain the theory of relativity in detail.")]
59 pub prompt: String,
60
61 #[arg(long, default_value = "1")]
63 pub concurrency: usize,
64
65 #[arg(long)]
67 pub long_context: bool,
68
69 #[arg(long, value_name = "DTYPE")]
71 pub kv_dtype: Option<String>,
72
73 #[arg(long, default_value_t = 1)]
77 pub n_repeats: u32,
78
79 #[arg(long, value_parser = super::bench_serve::parse_slo)]
81 pub goodput: Option<Slo>,
82
83 #[arg(long, default_value = "human")]
86 pub output: String,
87
88 #[arg(long)]
90 pub out: Option<PathBuf>,
91
92 #[arg(long)]
94 pub hw_id: Option<String>,
95
96 #[arg(long)]
98 pub commit_sha: Option<String>,
99}
100
101fn validate_command(cmd: &BenchCommand) -> Result<()> {
102 if cmd.n_repeats == 0 {
103 return Err(ferrum_types::FerrumError::model("--n-repeats must be > 0"));
104 }
105 if cmd.rounds == 0 {
106 return Err(ferrum_types::FerrumError::model("--rounds must be > 0"));
107 }
108 if cmd.concurrency == 0 {
109 return Err(ferrum_types::FerrumError::model(
110 "--concurrency must be > 0",
111 ));
112 }
113 measured_request_count(cmd)?;
114 if cmd.goodput.is_some_and(|slo| !slo.is_valid()) {
115 return Err(ferrum_types::FerrumError::model(
116 "--goodput values must be positive finite numbers",
117 ));
118 }
119 Ok(())
120}
121
122fn measured_request_count(cmd: &BenchCommand) -> Result<u32> {
123 let count = cmd
124 .rounds
125 .checked_mul(cmd.concurrency)
126 .ok_or_else(|| ferrum_types::FerrumError::model("rounds * concurrency overflow"))?;
127 u32::try_from(count).map_err(|_| {
128 ferrum_types::FerrumError::model("rounds * concurrency exceeds report capacity")
129 })
130}
131
132pub async fn execute(cmd: BenchCommand, config: CliConfig) -> Result<()> {
133 validate_command(&cmd)?;
134 let cache_dir = crate::source_resolver::hf_cache_dir(&config);
135 let resolved = crate::source_resolver::resolve_model_source(
136 &cmd.model,
137 &cache_dir,
138 crate::source_resolver::DownloadPolicy::AutoDownload,
139 None,
140 )
141 .await?;
142 let product_input = resolved.into_product_engine_input();
143 let model_id = product_input.public_model_id.clone();
144 let source = product_input.source;
145 let mut engine_config = product_input.engine_config;
146 engine_config.numerical_execution =
147 config.resolve_numerical_execution(cmd.numerical_profile.as_ref());
148 let model_sources = product_input.model_sources;
149 let defined_model = crate::source_resolver::define_registered_product_model(
150 model_sources.as_ref(),
151 &engine_config.numerical_execution,
152 engine_config.kv_cache.dtype,
153 )?;
154 eprintln!("{}", format!("Ferrum Benchmark - {}", model_id).bold());
155 eprintln!("{}", "=".repeat(60).dimmed());
156
157 let engine_model_path = source.local_path.to_string_lossy().to_string();
158
159 let device = super::run::select_device(&cmd.backend)?;
160 let backend_str = format!("{:?}", device).to_lowercase();
161 eprintln!("{} {:?}", "Device:".dimmed(), device);
162 let runtime_config = ferrum_types::RuntimeConfigSnapshot::capture_current();
163
164 #[cfg(feature = "cuda")]
165 {
166 let graph_mode =
167 crate::runtime_env::runtime_snapshot_value(&runtime_config, "FERRUM_CUDA_GRAPH")
168 .is_some();
169 if !graph_mode {
170 if let Ok(name) = ferrum_kernels::cuda_device_name(0) {
171 eprintln!("GPU 0: {name}");
172 }
173 }
174 let tp = crate::runtime_env::runtime_snapshot_value(&runtime_config, "FERRUM_TP")
175 .and_then(|v| v.parse::<usize>().ok())
176 .unwrap_or_else(|| ferrum_kernels::cuda_device_count().unwrap_or(1));
177 if tp > 1 {
178 eprintln!("Tensor Parallel: TP={tp}");
179 }
180 }
181
182 engine_config.sampling.default_params = bench_sampling_params(cmd.max_tokens);
183 engine_config.backend.device = device;
184 engine_config.backend.backend_options.insert(
185 "model_path".to_string(),
186 serde_json::Value::String(engine_model_path),
187 );
188 engine_config.scheduler.policy = ferrum_types::SchedulingPolicy::ContinuousBatch;
189 engine_config
190 .apply_runtime_config_snapshot(&runtime_config)
191 .map_err(ferrum_types::FerrumError::config)?;
192 let effective_kv_dtype = cmd
193 .kv_dtype
194 .as_deref()
195 .or_else(|| crate::runtime_env::runtime_snapshot_value(&runtime_config, "FERRUM_KV_DTYPE"));
196 super::run::apply_kv_dtype_override(&mut engine_config, effective_kv_dtype)?;
197 let engine = match (defined_model, model_sources) {
198 (Some(prepared), _) => {
199 ferrum_engine::create_defined_product_engine(engine_config, prepared).await?
200 }
201 (None, Some(sources)) => {
202 ferrum_engine::create_product_engine(engine_config, sources).await?
203 }
204 (None, None) => ferrum_engine::create_default_engine(engine_config).await?,
205 };
206
207 let prompt = if cmd.long_context {
208 generate_long_prompt()
209 } else {
210 cmd.prompt.clone()
211 };
212
213 let mode_str = if cmd.concurrency > 1 {
214 format!("concurrent({})", cmd.concurrency)
215 } else if cmd.long_context {
216 "long-context".to_string()
217 } else {
218 "sequential".to_string()
219 };
220
221 eprintln!(
222 "{}",
223 format!(
224 "Config: {} rounds × {} repeats, {} max_tokens, mode={}, prompt_len=~{}chars",
225 cmd.rounds,
226 cmd.n_repeats,
227 cmd.max_tokens,
228 mode_str,
229 prompt.len()
230 )
231 .dimmed()
232 );
233 if cmd.n_repeats < 3 {
234 eprintln!(
235 "{}",
236 " [warn] n_repeats < 3 — emitting mean only, no stddev/CI95 (PLAYBOOK § 0.4)".yellow()
237 );
238 }
239 eprintln!("{}", "=".repeat(60).dimmed());
240
241 eprintln!("{}", "Warmup...".dimmed());
243 let warmup = run_single(&*engine, &model_id, "Hello", 16).await?;
244 if !warmup.success {
245 return Err(ferrum_types::FerrumError::model(
246 "benchmark warmup request failed",
247 ));
248 }
249 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
250
251 let mut runs: Vec<RunRecord> = Vec::with_capacity(cmd.n_repeats as usize);
253 for repeat_idx in 0..cmd.n_repeats {
254 eprintln!(
255 "{}",
256 format!("Repeat {}/{}", repeat_idx + 1, cmd.n_repeats).bold()
257 );
258 let run = if cmd.concurrency > 1 {
259 run_concurrent_round(&*engine, &model_id, &prompt, &cmd).await?
260 } else {
261 run_sequential_round(&*engine, &model_id, &prompt, &cmd).await?
262 };
263 eprintln!(
264 " {} requests · {:.1}s · {:.1} tok/s",
265 run.records.len(),
266 run.duration_s,
267 run.records
268 .iter()
269 .map(|r| r.output_tokens as f64)
270 .sum::<f64>()
271 / run.duration_s
272 );
273 runs.push(run);
274 }
275
276 let scenario = if cmd.concurrency > 1 {
278 Scenario::ClosedLoop
280 } else {
281 Scenario::Cli
282 };
283 let concurrency = if cmd.concurrency > 1 {
284 Some(cmd.concurrency as u32)
285 } else {
286 None
287 };
288
289 let env = build_env(&cmd);
290 let slo = cmd.goodput.unwrap_or_else(Slo::unbounded);
291 let prompt_len = u32::try_from(prompt.len()).map_err(|_| {
292 ferrum_types::FerrumError::model("benchmark prompt length exceeds report capacity")
293 })?;
294 let report = compute_metrics(
295 model_id.clone(),
296 backend_str,
297 scenario,
298 concurrency,
299 None,
300 prompt_len, cmd.max_tokens,
302 0, slo,
304 runs,
305 env,
306 );
307
308 emit_then_enforce_bench_report(&cmd, &report, &mode_str)
309}
310
311async fn run_sequential_round(
314 engine: &(dyn ferrum_interfaces::engine::LlmInferenceEngine + Send + Sync),
315 model_id: &str,
316 prompt: &str,
317 cmd: &BenchCommand,
318) -> Result<RunRecord> {
319 let expected_requests = measured_request_count(cmd)?;
320 let mut records = Vec::with_capacity(cmd.rounds);
321 let start = Instant::now();
322 for _ in 0..cmd.rounds {
323 match run_single(engine, model_id, prompt, cmd.max_tokens).await {
324 Ok(record) => records.push(record),
325 Err(error) => {
326 eprintln!(" request start error: {error}");
327 let mut quality = QualityIssueCounts::default();
328 quality.malformed_stream = 1;
329 records.push(failed_bench_record(quality));
330 }
331 }
332 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
334 }
335 let duration_s = start.elapsed().as_secs_f64();
336 Ok(RunRecord {
337 records,
338 expected_requests,
339 duration_s,
340 warmup: Default::default(),
341 })
342}
343
344async fn run_concurrent_round(
345 engine: &(dyn ferrum_interfaces::engine::LlmInferenceEngine + Send + Sync),
346 model_id: &str,
347 prompt: &str,
348 cmd: &BenchCommand,
349) -> Result<RunRecord> {
350 let expected_requests = measured_request_count(cmd)?;
351 let mut records = Vec::with_capacity(expected_requests as usize);
352 let start = Instant::now();
353 for _ in 0..cmd.rounds {
354 let mut handles = Vec::with_capacity(cmd.concurrency);
355 for _ in 0..cmd.concurrency {
356 let request = make_request(model_id, prompt, cmd.max_tokens);
357 match engine.infer_stream(request).await {
358 Ok(stream) => handles.push(tokio::spawn(collect_stream(stream))),
359 Err(error) => {
360 eprintln!(" request start error: {error}");
361 let mut quality = QualityIssueCounts::default();
362 quality.malformed_stream = 1;
363 records.push(failed_bench_record(quality));
364 }
365 }
366 }
367 for handle in handles {
368 match handle.await {
369 Ok(Ok(r)) => records.push(r),
370 Ok(Err(e)) => {
371 eprintln!(" request error: {e}");
372 let mut quality = QualityIssueCounts::default();
373 quality.malformed_stream = 1;
374 records.push(failed_bench_record(quality));
375 }
376 Err(e) => {
377 eprintln!(" join error: {e}");
378 let mut quality = QualityIssueCounts::default();
379 quality.panic = 1;
380 records.push(failed_bench_record(quality));
381 }
382 }
383 }
384 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
385 }
386 let duration_s = start.elapsed().as_secs_f64();
387 Ok(RunRecord {
388 records,
389 expected_requests,
390 duration_s,
391 warmup: Default::default(),
392 })
393}
394
395fn failed_bench_record(quality_issues: QualityIssueCounts) -> RequestRecord {
396 RequestRecord {
397 benchmark_correlation: None,
398 server_request_id: None,
399 success: false,
400 ttft_ms: 0.0,
401 e2e_ms: 0.0,
402 input_tokens: 0,
403 server_input_tokens: None,
404 output_tokens: 0,
405 output_token_count_source: OutputTokenCountSource::None,
406 itl_evidence: RequestItlEvidence::failed(ItlEvidenceSource::EngineTokenEvents),
407 quality_issues,
408 itl_ms: vec![],
409 }
410}
411
412fn make_request(model_id: &str, prompt: &str, max_tokens: u32) -> InferenceRequest {
415 InferenceRequest {
416 id: RequestId(Uuid::new_v4()),
417 model_id: ferrum_types::ModelId(model_id.to_string()),
418 prompt: prompt.to_string(),
419 sampling_params: bench_sampling_params(max_tokens),
420 stream: true,
421 priority: Priority::Normal,
422 client_id: None,
423 session_id: None,
424 created_at: Utc::now(),
425 api_request: None,
426 evidence_request: Default::default(),
427 metadata: HashMap::new(),
428 }
429}
430
431fn bench_sampling_params(max_tokens: u32) -> SamplingParams {
432 SamplingParams {
433 max_tokens: max_tokens as usize,
434 temperature: 0.0, top_p: 1.0,
436 repetition_penalty: 1.0,
437 stop_sequences: vec![
438 "<|im_end|>".to_string(),
439 "</s>".to_string(),
440 "<|endoftext|>".to_string(),
441 ],
442 ..Default::default()
443 }
444}
445
446async fn run_single(
447 engine: &(dyn ferrum_interfaces::engine::LlmInferenceEngine + Send + Sync),
448 model_id: &str,
449 prompt: &str,
450 max_tokens: u32,
451) -> Result<RequestRecord> {
452 let request = make_request(model_id, prompt, max_tokens);
453 let stream = engine.infer_stream(request).await?;
454 Ok(collect_stream(stream).await?)
455}
456
457async fn collect_stream(
458 mut stream: std::pin::Pin<
459 Box<
460 dyn futures::Stream<
461 Item = std::result::Result<
462 ferrum_types::StreamChunk,
463 ferrum_types::FerrumError,
464 >,
465 > + Send,
466 >,
467 >,
468) -> Result<RequestRecord> {
469 let start = Instant::now();
470 let mut token_count: u32 = 0;
471 let mut first_token_time: Option<Instant> = None;
472 let mut last_token_time: Option<Instant> = None;
473 let mut itl_ms: Vec<f64> = Vec::new();
474 let mut quality_issues = QualityIssueCounts::default();
475
476 let mut got_finish = false;
477 while let Some(result) = stream.next().await {
478 match result {
479 Ok(chunk) => {
480 if chunk.token.is_some() {
481 let now = Instant::now();
482 if first_token_time.is_none() {
483 first_token_time = Some(now);
484 } else if let Some(prev) = last_token_time {
485 itl_ms.push((now - prev).as_secs_f64() * 1000.0);
486 }
487 last_token_time = Some(now);
488 token_count = token_count
489 .checked_add(1)
490 .expect("benchmark output token count overflow");
491 }
492 if chunk.finish_reason.is_some() {
493 got_finish = true;
494 break;
495 }
496 }
497 Err(_) => {
498 quality_issues.malformed_stream = 1;
499 break;
500 }
501 }
502 }
503 if !got_finish {
504 quality_issues.missing_done = 1;
505 if token_count > 0 {
506 eprintln!(
507 " [warn] stream ended without finish_reason ({} tokens)",
508 token_count
509 );
510 }
511 }
512 if token_count == 0 {
513 quality_issues.zero_output_tokens = 1;
514 }
515
516 let e2e_ms = start.elapsed().as_secs_f64() * 1000.0;
517 let ttft_ms = first_token_time
518 .map(|t| t.duration_since(start).as_secs_f64() * 1000.0)
519 .unwrap_or(e2e_ms);
520
521 let success = token_count > 0 && got_finish && quality_issues.request_error_count() == 0;
522 let observed_intervals =
523 u32::try_from(itl_ms.len()).expect("benchmark ITL interval count overflow");
524 Ok(RequestRecord {
525 benchmark_correlation: None,
526 server_request_id: None,
527 success,
528 ttft_ms,
529 e2e_ms,
530 input_tokens: 0, server_input_tokens: None,
532 output_tokens: token_count,
533 output_token_count_source: if token_count > 0 {
534 OutputTokenCountSource::StreamChunks
535 } else {
536 OutputTokenCountSource::None
537 },
538 itl_evidence: RequestItlEvidence::engine(success, token_count, observed_intervals),
539 quality_issues,
540 itl_ms,
541 })
542}
543
544fn build_env(cmd: &BenchCommand) -> Env {
547 let commit_sha = cmd
548 .commit_sha
549 .clone()
550 .or_else(|| {
551 std::process::Command::new("git")
552 .args(["rev-parse", "--short", "HEAD"])
553 .output()
554 .ok()
555 .and_then(|o| String::from_utf8(o.stdout).ok())
556 .map(|s| s.trim().to_string())
557 })
558 .unwrap_or_else(|| "unknown".to_string());
559
560 #[allow(unused_mut)]
561 let mut features: Vec<String> = Vec::new();
562 #[cfg(feature = "metal")]
563 features.push("metal".to_string());
564 #[cfg(feature = "cuda")]
565 features.push("cuda".to_string());
566
567 let mut env = Env::capture_minimal(commit_sha, features);
568 if let Some(hw) = cmd.hw_id.clone() {
569 env.hw_id = hw;
570 }
571 env
572}
573
574fn print_human_summary(report: &BenchReport, cmd: &BenchCommand, mode_str: &str) {
577 eprintln!();
578 eprintln!("{}", "=".repeat(60));
579 eprintln!("{}", format!("BENCHMARK RESULTS ({})", mode_str).bold());
580 eprintln!("{}", "=".repeat(60));
581 eprintln!("Model: {}", report.model);
582 eprintln!("Backend: {}", report.backend);
583 eprintln!("Rounds: {}", cmd.rounds);
584 eprintln!("Repeats: {}", report.n_repeats);
585 eprintln!("Max tokens/req: {}", cmd.max_tokens);
586 if let Some(c) = report.concurrency {
587 eprintln!("Concurrency: {}", c);
588 }
589 eprintln!("{}", "-".repeat(60));
590 let fmt = |s: &ferrum_bench_core::ScalarStats| -> String {
591 if report.n_repeats >= 3 {
592 format!("{:.1} ± {:.1}", s.mean, s.ci95_hw)
593 } else {
594 format!("{:.1}", s.mean)
595 }
596 };
597 eprintln!(
598 "TTFT_ms p50={} p95={} p99={}",
599 fmt(&report.ttft_ms.p50),
600 fmt(&report.ttft_ms.p95),
601 fmt(&report.ttft_ms.p99)
602 );
603 eprintln!(
604 "TPOT_ms p50={} p95={} p99={}",
605 fmt(&report.tpot_ms.p50),
606 fmt(&report.tpot_ms.p95),
607 fmt(&report.tpot_ms.p99)
608 );
609 if report.has_complete_itl_evidence() {
610 eprintln!(
611 "ITL_ms p50={} p95={} p99={}",
612 fmt(&report.itl_ms.p50),
613 fmt(&report.itl_ms.p95),
614 fmt(&report.itl_ms.p99)
615 );
616 } else {
617 eprintln!("ITL_ms unavailable");
618 }
619 eprintln!("Output thr {} tok/s", fmt(&report.output_throughput_tps));
620 if cmd.goodput.is_some() {
621 eprintln!("Goodput {} req/s", fmt(&report.goodput_rps));
622 }
623 eprintln!("env_hash: {}", report.env_hash);
624 eprintln!("{}", "=".repeat(60));
625}
626
627fn emit_json(cmd: &BenchCommand, report: &BenchReport) -> Result<()> {
628 let pretty = serde_json::to_string_pretty(report).expect("serialize");
629 if let Some(out) = cmd.out.as_ref() {
630 std::fs::write(out, &pretty).map_err(|e| {
631 ferrum_types::FerrumError::model(format!("write {}: {e}", out.display()))
632 })?;
633 eprintln!("\n→ wrote {}", out.display());
634 } else {
635 println!("{}", pretty);
636 }
637 Ok(())
638}
639
640fn emit_then_enforce_bench_report(
641 cmd: &BenchCommand,
642 report: &BenchReport,
643 mode_str: &str,
644) -> Result<()> {
645 match cmd.output.as_str() {
646 "human" => print_human_summary(report, cmd, mode_str),
647 "json" => emit_json(cmd, report)?,
648 other => {
649 return Err(ferrum_types::FerrumError::model(format!(
650 "unknown --output '{other}': allowed values are human, json"
651 )))
652 }
653 }
654
655 ferrum_bench_core::trace::flush_global_trace();
659 enforce_bench_error_policy(report)
660}
661
662fn enforce_bench_error_policy(report: &BenchReport) -> Result<()> {
663 let errored = report
664 .errored_per_run
665 .iter()
666 .try_fold(0_u64, |total, value| total.checked_add(*value as u64))
667 .ok_or_else(|| ferrum_types::FerrumError::model("benchmark error count overflow"))?;
668 if errored > 0 {
669 return Err(ferrum_types::FerrumError::model(format!(
670 "benchmark measured requests failed: {errored}"
671 )));
672 }
673 Ok(())
674}
675
676fn generate_long_prompt() -> String {
678 let base = "The history of artificial intelligence is a fascinating journey through decades of research, breakthroughs, and setbacks. From the early days of symbolic AI in the 1950s, through the AI winters, to the modern era of deep learning and large language models, the field has undergone remarkable transformations. ";
679 let mut prompt = String::with_capacity(8192);
680 prompt.push_str("Please provide a comprehensive analysis of the following text, identifying key themes, patterns, and insights:\n\n");
681 for _ in 0..25 {
682 prompt.push_str(base);
683 }
684 prompt.push_str("\n\nNow analyze the above text in detail:");
685 prompt
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn command() -> BenchCommand {
693 BenchCommand {
694 model: "test".to_string(),
695 rounds: 3,
696 max_tokens: 4,
697 backend: "cpu".to_string(),
698 numerical_profile: None,
699 prompt: "hello".to_string(),
700 concurrency: 2,
701 long_context: false,
702 kv_dtype: None,
703 n_repeats: 1,
704 goodput: None,
705 output: "json".to_string(),
706 out: None,
707 hw_id: None,
708 commit_sha: None,
709 }
710 }
711
712 #[test]
713 fn validates_expected_request_count_and_numeric_inputs() {
714 let mut cmd = command();
715 assert_eq!(measured_request_count(&cmd).unwrap(), 6);
716 assert_eq!(
717 measured_request_count(&cmd).unwrap() as usize,
718 cmd.rounds * cmd.concurrency
719 );
720 assert!(validate_command(&cmd).is_ok());
721 cmd.concurrency = 1;
722 assert_eq!(measured_request_count(&cmd).unwrap() as usize, cmd.rounds);
723 assert!(validate_command(&cmd).is_ok());
724 cmd.n_repeats = 0;
725 assert!(validate_command(&cmd).is_err());
726 cmd.n_repeats = 1;
727 cmd.goodput = Some(Slo {
728 ttft_p99_ms: f64::INFINITY,
729 ..Slo::default()
730 });
731 assert!(validate_command(&cmd).is_err());
732 }
733
734 #[test]
735 fn failed_bench_record_is_explicit_error_evidence() {
736 let mut quality = QualityIssueCounts::default();
737 quality.panic = 1;
738 let record = failed_bench_record(quality);
739 assert!(!record.success);
740 assert_eq!(record.quality_issues.panic, 1);
741 assert_eq!(
742 record.output_token_count_source,
743 OutputTokenCountSource::None
744 );
745 assert_eq!(
746 record.itl_evidence.source,
747 ItlEvidenceSource::EngineTokenEvents
748 );
749 }
750
751 #[test]
752 fn failed_bench_report_is_written_before_nonzero_result() {
753 let out = std::env::temp_dir().join(format!(
754 "ferrum-bench-failed-report-{}-{}.json",
755 std::process::id(),
756 std::time::SystemTime::now()
757 .duration_since(std::time::UNIX_EPOCH)
758 .unwrap()
759 .as_nanos()
760 ));
761 let _ = std::fs::remove_file(&out);
762 let mut quality = QualityIssueCounts::default();
763 quality.malformed_stream = 1;
764 let report = compute_metrics(
765 "test".to_string(),
766 "cpu".to_string(),
767 Scenario::Cli,
768 None,
769 None,
770 1,
771 1,
772 0,
773 Slo::default(),
774 vec![RunRecord {
775 records: vec![failed_bench_record(quality)],
776 expected_requests: 1,
777 duration_s: 1.0,
778 warmup: Default::default(),
779 }],
780 Env::default(),
781 );
782 let mut cmd = command();
783 cmd.out = Some(out.clone());
784 let error = emit_then_enforce_bench_report(&cmd, &report, "test")
785 .expect_err("failed measured request must return nonzero");
786 assert!(error.to_string().contains("measured requests failed"));
787 let json: serde_json::Value =
788 serde_json::from_slice(&std::fs::read(&out).unwrap()).unwrap();
789 assert_eq!(json["errored_per_run"], serde_json::json!([1]));
790 assert_eq!(json["repeat_metrics"][0]["itl_ineligible_requests"], 1);
791 let _ = std::fs::remove_file(out);
792 }
793}