iai_callgrind_runner/runner/
format.rs

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
18/// The subset of callgrind metrics to format in the given order
19pub 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
43/// The error metrics to format in the given order
44pub const ERROR_METRICS_DEFAULT: [ErrorMetricKind; 4] = [
45    ErrorMetricKind::Errors,
46    ErrorMetricKind::Contexts,
47    ErrorMetricKind::SuppressedErrors,
48    ErrorMetricKind::SuppressedContexts,
49];
50
51/// The subset of dhat metrics to format in the given order
52pub 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
63/// The string used to signal that a value is not available
64pub 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
74/// The `DIFF_WIDTH` - the length of the unit
75pub const FLOAT_WIDTH: usize = DIFF_WIDTH - 1;
76
77#[allow(clippy::doc_link_with_quotes)]
78/// The maximum line width
79///
80/// indent + left + "|" + metric width + " " + "(" + percentage + ")" + " " + "[" + factor + "]"
81pub 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            // do nothing
352        }
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    /// Create a new `VerticalFormatter` (the default format)
433    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    /// Print the internal buffer as is and clear it afterwards
454    pub fn print_buffer(&mut self) {
455        print!("{}", self.buffer);
456        self.clear();
457    }
458
459    /// Write the indentation depending on the chosen [`OutputFormat`] and [`IndentKind`]
460    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    /// Format the baseline
624    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                // We only check for `new` errors
834                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            // Since there is no total, show_all is partly ignored, and we show all data in a little
900            // bit more aggregated form without the multiple files headlines. This affects currently
901            // the output of `Massif` and `BBV`.
902            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            // no data to show
913        }
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
965// Return the formatted `String` if `NoCapture` is not `False`
966pub 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    // These unwraps are safe because `no_capture_footer` returns None only if `NoCapture` is
995    // `False`
996    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}