1use 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#[derive(Debug)]
31pub struct BaselineFlamegraphGenerator {
32 pub baseline_kind: BaselineKind,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38#[allow(clippy::struct_excessive_bools)]
39pub struct Config {
40 pub direction: Direction,
42 pub event_kinds: Vec<EventKind>,
44 pub kind: FlamegraphKind,
46 pub min_width: f64,
48 pub negate_differential: bool,
50 pub normalize_differential: bool,
52 pub subtitle: Option<String>,
54 pub title: Option<String>,
56}
57
58#[derive(Debug, Clone)]
60pub struct Flamegraph {
61 pub config: Config,
63}
64
65#[derive(Debug)]
67pub struct LoadBaselineFlamegraphGenerator {
68 pub baseline: BaselineName,
70 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#[derive(Debug)]
86pub struct SaveBaselineFlamegraphGenerator {
87 pub baseline: BaselineName,
89}
90
91pub trait FlamegraphGenerator {
93 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 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 Flamegraph::create_differential(
149 &output_path,
150 &mut flamegraph.options(*event_kind, output_path.to_diff_path().file_name()),
151 &total_base,
152 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 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 pub fn is_differential(&self) -> bool {
208 matches!(
209 self.config.kind,
210 FlamegraphKind::Differential | FlamegraphKind::All
211 )
212 }
213
214 pub fn is_regular(&self) -> bool {
216 matches!(
217 self.config.kind,
218 FlamegraphKind::Regular | FlamegraphKind::All
219 )
220 }
221
222 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 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 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 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 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 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 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 }
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 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}