iai_callgrind_runner/runner/callgrind/
flamegraph.rs

1//! Module containing the callgrind flamegraph elements
2use std::borrow::Cow;
3use std::cmp::Ordering;
4use std::fs::File;
5use std::io::{BufWriter, Cursor, Write as IoWrite};
6use std::path::{Path, PathBuf};
7
8use anyhow::{Context, Result};
9use inferno::flamegraph::{Direction, Options};
10
11use super::flamegraph_parser::{FlamegraphMap, FlamegraphParser};
12use super::parser::{CallgrindParser, CallgrindProperties, Sentinel};
13use crate::api::{self, EventKind, FlamegraphKind};
14use crate::runner::summary::{BaselineKind, BaselineName, FlamegraphSummaries, FlamegraphSummary};
15use crate::runner::tool::path::{ToolOutputPath, ToolOutputPathKind};
16
17type ParserOutput = Vec<(PathBuf, CallgrindProperties, FlamegraphMap)>;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20enum OutputPathKind {
21    Regular,
22    Old,
23    Base(String),
24    DiffOld,
25    DiffBase(String),
26    DiffBases(String, String),
27}
28
29/// The generator for flamegraphs when not run with --load-baseline or --save-baseline
30#[derive(Debug)]
31pub struct BaselineFlamegraphGenerator {
32    /// The [`BaselineKind`]
33    pub baseline_kind: BaselineKind,
34}
35
36/// The main configuration for a flamegraph
37#[derive(Debug, Clone, PartialEq)]
38#[allow(clippy::struct_excessive_bools)]
39pub struct Config {
40    /// The direction of the flamegraph. Top to bottom or vice versa
41    pub direction: Direction,
42    /// The event kinds for which a flamegraph should be generated
43    pub event_kinds: Vec<EventKind>,
44    /// The [`FlamegraphKind`]
45    pub kind: FlamegraphKind,
46    /// The minimum width which should be displayed
47    pub min_width: f64,
48    /// If true, negate a differential flamegraph
49    pub negate_differential: bool,
50    /// If true, normalize a differential flamegraph
51    pub normalize_differential: bool,
52    /// The subtitle to use for the flamegraphs
53    pub subtitle: Option<String>,
54    /// The title to use for the flamegraphs
55    pub title: Option<String>,
56}
57
58/// The generated callgrind `Flamegraph`
59#[derive(Debug, Clone)]
60pub struct Flamegraph {
61    /// The [`Config`]
62    pub config: Config,
63}
64
65/// The generator for flamegraphs when run with --load-baseline
66#[derive(Debug)]
67pub struct LoadBaselineFlamegraphGenerator {
68    /// The baseline to compare with
69    pub baseline: BaselineName,
70    /// The loaded baseline with [`BaselineName`]
71    pub loaded_baseline: BaselineName,
72}
73
74#[derive(Debug, Clone)]
75struct OutputPath {
76    pub baseline_kind: BaselineKind,
77    pub dir: PathBuf,
78    pub event_kind: EventKind,
79    pub kind: OutputPathKind,
80    pub modifiers: Vec<String>,
81    pub name: String,
82}
83
84/// The generator for flamegraphs when run with --save-baseline
85#[derive(Debug)]
86pub struct SaveBaselineFlamegraphGenerator {
87    /// The baseline with [`BaselineName`]
88    pub baseline: BaselineName,
89}
90
91/// The trait a flamegraph generator needs to implement
92pub trait FlamegraphGenerator {
93    /// Create a new flamegraph generator
94    fn create(
95        &self,
96        flamegraph: &Flamegraph,
97        tool_output_path: &ToolOutputPath,
98        sentinel: Option<&Sentinel>,
99        project_root: &Path,
100    ) -> Result<Vec<FlamegraphSummary>>;
101}
102
103impl FlamegraphGenerator for BaselineFlamegraphGenerator {
104    fn create(
105        &self,
106        flamegraph: &Flamegraph,
107        tool_output_path: &ToolOutputPath,
108        sentinel: Option<&Sentinel>,
109        project_root: &Path,
110    ) -> Result<Vec<FlamegraphSummary>> {
111        // We need the dummy path just to clean up and organize the output files independently of
112        // the EventKind of the OutputPath
113        let mut output_path = OutputPath::new(tool_output_path, EventKind::Ir);
114        output_path.init()?;
115        output_path.to_diff_path().clear(true)?;
116        output_path.shift(true)?;
117        output_path.set_modifiers(["total"]);
118
119        if flamegraph.config.kind == FlamegraphKind::None
120            || flamegraph.config.event_kinds.is_empty()
121        {
122            return Ok(vec![]);
123        }
124
125        let (maps, base_maps) =
126            flamegraph.parse(tool_output_path, sentinel, project_root, false)?;
127
128        let total = total_flamegraph_map_from_parsed(&maps).unwrap();
129
130        let mut flamegraph_summaries = FlamegraphSummaries::default();
131        for event_kind in &flamegraph.config.event_kinds {
132            let mut flamegraph_summary = FlamegraphSummary::new(*event_kind);
133            output_path.set_event_kind(*event_kind);
134
135            let stacks_lines = total.to_stack_format(event_kind)?;
136            if flamegraph.is_regular() {
137                Flamegraph::write(
138                    &output_path,
139                    &mut flamegraph.options(*event_kind, output_path.file_name()),
140                    stacks_lines.iter().map(std::string::String::as_str),
141                )?;
142                flamegraph_summary.regular_path = Some(output_path.to_path());
143            }
144
145            if let Some(base_maps) = &base_maps {
146                let total_base = total_flamegraph_map_from_parsed(base_maps).unwrap();
147                // Is Some if FlamegraphKind::Differential or FlamegraphKind::All
148                Flamegraph::create_differential(
149                    &output_path,
150                    &mut flamegraph.options(*event_kind, output_path.to_diff_path().file_name()),
151                    &total_base,
152                    // This unwrap is safe since we always have differential options if the
153                    // flamegraph kind is differential
154                    flamegraph.differential_options().unwrap(),
155                    *event_kind,
156                    &stacks_lines,
157                )?;
158
159                flamegraph_summary.base_path = Some(output_path.to_base_path().to_path());
160                flamegraph_summary.diff_path = Some(output_path.to_diff_path().to_path());
161            }
162
163            flamegraph_summaries.totals.push(flamegraph_summary);
164        }
165
166        Ok(flamegraph_summaries.totals)
167    }
168}
169
170impl From<api::FlamegraphConfig> for Config {
171    fn from(value: api::FlamegraphConfig) -> Self {
172        Self {
173            kind: value.kind.unwrap_or(FlamegraphKind::All),
174            negate_differential: value.negate_differential.unwrap_or_default(),
175            normalize_differential: value.normalize_differential.unwrap_or(false),
176            event_kinds: value.event_kinds.unwrap_or_else(|| vec![EventKind::Ir]),
177            direction: value
178                .direction
179                .map_or_else(|| Direction::Inverted, std::convert::Into::into),
180            title: value.title.clone(),
181            subtitle: value.subtitle.clone(),
182            min_width: value.min_width.unwrap_or(0.1f64),
183        }
184    }
185}
186
187impl From<api::Direction> for Direction {
188    fn from(value: api::Direction) -> Self {
189        match value {
190            api::Direction::TopToBottom => Self::Inverted,
191            api::Direction::BottomToTop => Self::Straight,
192        }
193    }
194}
195
196impl Flamegraph {
197    /// Create a new `Flamegraph`
198    pub fn new(heading: String, mut config: Config) -> Self {
199        if config.title.is_none() {
200            config.title = Some(heading);
201        }
202
203        Self { config }
204    }
205
206    /// Return true if this flamegraph is a differential flamegraph
207    pub fn is_differential(&self) -> bool {
208        matches!(
209            self.config.kind,
210            FlamegraphKind::Differential | FlamegraphKind::All
211        )
212    }
213
214    /// Return true if this flamegraph is a regular flamegraph
215    pub fn is_regular(&self) -> bool {
216        matches!(
217            self.config.kind,
218            FlamegraphKind::Regular | FlamegraphKind::All
219        )
220    }
221
222    /// Return the [`Options`] of this flamegraph
223    pub fn options(&self, event_kind: EventKind, subtitle: String) -> Options<'_> {
224        let mut options = Options::default();
225        options.negate_differentials = self.config.negate_differential;
226        options.direction = self.config.direction;
227        options.title.clone_from(
228            self.config
229                .title
230                .as_ref()
231                .expect("A title must be present at this point"),
232        );
233
234        options.subtitle = if let Some(subtitle) = &self.config.subtitle {
235            Some(subtitle.clone())
236        } else {
237            Some(subtitle)
238        };
239
240        options.min_width = self.config.min_width;
241        options.count_name = event_kind.to_string();
242        options
243    }
244
245    /// Return the [`inferno::differential::Options`] for a differential flamegraph
246    pub fn differential_options(&self) -> Option<inferno::differential::Options> {
247        self.is_differential()
248            .then(|| inferno::differential::Options {
249                normalize: self.config.normalize_differential,
250                ..Default::default()
251            })
252    }
253
254    /// Parse the flamegraph
255    pub fn parse<P>(
256        &self,
257        tool_output_path: &ToolOutputPath,
258        sentinel: Option<&Sentinel>,
259        project_root: P,
260        no_differential: bool,
261    ) -> Result<(ParserOutput, Option<ParserOutput>)>
262    where
263        P: Into<PathBuf>,
264    {
265        let parser = FlamegraphParser::new(sentinel, project_root);
266        // We need this map in all remaining cases of `FlamegraphKinds`
267        let mut maps = parser.parse(tool_output_path)?;
268
269        let base_path = tool_output_path.to_base_path();
270        let mut base_maps = (!no_differential && self.is_differential() && base_path.exists())
271            .then(|| parser.parse(&base_path))
272            .transpose()?;
273
274        if self.config.event_kinds.iter().any(EventKind::is_derived) {
275            for map in &mut maps {
276                map.2.make_summary()?;
277            }
278            if let Some(maps) = base_maps.as_mut() {
279                for map in maps {
280                    map.2.make_summary()?;
281                }
282            }
283        }
284
285        Ok((maps, base_maps))
286    }
287
288    fn create_differential(
289        output_path: &OutputPath,
290        options: &mut inferno::flamegraph::Options,
291        base_map: &FlamegraphMap,
292        differential_options: inferno::differential::Options,
293        event_kind: EventKind,
294        stacks_lines: &[String],
295    ) -> Result<()> {
296        let base_stacks_lines = base_map.to_stack_format(&event_kind)?;
297
298        let cursor = Cursor::new(stacks_lines.join("\n"));
299        let base_cursor = Cursor::new(base_stacks_lines.join("\n"));
300        let mut result = Cursor::new(vec![]);
301
302        inferno::differential::from_readers(differential_options, base_cursor, cursor, &mut result)
303            .context("Failed creating a differential flamegraph")?;
304
305        let diff_output_path = output_path.to_diff_path();
306        Self::write(
307            &diff_output_path,
308            options,
309            String::from_utf8_lossy(result.get_ref()).lines(),
310        )
311    }
312
313    fn write<'stacks>(
314        output_path: &OutputPath,
315        options: &mut Options<'_>,
316        stacks: impl Iterator<Item = &'stacks str>,
317    ) -> Result<()> {
318        let path = output_path.to_path();
319        let mut writer = BufWriter::new(output_path.create()?);
320        inferno::flamegraph::from_lines(options, stacks, &mut writer)
321            .with_context(|| format!("Failed creating a flamegraph at '{}'", path.display()))?;
322
323        writer
324            .flush()
325            .with_context(|| format!("Failed flushing content to '{}'", path.display()))
326    }
327}
328
329impl FlamegraphGenerator for LoadBaselineFlamegraphGenerator {
330    fn create(
331        &self,
332        flamegraph: &Flamegraph,
333        tool_output_path: &ToolOutputPath,
334        sentinel: Option<&Sentinel>,
335        project_root: &Path,
336    ) -> Result<Vec<FlamegraphSummary>> {
337        // We need the dummy path just to clean up and organize the output files independently of
338        // the EventKind of the OutputPath
339        let mut output_path = OutputPath::new(tool_output_path, EventKind::Ir);
340
341        if flamegraph.config.kind == FlamegraphKind::None
342            || flamegraph.config.event_kinds.is_empty()
343            || !flamegraph.is_differential()
344        {
345            return Ok(vec![]);
346        }
347
348        output_path.to_diff_path().clear(true)?;
349        output_path.set_modifiers(["total"]);
350
351        let (maps, base_maps) = flamegraph
352            .parse(tool_output_path, sentinel, project_root, false)
353            .map(|(a, b)| (a, b.unwrap()))?;
354
355        let mut flamegraph_summaries = FlamegraphSummaries::default();
356        if let Some(total) = total_flamegraph_map_from_parsed(&maps) {
357            let base_total = total_flamegraph_map_from_parsed(&base_maps);
358
359            if let Some(base_total) = base_total {
360                for event_kind in &flamegraph.config.event_kinds {
361                    let mut flamegraph_summary = FlamegraphSummary::new(*event_kind);
362                    output_path.set_event_kind(*event_kind);
363
364                    Flamegraph::create_differential(
365                        &output_path,
366                        &mut flamegraph
367                            .options(*event_kind, output_path.to_diff_path().file_name()),
368                        &base_total,
369                        // This unwrap is safe since we always produce a differential flamegraph
370                        flamegraph.differential_options().unwrap(),
371                        *event_kind,
372                        &total.to_stack_format(event_kind)?,
373                    )?;
374
375                    flamegraph_summary.regular_path = Some(output_path.to_path());
376                    flamegraph_summary.base_path = Some(output_path.to_base_path().to_path());
377                    flamegraph_summary.diff_path = Some(output_path.to_diff_path().to_path());
378
379                    flamegraph_summaries.totals.push(flamegraph_summary);
380                }
381            }
382        }
383
384        Ok(flamegraph_summaries.totals)
385    }
386}
387
388impl OutputPath {
389    pub fn new(tool_output_path: &ToolOutputPath, event_kind: EventKind) -> Self {
390        Self {
391            kind: match &tool_output_path.kind {
392                ToolOutputPathKind::Out
393                | ToolOutputPathKind::Log
394                | ToolOutputPathKind::Xtree
395                | ToolOutputPathKind::Xleak => OutputPathKind::Regular,
396                ToolOutputPathKind::OldOut
397                | ToolOutputPathKind::OldLog
398                | ToolOutputPathKind::OldXtree
399                | ToolOutputPathKind::OldXleak => OutputPathKind::Old,
400                ToolOutputPathKind::BaseLog(name)
401                | ToolOutputPathKind::BaseOut(name)
402                | ToolOutputPathKind::BaseXtree(name)
403                | ToolOutputPathKind::BaseXleak(name) => OutputPathKind::Base(name.clone()),
404            },
405            event_kind,
406            baseline_kind: tool_output_path.baseline_kind.clone(),
407            dir: tool_output_path.dir.clone(),
408            name: tool_output_path.name.clone(),
409            modifiers: Vec::default(),
410        }
411    }
412
413    pub fn init(&self) -> Result<()> {
414        std::fs::create_dir_all(&self.dir).with_context(|| {
415            format!(
416                "Failed creating flamegraph directory '{}'",
417                self.dir.display()
418            )
419        })
420    }
421
422    pub fn create(&self) -> Result<File> {
423        let path = self.to_path();
424        File::create(&path)
425            .with_context(|| format!("Failed creating flamegraph file '{}'", path.display()))
426    }
427
428    pub fn clear(&self, ignore_event_kind: bool) -> Result<()> {
429        for path in self.real_paths(ignore_event_kind)? {
430            std::fs::remove_file(path)?;
431        }
432
433        Ok(())
434    }
435
436    /// This method will remove all differential flamegraphs for a specific base or old
437    ///
438    /// The differential flamegraphs with a base can end with the base name
439    /// (`*.diff.base@<name>.svg`) and/or with the parts until `flamegraph` removed start with the
440    /// base name (`base@<name>.diff.*`)
441    pub fn clear_diff(&self) -> Result<()> {
442        let extension = match &self.baseline_kind {
443            BaselineKind::Old => "diff.old.svg".to_owned(),
444            BaselineKind::Name(name) => format!("diff.base@{name}.svg"),
445        };
446        for entry in std::fs::read_dir(&self.dir)
447            .with_context(|| format!("Failed reading directory '{}'", self.dir.display()))?
448        {
449            let entry = entry?;
450            let file_name = entry.file_name().to_string_lossy().to_string();
451            if let Some(suffix) =
452                file_name.strip_prefix(format!("callgrind.{}", &self.name).as_str())
453            {
454                let path = entry.path();
455
456                if suffix.ends_with(extension.as_str()) {
457                    std::fs::remove_file(&path).with_context(|| {
458                        format!("Failed removing flamegraph file: '{}'", path.display())
459                    })?;
460                }
461
462                if let BaselineKind::Name(name) = &self.baseline_kind {
463                    if suffix
464                        .split('.')
465                        .skip_while(|p| *p != "flamegraph")
466                        .take(3)
467                        .eq([
468                            "flamegraph".to_owned(),
469                            format!("base@{name}"),
470                            "diff".to_owned(),
471                        ])
472                    {
473                        std::fs::remove_file(&path).with_context(|| {
474                            format!("Failed removing flamegraph file: '{}'", path.display())
475                        })?;
476                    }
477                } else {
478                    // do nothing
479                }
480            }
481        }
482
483        Ok(())
484    }
485
486    pub fn shift(&self, ignore_event_kind: bool) -> Result<()> {
487        match &self.baseline_kind {
488            BaselineKind::Old => {
489                self.to_base_path().clear(ignore_event_kind)?;
490                for path in self.real_paths(ignore_event_kind)? {
491                    let new_path = path.with_extension("old.svg");
492                    std::fs::rename(&path, &new_path).with_context(|| {
493                        format!(
494                            "Failed moving flamegraph file from '{}' to '{}'",
495                            path.display(),
496                            new_path.display()
497                        )
498                    })?;
499                }
500                Ok(())
501            }
502            BaselineKind::Name(_) => self.clear(ignore_event_kind),
503        }
504    }
505
506    pub fn to_diff_path(&self) -> Self {
507        Self {
508            kind: match (&self.kind, &self.baseline_kind) {
509                (OutputPathKind::Regular, BaselineKind::Old) => OutputPathKind::DiffOld,
510                (OutputPathKind::Regular, BaselineKind::Name(name)) => {
511                    OutputPathKind::DiffBase(name.to_string())
512                }
513                (OutputPathKind::Base(name), BaselineKind::Name(other)) => {
514                    OutputPathKind::DiffBases(name.clone(), other.to_string())
515                }
516                (OutputPathKind::Old | OutputPathKind::Base(_), _) => unreachable!(),
517                (value, _) => value.clone(),
518            },
519            ..self.clone()
520        }
521    }
522
523    pub fn to_base_path(&self) -> Self {
524        Self {
525            kind: match &self.baseline_kind {
526                BaselineKind::Old => OutputPathKind::Old,
527                BaselineKind::Name(name) => OutputPathKind::Base(name.to_string()),
528            },
529            ..self.clone()
530        }
531    }
532
533    pub fn extension(&self) -> String {
534        match &self.kind {
535            OutputPathKind::Regular => format!("{}.flamegraph.svg", self.event_kind.to_name()),
536            OutputPathKind::Old => format!("{}.flamegraph.old.svg", self.event_kind.to_name()),
537            OutputPathKind::Base(name) => {
538                format!("{}.flamegraph.base@{name}.svg", self.event_kind.to_name())
539            }
540            OutputPathKind::DiffOld => {
541                format!("{}.flamegraph.diff.old.svg", self.event_kind.to_name())
542            }
543            OutputPathKind::DiffBase(name) => {
544                format!(
545                    "{}.flamegraph.diff.base@{name}.svg",
546                    self.event_kind.to_name()
547                )
548            }
549            OutputPathKind::DiffBases(name, base) => {
550                format!(
551                    "{}.flamegraph.base@{name}.diff.base@{base}.svg",
552                    self.event_kind.to_name()
553                )
554            }
555        }
556    }
557
558    pub fn set_modifiers<I, T>(&mut self, modifiers: T)
559    where
560        T: IntoIterator<Item = I>,
561        I: Into<String>,
562    {
563        self.modifiers = modifiers.into_iter().map(Into::into).collect();
564    }
565
566    pub fn set_event_kind(&mut self, event_kind: EventKind) {
567        self.event_kind = event_kind;
568    }
569
570    pub fn real_paths(&self, ignore_event_kind: bool) -> Result<Vec<PathBuf>> {
571        let extension = self.extension();
572        let to_match = if ignore_event_kind {
573            extension
574                .split_once('.')
575                .expect("The '.' delimiter should be present at least once")
576                .1
577        } else {
578            &extension
579        };
580
581        let mut paths = vec![];
582        for entry in std::fs::read_dir(&self.dir)
583            .with_context(|| format!("Failed reading directory '{}'", self.dir.display()))?
584        {
585            let path = entry?;
586            let file_name = path.file_name().to_string_lossy().to_string();
587            if let Some(suffix) =
588                file_name.strip_prefix(format!("callgrind.{}.", &self.name).as_str())
589            {
590                if suffix.ends_with(to_match) {
591                    paths.push(path.path());
592                }
593            }
594        }
595
596        Ok(paths)
597    }
598
599    pub fn file_name(&self) -> String {
600        if self.modifiers.is_empty() {
601            format!("callgrind.{}.{}", self.name, self.extension())
602        } else {
603            format!(
604                "callgrind.{}.{}.{}",
605                self.name,
606                self.modifiers.join("."),
607                self.extension()
608            )
609        }
610    }
611
612    pub fn to_path(&self) -> PathBuf {
613        self.dir.join(self.file_name())
614    }
615}
616
617impl FlamegraphGenerator for SaveBaselineFlamegraphGenerator {
618    fn create(
619        &self,
620        flamegraph: &Flamegraph,
621        tool_output_path: &ToolOutputPath,
622        sentinel: Option<&Sentinel>,
623        project_root: &Path,
624    ) -> Result<Vec<FlamegraphSummary>> {
625        // We need the dummy path just to clean up and organize the output files independently of
626        // the EventKind of the OutputPath
627        let mut output_path = OutputPath::new(tool_output_path, EventKind::Ir);
628        output_path.init()?;
629        output_path.clear(true)?;
630        output_path.clear_diff()?;
631        output_path.set_modifiers(["total"]);
632
633        if flamegraph.config.kind == FlamegraphKind::None
634            || flamegraph.config.event_kinds.is_empty()
635            || !flamegraph.is_regular()
636        {
637            return Ok(vec![]);
638        }
639
640        let (maps, _) = flamegraph.parse(tool_output_path, sentinel, project_root, true)?;
641        let total_map = total_flamegraph_map_from_parsed(&maps).unwrap();
642
643        let mut flamegraph_summaries = FlamegraphSummaries::default();
644        for event_kind in &flamegraph.config.event_kinds {
645            let mut flamegraph_summary = FlamegraphSummary::new(*event_kind);
646            output_path.set_event_kind(*event_kind);
647
648            Flamegraph::write(
649                &output_path,
650                &mut flamegraph.options(*event_kind, output_path.file_name()),
651                total_map
652                    .to_stack_format(event_kind)?
653                    .iter()
654                    .map(String::as_str),
655            )?;
656
657            flamegraph_summary.regular_path = Some(output_path.to_path());
658            flamegraph_summaries.summaries.push(flamegraph_summary);
659        }
660
661        Ok(flamegraph_summaries.totals)
662    }
663}
664
665fn total_flamegraph_map_from_parsed(maps: &ParserOutput) -> Option<Cow<'_, FlamegraphMap>> {
666    match maps.len().cmp(&1) {
667        Ordering::Less => None,
668        Ordering::Equal => Some(Cow::Borrowed(&maps[0].2)),
669        Ordering::Greater => {
670            let mut total = maps[0].2.clone();
671            for (_, _, map) in maps.iter().skip(1) {
672                total.add(map);
673            }
674            Some(Cow::Owned(total))
675        }
676    }
677}