1use std::borrow::Cow;
2use std::fmt::{Display, Write};
3use std::path::PathBuf;
4
5use anyhow::Result;
6use colored::{Color, ColoredString, Colorize};
7
8use super::args::NoCapture;
9use super::bin_bench::BinBench;
10use super::common::{Config, ModulePath};
11use super::lib_bench::LibBench;
12use super::meta::Metadata;
13use super::summary::{Diffs, MetricsDiff, SegmentDetails, ToolMetricSummary, ToolRun};
14use super::tool::ValgrindTool;
15use crate::api::{self, DhatMetricKind, ErrorMetricKind, EventKind};
16use crate::util::{make_relative, to_string_signed_short, truncate_str_utf8, EitherOrBoth};
17
18pub const CALLGRIND_DEFAULT: [EventKind; 21] = [
20 EventKind::Ir,
21 EventKind::L1hits,
22 EventKind::LLhits,
23 EventKind::RamHits,
24 EventKind::TotalRW,
25 EventKind::EstimatedCycles,
26 EventKind::SysCount,
27 EventKind::SysTime,
28 EventKind::SysCpuTime,
29 EventKind::Ge,
30 EventKind::Bc,
31 EventKind::Bcm,
32 EventKind::Bi,
33 EventKind::Bim,
34 EventKind::ILdmr,
35 EventKind::DLdmr,
36 EventKind::DLdmw,
37 EventKind::AcCost1,
38 EventKind::AcCost2,
39 EventKind::SpLoss1,
40 EventKind::SpLoss2,
41];
42
43pub const ERROR_METRICS_DEFAULT: [ErrorMetricKind; 4] = [
45 ErrorMetricKind::Errors,
46 ErrorMetricKind::Contexts,
47 ErrorMetricKind::SuppressedErrors,
48 ErrorMetricKind::SuppressedContexts,
49];
50
51pub const DHAT_DEFAULT: [DhatMetricKind; 8] = [
53 DhatMetricKind::TotalBytes,
54 DhatMetricKind::TotalBlocks,
55 DhatMetricKind::AtTGmaxBytes,
56 DhatMetricKind::AtTGmaxBlocks,
57 DhatMetricKind::AtTEndBytes,
58 DhatMetricKind::AtTEndBlocks,
59 DhatMetricKind::ReadsBytes,
60 DhatMetricKind::WritesBytes,
61];
62
63pub const NOT_AVAILABLE: &str = "N/A";
65pub const UNKNOWN: &str = "*********";
66pub const NO_CHANGE: &str = "No change";
67
68pub const METRIC_WIDTH: usize = 20;
69pub const FIELD_WIDTH: usize = 21;
70
71pub const LEFT_WIDTH: usize = METRIC_WIDTH + FIELD_WIDTH;
72pub const DIFF_WIDTH: usize = 9;
73
74pub const FLOAT_WIDTH: usize = DIFF_WIDTH - 1;
76
77#[allow(clippy::doc_link_with_quotes)]
78pub const MAX_WIDTH: usize = 2 + LEFT_WIDTH + 1 + METRIC_WIDTH + 2 * 11;
82
83pub trait Formatter {
84 fn format_single(
85 &mut self,
86 baselines: (Option<String>, Option<String>),
87 details: Option<&EitherOrBoth<SegmentDetails>>,
88 metrics_summary: &ToolMetricSummary,
89 ) -> Result<()>;
90
91 fn format(
92 &mut self,
93 config: &Config,
94 baselines: (Option<String>, Option<String>),
95 tool_run: &ToolRun,
96 ) -> Result<()>;
97
98 fn print(
99 &mut self,
100 config: &Config,
101 baselines: (Option<String>, Option<String>),
102 tool_run: &ToolRun,
103 ) -> Result<()>
104 where
105 Self: std::fmt::Display,
106 {
107 if self.get_output_format().is_default() {
108 self.format(config, baselines, tool_run)?;
109 print!("{self}");
110 self.clear();
111 }
112 Ok(())
113 }
114
115 fn get_output_format(&self) -> &OutputFormat;
116
117 fn clear(&mut self);
118
119 fn print_comparison(
120 &mut self,
121 function_name: &str,
122 id: &str,
123 details: Option<&str>,
124 metrics_summary: &ToolMetricSummary,
125 ) -> Result<()>;
126}
127
128pub struct BinaryBenchmarkHeader {
129 inner: Header,
130 has_tools_enabled: bool,
131 output_format: OutputFormat,
132}
133
134pub struct ComparisonHeader {
135 pub function_name: String,
136 pub id: String,
137 pub details: Option<String>,
138 pub indent: String,
139}
140
141struct Header {
142 module_path: String,
143 id: Option<String>,
144 description: Option<String>,
145}
146
147pub struct LibraryBenchmarkHeader {
148 inner: Header,
149 has_tools_enabled: bool,
150 output_format: OutputFormat,
151}
152
153#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
154pub enum OutputFormatKind {
155 #[default]
156 Default,
157 Json,
158 PrettyJson,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct OutputFormat {
163 pub kind: OutputFormatKind,
164 pub truncate_description: Option<usize>,
165 pub show_intermediate: bool,
166 pub show_grid: bool,
167}
168
169#[derive(Debug, Clone)]
170pub struct VerticalFormatter {
171 buffer: String,
172 indent: String,
173 indent_tool_header: String,
174 indent_sub_header: String,
175 output_format: OutputFormat,
176}
177
178impl BinaryBenchmarkHeader {
179 pub fn new(meta: &Metadata, bin_bench: &BinBench) -> Self {
180 let path = make_relative(&meta.project_root, &bin_bench.command.path);
181
182 let command_args: Vec<String> = bin_bench
183 .command
184 .args
185 .iter()
186 .map(|s| s.to_string_lossy().to_string())
187 .collect();
188 let command_args = shlex::try_join(command_args.iter().map(String::as_str)).unwrap();
189
190 let description = if command_args.is_empty() {
191 format!(
192 "({}) -> {}",
193 bin_bench.args.as_ref().map_or("", String::as_str),
194 path.display(),
195 )
196 } else {
197 format!(
198 "({}) -> {} {}",
199 bin_bench.args.as_ref().map_or("", String::as_str),
200 path.display(),
201 command_args
202 )
203 };
204
205 Self {
206 inner: Header::new(
207 &bin_bench.module_path,
208 bin_bench.id.clone(),
209 Some(description),
210 &bin_bench.output_format,
211 ),
212 has_tools_enabled: bin_bench.tools.has_tools_enabled(),
213 output_format: bin_bench.output_format,
214 }
215 }
216
217 pub fn print(&self) {
218 if self.output_format.kind == OutputFormatKind::Default {
219 self.inner.print();
220 if self.has_tools_enabled {
221 let mut formatter = VerticalFormatter::new(self.output_format);
222 formatter.format_tool_headline(ValgrindTool::Callgrind);
223 formatter.print_buffer();
224 }
225 }
226 }
227
228 pub fn to_title(&self) -> String {
229 self.inner.to_title()
230 }
231
232 pub fn description(&self) -> Option<String> {
233 self.inner.description.clone()
234 }
235}
236
237impl ComparisonHeader {
238 pub fn new<T, U, V>(
239 function_name: T,
240 id: U,
241 details: Option<V>,
242 output_format: &OutputFormat,
243 ) -> Self
244 where
245 T: Into<String>,
246 U: Into<String>,
247 V: Into<String>,
248 {
249 Self {
250 function_name: function_name.into(),
251 id: id.into(),
252 details: details.map(Into::into),
253 indent: if output_format.show_grid {
254 "|-".bright_black().to_string()
255 } else {
256 " ".to_owned()
257 },
258 }
259 }
260
261 pub fn print(&self) {
262 println!("{self}");
263 }
264}
265
266impl Display for ComparisonHeader {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 write!(
269 f,
270 "{}{} {} {}",
271 self.indent,
272 "Comparison with".yellow().bold(),
273 self.function_name.green(),
274 self.id.cyan()
275 )?;
276
277 if let Some(details) = &self.details {
278 write!(f, ":{}", details.blue().bold())?;
279 }
280
281 Ok(())
282 }
283}
284
285impl Header {
286 pub fn new<T>(
287 module_path: &ModulePath,
288 id: T,
289 description: Option<String>,
290 output_format: &OutputFormat,
291 ) -> Self
292 where
293 T: Into<Option<String>>,
294 {
295 let truncated = description
296 .map(|d| truncate_description(&d, output_format.truncate_description).to_string());
297
298 Self {
299 module_path: module_path.to_string(),
300 id: id.into(),
301 description: truncated,
302 }
303 }
304
305 pub fn print(&self) {
306 println!("{self}");
307 }
308
309 pub fn to_title(&self) -> String {
310 let mut output = String::new();
311
312 write!(output, "{}", self.module_path).unwrap();
313 if let Some(id) = &self.id {
314 match &self.description {
315 Some(description) if !description.is_empty() => {
316 write!(output, " {id}:{description}").unwrap();
317 }
318 _ => {
319 write!(output, " {id}").unwrap();
320 }
321 }
322 }
323 output
324 }
325}
326
327impl Display for Header {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 f.write_fmt(format_args!("{}", self.module_path.green()))?;
330
331 if let Some(id) = &self.id {
332 match &self.description {
333 Some(description) if !description.is_empty() => {
334 f.write_fmt(format_args!(
335 " {}{}{}",
336 id.cyan(),
337 ":".cyan(),
338 description.bold().blue(),
339 ))?;
340 }
341 _ if !id.is_empty() => {
342 f.write_fmt(format_args!(" {}", id.cyan()))?;
343 }
344 _ => {}
345 }
346 } else if let Some(description) = &self.description {
347 if !description.is_empty() {
348 f.write_fmt(format_args!(" {}", description.bold().blue()))?;
349 }
350 } else {
351 }
353 Ok(())
354 }
355}
356
357impl LibraryBenchmarkHeader {
358 pub fn new(lib_bench: &LibBench) -> Self {
359 let header = Header::new(
360 &lib_bench.module_path,
361 lib_bench.id.clone(),
362 lib_bench.args.clone(),
363 &lib_bench.output_format,
364 );
365
366 Self {
367 inner: header,
368 has_tools_enabled: lib_bench.tools.has_tools_enabled(),
369 output_format: lib_bench.output_format,
370 }
371 }
372
373 pub fn print(&self) {
374 if self.output_format.is_default() {
375 self.inner.print();
376 if self.has_tools_enabled {
377 let mut formatter = VerticalFormatter::new(self.output_format);
378 formatter.format_tool_headline(ValgrindTool::Callgrind);
379 formatter.print_buffer();
380 }
381 }
382 }
383
384 pub fn to_title(&self) -> String {
385 self.inner.to_title()
386 }
387
388 pub fn description(&self) -> Option<String> {
389 self.inner.description.clone()
390 }
391}
392
393impl OutputFormat {
394 pub fn is_default(&self) -> bool {
395 self.kind == OutputFormatKind::Default
396 }
397
398 pub fn is_json(&self) -> bool {
399 self.kind == OutputFormatKind::Json || self.kind == OutputFormatKind::PrettyJson
400 }
401}
402
403impl From<api::OutputFormat> for OutputFormat {
404 fn from(value: api::OutputFormat) -> Self {
405 Self {
406 kind: OutputFormatKind::Default,
407 truncate_description: value.truncate_description.unwrap_or(Some(50)),
408 show_intermediate: value.show_intermediate.unwrap_or(false),
409 show_grid: value.show_grid.unwrap_or(false),
410 }
411 }
412}
413
414impl Default for OutputFormat {
415 fn default() -> Self {
416 Self {
417 kind: OutputFormatKind::default(),
418 truncate_description: Some(50),
419 show_intermediate: false,
420 show_grid: false,
421 }
422 }
423}
424
425enum IndentKind {
426 Normal,
427 ToolHeadline,
428 ToolSubHeadline,
429}
430
431impl VerticalFormatter {
432 pub fn new(output_format: OutputFormat) -> Self {
434 if output_format.show_grid {
435 Self {
436 buffer: String::new(),
437 indent: "| ".bright_black().to_string(),
438 indent_sub_header: "|-".bright_black().to_string(),
439 indent_tool_header: "|=".bright_black().to_string(),
440 output_format,
441 }
442 } else {
443 Self {
444 buffer: String::new(),
445 indent: " ".bright_black().to_string(),
446 indent_sub_header: " ".bright_black().to_string(),
447 indent_tool_header: " ".bright_black().to_string(),
448 output_format,
449 }
450 }
451 }
452
453 pub fn print_buffer(&mut self) {
455 print!("{}", self.buffer);
456 self.clear();
457 }
458
459 fn write_indent(&mut self, kind: &IndentKind) {
461 match kind {
462 IndentKind::Normal => write!(self, "{}", self.indent.clone()).unwrap(),
463 IndentKind::ToolHeadline => {
464 write!(self, "{}", self.indent_tool_header.clone()).unwrap();
465 }
466 IndentKind::ToolSubHeadline => {
467 write!(self, "{}", self.indent_sub_header.clone()).unwrap();
468 }
469 }
470 }
471
472 fn write_field<T>(
473 &mut self,
474 field: &str,
475 values: &EitherOrBoth<T>,
476 color: Option<Color>,
477 left_align: bool,
478 ) where
479 T: AsRef<str>,
480 {
481 self.write_indent(&IndentKind::Normal);
482
483 match values {
484 EitherOrBoth::Left(left) => {
485 let left = left.as_ref();
486 let colored = match color {
487 Some(color) => left.color(color).bold(),
488 None => left.bold(),
489 };
490
491 if left_align {
492 writeln!(self, "{field:<FIELD_WIDTH$}{colored}").unwrap();
493 } else {
494 writeln!(
495 self,
496 "{field:<FIELD_WIDTH$}{}{colored}",
497 " ".repeat(METRIC_WIDTH.saturating_sub(left.len()))
498 )
499 .unwrap();
500 }
501 }
502 EitherOrBoth::Right(right) => {
503 let right = right.as_ref().trim();
504 let colored = match color {
505 Some(color) => right.color(color),
506 None => ColoredString::from(right),
507 };
508
509 writeln!(
510 self,
511 "{field:<FIELD_WIDTH$}{}|{colored}",
512 " ".repeat(METRIC_WIDTH),
513 )
514 .unwrap();
515 }
516 EitherOrBoth::Both(left, right) => {
517 let left = left.as_ref().trim();
518 let right = right.as_ref().trim();
519
520 let colored_left = match color {
521 Some(color) => left.color(color).bold(),
522 None => left.bold(),
523 };
524 let colored_right = match color {
525 Some(color) => right.color(color),
526 None => ColoredString::from(right),
527 };
528
529 if left.len() > METRIC_WIDTH {
530 writeln!(self, "{field:<FIELD_WIDTH$}{colored_left}").unwrap();
531 self.write_indent(&IndentKind::Normal);
532 writeln!(self, "{}|{colored_right}", " ".repeat(LEFT_WIDTH)).unwrap();
533 } else if left_align {
534 writeln!(
535 self,
536 "{field:<FIELD_WIDTH$}{colored_left}{}|{colored_right}",
537 " ".repeat(METRIC_WIDTH - left.len()),
538 )
539 .unwrap();
540 } else {
541 writeln!(
542 self,
543 "{field:<FIELD_WIDTH$}{}{colored_left}|{colored_right}",
544 " ".repeat(METRIC_WIDTH - left.len()),
545 )
546 .unwrap();
547 }
548 }
549 }
550 }
551
552 fn write_metric(&mut self, field: &str, metrics: &EitherOrBoth<&u64>, diffs: Option<Diffs>) {
553 match metrics {
554 EitherOrBoth::Left(new) => {
555 let right = format!(
556 "{NOT_AVAILABLE:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
557 UNKNOWN.bright_black()
558 );
559 self.write_field(
560 field,
561 &EitherOrBoth::Both(&new.to_string(), &right),
562 None,
563 false,
564 );
565 }
566 EitherOrBoth::Right(old) => {
567 let right = format!(
568 "{old:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
569 UNKNOWN.bright_black()
570 );
571 self.write_field(
572 field,
573 &EitherOrBoth::Both(NOT_AVAILABLE, &right),
574 None,
575 false,
576 );
577 }
578 EitherOrBoth::Both(new, old) if new == old => {
579 let right = format!(
580 "{old:<METRIC_WIDTH$} ({:^DIFF_WIDTH$})",
581 NO_CHANGE.bright_black()
582 );
583 self.write_field(
584 field,
585 &EitherOrBoth::Both(&new.to_string(), &right),
586 None,
587 false,
588 );
589 }
590 EitherOrBoth::Both(new, old) => {
591 let diffs = diffs.expect(
592 "If there are new metrics and old metrics there should be a difference present",
593 );
594 let pct_string = format_float(diffs.diff_pct, '%');
595 let factor_string = format_float(diffs.factor, 'x');
596
597 let right = format!(
598 "{old:<METRIC_WIDTH$} ({pct_string:^DIFF_WIDTH$}) \
599 [{factor_string:^DIFF_WIDTH$}]"
600 );
601 self.write_field(
602 field,
603 &EitherOrBoth::Both(&new.to_string(), &right),
604 None,
605 false,
606 );
607 }
608 }
609 }
610
611 fn write_empty_line(&mut self) {
612 let indent = self.indent.trim_end().to_owned();
613 if !indent.is_empty() {
614 writeln!(self, "{indent}").unwrap();
615 }
616 }
617
618 fn write_left_indented(&mut self, value: &str) {
619 self.write_indent(&IndentKind::Normal);
620 writeln!(self, "{}{value}", " ".repeat(FIELD_WIDTH)).unwrap();
621 }
622
623 fn format_baseline(&mut self, baselines: (Option<String>, Option<String>)) {
625 match baselines {
626 (None, None) => {}
627 _ => {
628 self.write_field(
629 "Baselines:",
630 &EitherOrBoth::try_from(baselines)
631 .expect("At least on baseline should be present")
632 .as_ref()
633 .map(String::as_str),
634 None,
635 false,
636 );
637 }
638 }
639 }
640
641 fn format_details(&mut self, details: &str) {
642 let mut details = details.lines();
643 if let Some(head_line) = details.next() {
644 self.write_indent(&IndentKind::Normal);
645 writeln!(self, "{:<FIELD_WIDTH$}{}", "Details:", head_line).unwrap();
646 for body_line in details {
647 if body_line.is_empty() {
648 self.write_empty_line();
649 } else {
650 self.write_left_indented(body_line);
651 }
652 }
653 }
654 }
655
656 fn format_metrics<'a, K: Display>(
657 &mut self,
658 metrics: impl Iterator<Item = (K, &'a MetricsDiff)>,
659 ) {
660 for (metric_kind, diff) in metrics {
661 let description = format!("{metric_kind}:");
662 self.write_metric(&description, &diff.metrics.as_ref(), diff.diffs);
663 }
664 }
665
666 fn format_tool_total_header(&mut self) {
667 self.write_indent(&IndentKind::ToolSubHeadline);
668 writeln!(self, "{} {}", "##".yellow(), "Total".bold()).unwrap();
669 }
670
671 fn format_multiple_segment_header(&mut self, details: &EitherOrBoth<SegmentDetails>) {
672 fn fields(detail: &SegmentDetails) -> String {
673 let mut result = String::new();
674 write!(result, "pid: {}", detail.pid).unwrap();
675
676 if let Some(ppid) = detail.parent_pid {
677 write!(result, " ppid: {ppid}").unwrap();
678 }
679 if let Some(thread) = detail.thread {
680 write!(result, " thread: {thread}").unwrap();
681 }
682 if let Some(part) = detail.part {
683 write!(result, " part: {part}").unwrap();
684 }
685
686 result
687 }
688
689 self.write_indent(&IndentKind::ToolSubHeadline);
690 write!(self, "{} ", "##".yellow()).unwrap();
691
692 let max_left = LEFT_WIDTH - 3;
693 match details {
694 EitherOrBoth::Left(new) => {
695 let left = fields(new);
696 let len = left.len();
697 let left = left.bold();
698
699 if len > max_left {
700 writeln!(self, "{left}\n{}|{NOT_AVAILABLE}", " ".repeat(max_left + 5)).unwrap();
701 } else {
702 writeln!(self, "{left}{}|{NOT_AVAILABLE}", " ".repeat(max_left - len)).unwrap();
703 }
704 }
705 EitherOrBoth::Right(old) => {
706 let right = fields(old);
707
708 writeln!(
709 self,
710 "{}{}|{right}",
711 NOT_AVAILABLE.bold(),
712 " ".repeat(max_left - NOT_AVAILABLE.len())
713 )
714 .unwrap();
715 }
716 EitherOrBoth::Both(new, old) => {
717 let left = fields(new);
718 let len = left.len();
719 let right = fields(old);
720 let left = left.bold();
721
722 if len > max_left {
723 writeln!(self, "{left}\n{}|{right}", " ".repeat(max_left + 5)).unwrap();
724 } else {
725 writeln!(self, "{left}{}|{right}", " ".repeat(max_left - len)).unwrap();
726 }
727 }
728 }
729 }
730
731 fn format_command(&mut self, config: &Config, command: &EitherOrBoth<&String>) {
732 let paths = match command {
733 EitherOrBoth::Left(new) => {
734 if new.starts_with(&config.bench_bin.display().to_string()) {
735 EitherOrBoth::Left(make_relative(&config.meta.project_root, &config.bench_bin))
736 } else {
737 EitherOrBoth::Left(make_relative(&config.meta.project_root, PathBuf::from(new)))
738 }
739 }
740 EitherOrBoth::Right(old) => {
741 if old.starts_with(&config.bench_bin.display().to_string()) {
742 EitherOrBoth::Right(make_relative(&config.meta.project_root, &config.bench_bin))
743 } else {
744 EitherOrBoth::Right(make_relative(
745 &config.meta.project_root,
746 PathBuf::from(old),
747 ))
748 }
749 }
750 EitherOrBoth::Both(new, old) if new == old => {
751 if new.starts_with(&config.bench_bin.display().to_string()) {
752 EitherOrBoth::Left(make_relative(&config.meta.project_root, &config.bench_bin))
753 } else {
754 EitherOrBoth::Left(make_relative(&config.meta.project_root, PathBuf::from(new)))
755 }
756 }
757 EitherOrBoth::Both(new, old) => {
758 let new_command = if new.starts_with(&config.bench_bin.display().to_string()) {
759 make_relative(&config.meta.project_root, &config.bench_bin)
760 } else {
761 make_relative(&config.meta.project_root, PathBuf::from(new))
762 };
763 let old_command = if old.starts_with(&config.bench_bin.display().to_string()) {
764 make_relative(&config.meta.project_root, &config.bench_bin)
765 } else {
766 make_relative(&config.meta.project_root, PathBuf::from(old))
767 };
768 EitherOrBoth::Both(new_command, old_command)
769 }
770 };
771
772 self.write_field(
773 "Command:",
774 &paths.map(|p| p.display().to_string()),
775 Some(Color::Blue),
776 true,
777 );
778 }
779
780 pub fn format_tool_headline(&mut self, tool: ValgrindTool) {
781 self.write_indent(&IndentKind::ToolHeadline);
782
783 let id = tool.id();
784 writeln!(
785 self,
786 "{} {} {}",
787 "=======".bright_black(),
788 id.to_ascii_uppercase(),
789 "=".repeat(MAX_WIDTH.saturating_sub(id.len() + 9))
790 .bright_black(),
791 )
792 .unwrap();
793 }
794}
795
796impl Display for VerticalFormatter {
797 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798 f.write_str(&self.buffer)
799 }
800}
801
802impl Write for VerticalFormatter {
803 fn write_str(&mut self, s: &str) -> std::fmt::Result {
804 self.buffer.push_str(s);
805 Ok(())
806 }
807}
808
809impl Formatter for VerticalFormatter {
810 fn format_single(
811 &mut self,
812 baselines: (Option<String>, Option<String>),
813 details: Option<&EitherOrBoth<SegmentDetails>>,
814 metrics_summary: &ToolMetricSummary,
815 ) -> Result<()> {
816 match metrics_summary {
817 ToolMetricSummary::None => {
818 if let Some(info) = details {
819 if let Some(new) = info.left() {
820 if let Some(details) = &new.details {
821 self.format_details(details);
822 }
823 }
824 }
825 }
826 ToolMetricSummary::ErrorSummary(summary) => {
827 self.format_metrics(
828 ERROR_METRICS_DEFAULT
829 .iter()
830 .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
831 );
832
833 if let Some(info) = details {
835 if summary
836 .diff_by_kind(&ErrorMetricKind::Errors)
837 .map_or(false, |e| e.metrics.left().map_or(false, |l| *l > 0))
838 {
839 if let Some(new) = info.left() {
840 if let Some(details) = new.details.as_ref() {
841 self.format_details(details);
842 }
843 }
844 }
845 }
846 }
847 ToolMetricSummary::DhatSummary(summary) => self.format_metrics(
848 DHAT_DEFAULT
849 .iter()
850 .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
851 ),
852 ToolMetricSummary::CallgrindSummary(summary) => {
853 self.format_baseline(baselines);
854 self.format_metrics(
855 CALLGRIND_DEFAULT
856 .iter()
857 .filter_map(|e| summary.diff_by_kind(e).map(|d| (e, d))),
858 );
859 }
860 }
861 Ok(())
862 }
863
864 fn format(
865 &mut self,
866 config: &Config,
867 baselines: (Option<String>, Option<String>),
868 tool_run: &ToolRun,
869 ) -> Result<()> {
870 if tool_run.has_multiple() && self.output_format.show_intermediate {
871 let mut first = true;
872 for segment in &tool_run.segments {
873 self.format_multiple_segment_header(&segment.details);
874 self.format_command(config, &segment.details.as_ref().map(|i| &i.command));
875
876 if first {
877 self.format_single(
878 baselines.clone(),
879 Some(&segment.details),
880 &segment.metrics_summary,
881 )?;
882 first = false;
883 } else {
884 self.format_single(
885 (None, None),
886 Some(&segment.details),
887 &segment.metrics_summary,
888 )?;
889 }
890 }
891
892 if tool_run.total.is_some() {
893 self.format_tool_total_header();
894 self.format_single((None, None), None, &tool_run.total)?;
895 }
896 } else if tool_run.total.is_some() {
897 self.format_single(baselines, None, &tool_run.total)?;
898 } else if tool_run.total.is_none() && !tool_run.segments.is_empty() {
899 for segment in &tool_run.segments {
903 self.format_command(config, &segment.details.as_ref().map(|i| &i.command));
904
905 if let Some(new) = segment.details.left() {
906 if let Some(details) = &new.details {
907 self.format_details(details);
908 }
909 }
910 }
911 } else {
912 }
914
915 Ok(())
916 }
917
918 fn print_comparison(
919 &mut self,
920 function_name: &str,
921 id: &str,
922 details: Option<&str>,
923 metrics_summary: &ToolMetricSummary,
924 ) -> Result<()> {
925 if self.output_format.is_default() {
926 ComparisonHeader::new(function_name, id, details, &self.output_format).print();
927
928 self.format_single((None, None), None, metrics_summary)?;
929 self.print_buffer();
930 }
931
932 Ok(())
933 }
934
935 fn clear(&mut self) {
936 self.buffer.clear();
937 }
938
939 fn get_output_format(&self) -> &OutputFormat {
940 &self.output_format
941 }
942}
943
944pub fn format_float(float: f64, unit: char) -> ColoredString {
945 let signed_short = to_string_signed_short(float);
946 if float.is_infinite() {
947 if float.is_sign_positive() {
948 format!("{signed_short:+^DIFF_WIDTH$}").bright_red().bold()
949 } else {
950 format!("{signed_short:-^DIFF_WIDTH$}")
951 .bright_green()
952 .bold()
953 }
954 } else if float.is_sign_positive() {
955 format!("{signed_short:^+FLOAT_WIDTH$}{unit}")
956 .bright_red()
957 .bold()
958 } else {
959 format!("{signed_short:^+FLOAT_WIDTH$}{unit}")
960 .bright_green()
961 .bold()
962 }
963}
964
965pub fn no_capture_footer(nocapture: NoCapture) -> Option<String> {
967 match nocapture {
968 NoCapture::True => Some(format!(
969 "{} {}",
970 "-".yellow(),
971 "end of stdout/stderr".yellow()
972 )),
973 NoCapture::False => None,
974 NoCapture::Stderr => Some(format!("{} {}", "-".yellow(), "end of stderr".yellow())),
975 NoCapture::Stdout => Some(format!("{} {}", "-".yellow(), "end of stdout".yellow())),
976 }
977}
978
979pub fn print_no_capture_footer(
980 nocapture: NoCapture,
981 stdout: Option<&api::Stdio>,
982 stderr: Option<&api::Stdio>,
983) {
984 let stdout_is_pipe = stdout.map_or(
985 nocapture == NoCapture::False || nocapture == NoCapture::Stderr,
986 api::Stdio::is_pipe,
987 );
988
989 let stderr_is_pipe = stderr.map_or(
990 nocapture == NoCapture::False || nocapture == NoCapture::Stdout,
991 api::Stdio::is_pipe,
992 );
993
994 match (stdout_is_pipe, stderr_is_pipe) {
997 (true, true) => {}
998 (true, false) => {
999 println!("{}", no_capture_footer(NoCapture::Stderr).unwrap());
1000 }
1001 (false, true) => {
1002 println!("{}", no_capture_footer(NoCapture::Stdout).unwrap());
1003 }
1004 (false, false) => {
1005 println!("{}", no_capture_footer(NoCapture::True).unwrap());
1006 }
1007 }
1008}
1009
1010fn truncate_description(description: &str, truncate_description: Option<usize>) -> Cow<'_, str> {
1011 if let Some(num) = truncate_description {
1012 let new_description = truncate_str_utf8(description, num);
1013 if new_description.len() < description.len() {
1014 Cow::Owned(format!("{new_description}..."))
1015 } else {
1016 Cow::Borrowed(description)
1017 }
1018 } else {
1019 Cow::Borrowed(description)
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use indexmap::indexmap;
1026 use pretty_assertions::assert_eq;
1027 use rstest::rstest;
1028
1029 use super::*;
1030 use crate::runner::metrics::Metrics;
1031
1032 #[rstest]
1033 #[case::simple("some::module", Some("id"), Some("1, 2"), "some::module id:1, 2")]
1034 #[case::id_but_no_description("some::module", Some("id"), None, "some::module id")]
1035 #[case::id_but_empty_description("some::module", Some("id"), Some(""), "some::module id")]
1036 #[case::no_id_but_description("some::module", None, Some("1, 2, 3"), "some::module 1, 2, 3")]
1037 #[case::no_id_no_description("some::module", None, None, "some::module")]
1038 #[case::no_id_empty_description("some::module", None, Some(""), "some::module")]
1039 #[case::length_is_greater_than_default(
1040 "some::module",
1041 Some("id"),
1042 Some("012345678901234567890123456789012345678901234567890123456789"),
1043 "some::module id:012345678901234567890123456789012345678901234567890123456789"
1044 )]
1045 fn test_header_display_when_no_truncate(
1046 #[case] module_path: &str,
1047 #[case] id: Option<&str>,
1048 #[case] description: Option<&str>,
1049 #[case] expected: &str,
1050 ) {
1051 colored::control::set_override(false);
1052
1053 let output_format = OutputFormat {
1054 truncate_description: None,
1055 ..Default::default()
1056 };
1057 let header = Header::new(
1058 &ModulePath::new(module_path),
1059 id.map(ToOwned::to_owned),
1060 description.map(ToOwned::to_owned),
1061 &output_format,
1062 );
1063
1064 assert_eq!(header.to_string(), expected);
1065 }
1066
1067 #[rstest]
1068 #[case::truncate_0(
1069 "some::module",
1070 Some("id"),
1071 Some("1, 2, 3"),
1072 Some(0),
1073 "some::module id:..."
1074 )]
1075 #[case::truncate_0_when_length_is_0(
1076 "some::module",
1077 Some("id"),
1078 Some(""),
1079 Some(0),
1080 "some::module id"
1081 )]
1082 #[case::truncate_0_when_length_is_1(
1083 "some::module",
1084 Some("id"),
1085 Some("1"),
1086 Some(0),
1087 "some::module id:..."
1088 )]
1089 #[case::truncate_1(
1090 "some::module",
1091 Some("id"),
1092 Some("1, 2, 3"),
1093 Some(1),
1094 "some::module id:1..."
1095 )]
1096 #[case::truncate_1_when_length_is_0(
1097 "some::module",
1098 Some("id"),
1099 Some(""),
1100 Some(1),
1101 "some::module id"
1102 )]
1103 #[case::truncate_1_when_length_is_1(
1104 "some::module",
1105 Some("id"),
1106 Some("1"),
1107 Some(1),
1108 "some::module id:1"
1109 )]
1110 #[case::truncate_1_when_length_is_2(
1111 "some::module",
1112 Some("id"),
1113 Some("1,"),
1114 Some(1),
1115 "some::module id:1..."
1116 )]
1117 #[case::truncate_3(
1118 "some::module",
1119 Some("id"),
1120 Some("1, 2, 3"),
1121 Some(3),
1122 "some::module id:1, ..."
1123 )]
1124 #[case::truncate_3_when_length_is_2(
1125 "some::module",
1126 Some("id"),
1127 Some("1,"),
1128 Some(3),
1129 "some::module id:1,"
1130 )]
1131 #[case::truncate_3_when_length_is_3(
1132 "some::module",
1133 Some("id"),
1134 Some("1, "),
1135 Some(3),
1136 "some::module id:1, "
1137 )]
1138 #[case::truncate_3_when_length_is_4(
1139 "some::module",
1140 Some("id"),
1141 Some("1, 2"),
1142 Some(3),
1143 "some::module id:1, ..."
1144 )]
1145 #[case::truncate_is_smaller_than_length(
1146 "some::module",
1147 Some("id"),
1148 Some("1, 2, 3, 4, 5"),
1149 Some(4),
1150 "some::module id:1, 2..."
1151 )]
1152 #[case::truncate_is_one_smaller_than_length(
1153 "some::module",
1154 Some("id"),
1155 Some("1, 2, 3"),
1156 Some(6),
1157 "some::module id:1, 2, ..."
1158 )]
1159 #[case::truncate_is_one_greater_than_length(
1160 "some::module",
1161 Some("id"),
1162 Some("1, 2, 3"),
1163 Some(8),
1164 "some::module id:1, 2, 3"
1165 )]
1166 #[case::truncate_is_far_greater_than_length(
1167 "some::module",
1168 Some("id"),
1169 Some("1, 2, 3"),
1170 Some(100),
1171 "some::module id:1, 2, 3"
1172 )]
1173 #[case::truncate_is_equal_to_length(
1174 "some::module",
1175 Some("id"),
1176 Some("1, 2, 3"),
1177 Some(7),
1178 "some::module id:1, 2, 3"
1179 )]
1180 #[case::description_is_empty(
1181 "some::module",
1182 Some("id"),
1183 Some(""),
1184 Some(100),
1185 "some::module id"
1186 )]
1187 fn test_header_display_when_truncate(
1188 #[case] module_path: &str,
1189 #[case] id: Option<&str>,
1190 #[case] description: Option<&str>,
1191 #[case] truncate_description: Option<usize>,
1192 #[case] expected: &str,
1193 ) {
1194 colored::control::set_override(false);
1195
1196 let output_format = OutputFormat {
1197 truncate_description,
1198 ..Default::default()
1199 };
1200
1201 let header = Header::new(
1202 &ModulePath::new(module_path),
1203 id.map(ToOwned::to_owned),
1204 description.map(ToOwned::to_owned),
1205 &output_format,
1206 );
1207
1208 assert_eq!(header.to_string(), expected);
1209 }
1210
1211 #[rstest]
1212 #[case::new_costs_0(EventKind::Ir, 0, None, "*********", None)]
1213 #[case::old_costs_0(EventKind::Ir, 1, Some(0), "+++inf+++", Some("+++inf+++"))]
1214 #[case::all_costs_0(EventKind::Ir, 0, Some(0), "No change", None)]
1215 #[case::new_costs_u64_max(EventKind::Ir, u64::MAX, None, "*********", None)]
1216 #[case::old_costs_u64_max(EventKind::Ir, u64::MAX / 10, Some(u64::MAX), "-90.0000%", Some("-10.0000x"))]
1217 #[case::all_costs_u64_max(EventKind::Ir, u64::MAX, Some(u64::MAX), "No change", None)]
1218 #[case::no_change_when_not_0(EventKind::Ir, 1000, Some(1000), "No change", None)]
1219 #[case::neg_change_when_not_0(EventKind::Ir, 2000, Some(3000), "-33.3333%", Some("-1.50000x"))]
1220 #[case::pos_change_when_not_0(EventKind::Ir, 2000, Some(1000), "+100.000%", Some("+2.00000x"))]
1221 #[case::pos_inf(EventKind::Ir, 2000, Some(0), "+++inf+++", Some("+++inf+++"))]
1222 #[case::neg_inf(EventKind::Ir, 0, Some(2000), "-100.000%", Some("---inf---"))]
1223 fn test_format_vertical_when_new_costs_are_present(
1224 #[case] event_kind: EventKind,
1225 #[case] new: u64,
1226 #[case] old: Option<u64>,
1227 #[case] diff_pct: &str,
1228 #[case] diff_fact: Option<&str>,
1229 ) {
1230 use crate::runner::summary::MetricsSummary;
1231
1232 colored::control::set_override(false);
1233
1234 let costs = match old {
1235 Some(old) => EitherOrBoth::Both(
1236 Metrics(indexmap! {event_kind => new}),
1237 Metrics(indexmap! {event_kind => old}),
1238 ),
1239 None => EitherOrBoth::Left(Metrics(indexmap! {event_kind => new})),
1240 };
1241 let metrics_summary = MetricsSummary::new(costs);
1242 let mut formatter = VerticalFormatter::new(OutputFormat::default());
1243 formatter.format_metrics(metrics_summary.all_diffs());
1244
1245 let expected = format!(
1246 " {:<21}{new:>METRIC_WIDTH$}|{:<METRIC_WIDTH$} ({diff_pct}){}\n",
1247 format!("{event_kind}:"),
1248 old.map_or(NOT_AVAILABLE.to_owned(), |o| o.to_string()),
1249 diff_fact.map_or_else(String::new, |f| format!(" [{f}]"))
1250 );
1251
1252 assert_eq!(formatter.buffer, expected);
1253 }
1254
1255 #[rstest]
1256 #[case::normal_no_grid(IndentKind::Normal, false, " ")]
1257 #[case::tool_header_no_grid(IndentKind::ToolHeadline, false, " ")]
1258 #[case::tool_sub_header_no_grid(IndentKind::ToolSubHeadline, false, " ")]
1259 #[case::normal_with_grid(IndentKind::Normal, true, "| ")]
1260 #[case::tool_header_with_grid(IndentKind::ToolHeadline, true, "|=")]
1261 #[case::tool_sub_header_with_grid(IndentKind::ToolSubHeadline, true, "|-")]
1262 fn test_vertical_formatter_write_indent(
1263 #[case] kind: IndentKind,
1264 #[case] show_grid: bool,
1265 #[case] expected: &str,
1266 ) {
1267 colored::control::set_override(false);
1268
1269 let output_format = OutputFormat {
1270 show_grid,
1271 ..Default::default()
1272 };
1273
1274 let mut formatter = VerticalFormatter::new(output_format);
1275 formatter.write_indent(&kind);
1276 assert_eq!(formatter.buffer, expected);
1277 }
1278
1279 #[rstest]
1280 #[case::left(
1281 "Some:",
1282 EitherOrBoth::Left("left"),
1283 " Some: left\n"
1284 )]
1285 #[case::right(
1286 "Field:",
1287 EitherOrBoth::Right("right"),
1288 " Field: |right\n"
1289 )]
1290 #[case::both(
1291 "Field:",
1292 EitherOrBoth::Both("left", "right"),
1293 " Field: left|right\n"
1294 )]
1295 #[case::both_u64_max(
1296 "Field:",
1297 EitherOrBoth::Both(format!("{}", u64::MAX), format!("{}", u64::MAX)),
1298 " Field: 18446744073709551615|18446744073709551615\n"
1299 )]
1300 #[case::split(
1301 "Field:",
1302 EitherOrBoth::Both(format!("{}1", u64::MAX), "right".to_owned()),
1303 " Field: 184467440737095516151\n |right\n"
1304 )]
1305 fn test_vertical_formatter_write_field<T>(
1306 #[case] field: &str,
1307 #[case] values: EitherOrBoth<T>,
1308 #[case] expected: &str,
1309 ) where
1310 T: AsRef<str>,
1311 {
1312 colored::control::set_override(false);
1313
1314 let output_format = OutputFormat::default();
1315
1316 let mut formatter = VerticalFormatter::new(output_format);
1317 formatter.write_field(field, &values, None, false);
1318 assert_eq!(formatter.buffer, expected);
1319 }
1320}