iai_callgrind_runner/runner/tool/
config.rs

1//! The module containing the [`ToolConfig`] and other related elements
2
3use std::collections::HashMap;
4use std::ffi::OsString;
5use std::io::stderr;
6use std::path::Path;
7
8use anyhow::{anyhow, Result};
9
10use super::args::ToolArgs;
11use super::parser::{parser_factory, ParserOutput};
12use super::path::ToolOutputPath;
13use super::regression::{RegressionConfig, ToolRegressionConfig};
14use super::run::{RunOptions, ToolCommand};
15use crate::api::{self, EntryPoint, RawArgs, Tool, Tools, ValgrindTool};
16use crate::runner::args::NoCapture;
17use crate::runner::callgrind::flamegraph::{
18    BaselineFlamegraphGenerator, Config as FlamegraphConfig, Flamegraph, FlamegraphGenerator,
19    LoadBaselineFlamegraphGenerator, SaveBaselineFlamegraphGenerator,
20};
21use crate::runner::callgrind::parser::Sentinel;
22use crate::runner::common::{Baselines, Config, ModulePath, Sandbox};
23use crate::runner::format::{print_no_capture_footer, Formatter, OutputFormat, VerticalFormatter};
24use crate::runner::meta::Metadata;
25use crate::runner::summary::{
26    BaselineKind, BaselineName, BenchmarkSummary, Profile, ProfileData, ProfileTotal,
27    ToolMetricSummary, ToolRegression,
28};
29use crate::runner::{cachegrind, callgrind, DEFAULT_TOGGLE};
30use crate::util::Glob;
31
32/// The tool specific flamegraph configuration
33#[derive(Debug, Clone, PartialEq)]
34pub enum ToolFlamegraphConfig {
35    /// The callgrind configuration
36    Callgrind(FlamegraphConfig),
37    /// If there is no configuration
38    None,
39}
40
41/// The [`ToolConfig`] containing the basic configuration values to run the benchmark for this tool
42#[derive(Debug, Clone)]
43pub struct ToolConfig {
44    /// The arguments to pass to the valgrind executable
45    pub args: ToolArgs,
46    /// The [`EntryPoint`] of this tool
47    pub entry_point: EntryPoint,
48    /// The tool specific flamegraph configuration
49    pub flamegraph_config: ToolFlamegraphConfig,
50    /// The [`Glob`] patterns used to matched a function in the call stack of a program point
51    pub frames: Vec<Glob>,
52    /// If true, this tool is the default tool for the benchmark run
53    pub is_default: bool,
54    /// If true, this tool is enabled for this benchmark
55    pub is_enabled: bool,
56    /// The tool specific regression check configuration
57    pub regression_config: ToolRegressionConfig,
58    /// The [`ValgrindTool`]
59    pub tool: ValgrindTool,
60}
61
62#[derive(Debug)]
63struct ToolConfigBuilder {
64    entry_point: Option<EntryPoint>,
65    flamegraph_config: ToolFlamegraphConfig,
66    frames: Vec<String>,
67    is_default: bool,
68    is_enabled: bool,
69    kind: ValgrindTool,
70    raw_args: RawArgs,
71    regression_config: ToolRegressionConfig,
72    tool: Option<Tool>,
73}
74
75/// Multiple [`ToolConfig`]s
76#[derive(Debug, Clone)]
77pub struct ToolConfigs(pub Vec<ToolConfig>);
78
79impl ToolConfig {
80    /// Create a new `ToolConfig`
81    pub fn new(
82        tool: ValgrindTool,
83        is_enabled: bool,
84        args: ToolArgs,
85        regression_config: ToolRegressionConfig,
86        flamegraph_config: ToolFlamegraphConfig,
87        entry_point: EntryPoint,
88        is_default: bool,
89        frames: Vec<Glob>,
90    ) -> Self {
91        Self {
92            args,
93            entry_point,
94            flamegraph_config,
95            frames,
96            is_default,
97            is_enabled,
98            regression_config,
99            tool,
100        }
101    }
102
103    /// Parse the [`Profile`] from profile data or log files
104    pub fn parse(
105        &self,
106        meta: &Metadata,
107        output_path: &ToolOutputPath,
108        parsed_old: Option<Vec<ParserOutput>>,
109    ) -> Result<Profile> {
110        let parser = parser_factory(self, meta.project_root.clone(), output_path);
111
112        let parsed_new = parser.parse()?;
113        let parsed_old = if let Some(parsed_old) = parsed_old {
114            parsed_old
115        } else {
116            parser.parse_base()?
117        };
118
119        let data = match (parsed_new.is_empty(), parsed_old.is_empty()) {
120            (true, false | true) => return Err(anyhow!("A new dataset should always be present")),
121            (false, true) => ProfileData::new(parsed_new, None),
122            (false, false) => ProfileData::new(parsed_new, Some(parsed_old)),
123        };
124
125        Ok(Profile {
126            tool: self.tool,
127            log_paths: output_path.to_log_output().real_paths()?,
128            out_paths: output_path.real_paths()?,
129            summaries: data,
130            flamegraphs: vec![],
131        })
132    }
133
134    fn print(
135        &self,
136        config: &Config,
137        output_format: &OutputFormat,
138        data: &ProfileData,
139        baselines: &Baselines,
140    ) -> Result<()> {
141        VerticalFormatter::new(output_format.clone()).print(
142            self.tool,
143            config,
144            baselines,
145            data,
146            self.is_default,
147        )
148    }
149}
150
151impl ToolConfigBuilder {
152    fn build(self) -> Result<ToolConfig> {
153        let args = match self.kind {
154            ValgrindTool::Callgrind => {
155                callgrind::args::Args::try_from_raw_args(&[&self.raw_args])?.into()
156            }
157            ValgrindTool::Cachegrind => {
158                cachegrind::args::Args::try_from_raw_args(&[&self.raw_args])?.into()
159            }
160            _ => ToolArgs::try_from_raw_args(self.kind, &[&self.raw_args])?,
161        };
162
163        Ok(ToolConfig::new(
164            self.kind,
165            self.is_enabled,
166            args,
167            self.regression_config,
168            self.flamegraph_config,
169            self.entry_point.unwrap_or(EntryPoint::None),
170            self.is_default,
171            self.frames.iter().map(Into::into).collect(),
172        ))
173    }
174
175    /// Build the entry point
176    ///
177    /// The `default_entry_point` can be different for example for binary benchmarks and library
178    /// benchmarks.
179    fn entry_point(
180        &mut self,
181        default_entry_point: &EntryPoint,
182        module_path: &ModulePath,
183        id: Option<&String>,
184    ) {
185        match self.kind {
186            ValgrindTool::Callgrind => {
187                let entry_point = self
188                    .tool
189                    .as_ref()
190                    .and_then(|t| t.entry_point.clone())
191                    .unwrap_or_else(|| default_entry_point.clone());
192
193                match &entry_point {
194                    EntryPoint::None => {}
195                    EntryPoint::Default => {
196                        self.raw_args
197                            .extend_ignore_flag(&[format!("toggle-collect={DEFAULT_TOGGLE}")]);
198                    }
199                    EntryPoint::Custom(custom) => {
200                        self.raw_args
201                            .extend_ignore_flag(&[format!("toggle-collect={custom}")]);
202                    }
203                }
204
205                self.entry_point = Some(entry_point);
206            }
207            ValgrindTool::DHAT => {
208                let entry_point = self
209                    .tool
210                    .as_ref()
211                    .and_then(|t| t.entry_point.clone())
212                    .unwrap_or_else(|| default_entry_point.clone());
213
214                if entry_point == EntryPoint::Default {
215                    let mut frames = if let Some(tool) = self.tool.as_ref() {
216                        if let Some(frames) = &tool.frames {
217                            frames.clone()
218                        } else {
219                            Vec::default()
220                        }
221                    } else {
222                        Vec::default()
223                    };
224
225                    // DHAT does not resolve function calls the same way as callgrind does. Somehow
226                    // the benchmark function matched by the `DEFAULT_TOGGLE` gets sometimes inlined
227                    // (although annotated with `#[inline(never)]`), so we need to fall back to the
228                    // next best thing which is the function that calls the benchmark function. At
229                    // this point the module path consists of `file::group::function`. The group in
230                    // the path is artificial and we need the real function path within the
231                    // benchmark file to create a matching glob pattern. That real path consists of
232                    // `file::module::id`. The `id`-function won't be matched literally but with a
233                    // wildcard to address the problem of functions with the same body being
234                    // condensed into a single function by the compiler. Since in rare cases that
235                    // can happen across modules the `module` is matched with a glob, too.
236                    if let [first, _, last] = module_path.components()[..] {
237                        frames.push(format!("{first}::{last}::*"));
238                        if let Some(id) = id {
239                            frames.push(format!("{first}::*::{id}"));
240                        }
241                    }
242
243                    self.frames = frames;
244                }
245
246                self.entry_point = Some(entry_point);
247            }
248            ValgrindTool::Cachegrind
249            | ValgrindTool::Memcheck
250            | ValgrindTool::Helgrind
251            | ValgrindTool::DRD
252            | ValgrindTool::Massif
253            | ValgrindTool::BBV => {}
254        }
255    }
256
257    fn flamegraph_config(&mut self) {
258        if let Some(tool) = &self.tool {
259            if let Some(flamegraph_config) = &tool.flamegraph_config {
260                self.flamegraph_config = flamegraph_config.clone().into();
261            }
262        }
263    }
264
265    fn meta_args(&mut self, meta: &Metadata) {
266        let raw_args = match self.kind {
267            ValgrindTool::Callgrind => &meta.args.callgrind_args,
268            ValgrindTool::Cachegrind => &meta.args.cachegrind_args,
269            ValgrindTool::DHAT => &meta.args.dhat_args,
270            ValgrindTool::Memcheck => &meta.args.memcheck_args,
271            ValgrindTool::Helgrind => &meta.args.helgrind_args,
272            ValgrindTool::DRD => &meta.args.drd_args,
273            ValgrindTool::Massif => &meta.args.massif_args,
274            ValgrindTool::BBV => &meta.args.bbv_args,
275        };
276
277        if let Some(args) = raw_args {
278            self.raw_args.update(args);
279        }
280    }
281
282    fn new(
283        valgrind_tool: ValgrindTool,
284        tool: Option<Tool>,
285        is_default: bool,
286        default_args: &HashMap<ValgrindTool, RawArgs>,
287        module_path: &ModulePath,
288        id: Option<&String>,
289        meta: &Metadata,
290        valgrind_args: &RawArgs,
291        default_entry_point: &EntryPoint,
292    ) -> Result<Self> {
293        let mut builder = Self {
294            is_enabled: is_default || tool.as_ref().map_or(true, |t| t.enable.unwrap_or(true)),
295            tool,
296            entry_point: Option::default(),
297            flamegraph_config: ToolFlamegraphConfig::None,
298            frames: Vec::default(),
299            is_default,
300            raw_args: default_args
301                .get(&valgrind_tool)
302                .cloned()
303                .unwrap_or_default(),
304            regression_config: ToolRegressionConfig::None,
305            kind: valgrind_tool,
306        };
307
308        // Since the construction sequence is currently always the same, the construction of the
309        // `ToolConfig` can happen here in one go instead of having a separate director for it.
310        builder.valgrind_args(valgrind_args);
311        builder.entry_point(default_entry_point, module_path, id);
312        builder.tool_args();
313        builder.meta_args(meta);
314        builder.flamegraph_config();
315        builder.regression_config(meta)?;
316
317        Ok(builder)
318    }
319
320    fn regression_config(&mut self, meta: &Metadata) -> Result<()> {
321        let meta_limits = match self.kind {
322            ValgrindTool::Callgrind => meta.args.callgrind_limits.clone(),
323            ValgrindTool::Cachegrind => meta.args.cachegrind_limits.clone(),
324            ValgrindTool::DHAT => meta.args.dhat_limits.clone(),
325            _ => None,
326        };
327
328        let mut regression_config = if let Some(tool) = &self.tool {
329            meta_limits
330                .map(Ok)
331                .or_else(|| tool.regression_config.clone().map(TryInto::try_into))
332                .transpose()
333                .map_err(|error| anyhow!("Invalid limits for {}: {error}", self.kind))?
334                .unwrap_or(ToolRegressionConfig::None)
335        } else {
336            meta_limits.unwrap_or(ToolRegressionConfig::None)
337        };
338
339        if let Some(fail_fast) = meta.args.regression_fail_fast {
340            match &mut regression_config {
341                ToolRegressionConfig::Callgrind(callgrind_regression_config) => {
342                    callgrind_regression_config.fail_fast = fail_fast;
343                }
344                ToolRegressionConfig::Cachegrind(cachegrind_regression_config) => {
345                    cachegrind_regression_config.fail_fast = fail_fast;
346                }
347                ToolRegressionConfig::Dhat(dhat_regression_config) => {
348                    dhat_regression_config.fail_fast = fail_fast;
349                }
350                ToolRegressionConfig::None => {}
351            }
352        }
353
354        self.regression_config = regression_config;
355
356        Ok(())
357    }
358
359    fn tool_args(&mut self) {
360        if let Some(tool) = self.tool.as_ref() {
361            self.raw_args.update(&tool.raw_args);
362        }
363    }
364
365    fn valgrind_args(&mut self, valgrind_args: &RawArgs) {
366        self.raw_args.update(valgrind_args);
367    }
368}
369
370impl ToolConfigs {
371    /// Create new `ToolConfigs`
372    ///
373    /// `default_entry_point` is callgrind specific and specified here because it is different for
374    /// library and binary benchmarks.
375    ///
376    /// `default_args` should only contain command-line arguments which are different for library
377    /// and binary benchmarks on a per tool basis. Usually, default arguments are part of the tool
378    /// specific `Args` struct for example for callgrind [`callgrind::args::Args`] or cachegrind
379    /// [`cachegrind::args::Args`].
380    ///
381    /// `valgrind_args` are from the in-benchmark configuration: `LibraryBenchmarkConfig` or
382    /// `BinaryBenchmarkConfig`
383    ///
384    /// # Errors
385    ///
386    /// This function will return an error if the configs cannot be created
387    pub fn new(
388        output_format: &mut OutputFormat,
389        mut tools: Tools,
390        module_path: &ModulePath,
391        id: Option<&String>,
392        meta: &Metadata,
393        default_tool: ValgrindTool,
394        default_entry_point: &EntryPoint,
395        valgrind_args: &RawArgs,
396        default_args: &HashMap<ValgrindTool, RawArgs>,
397    ) -> Result<Self> {
398        let extracted_tool = tools.consume(default_tool);
399
400        output_format.update(extracted_tool.as_ref());
401        let default_tool_config = ToolConfigBuilder::new(
402            default_tool,
403            extracted_tool,
404            true,
405            default_args,
406            module_path,
407            id,
408            meta,
409            valgrind_args,
410            default_entry_point,
411        )?
412        .build()?;
413
414        // The tool selection from the command line or env args overwrites the tool selection from
415        // the benchmark file. However, any tool configurations from the benchmark files are
416        // preserved.
417        let meta_tools = if meta.args.tools.is_empty() {
418            tools.0
419        } else {
420            let mut meta_tools = Vec::with_capacity(meta.args.tools.len());
421            for kind in &meta.args.tools {
422                if let Some(tool) = tools.consume(*kind) {
423                    meta_tools.push(tool);
424                } else {
425                    meta_tools.push(Tool::new(*kind));
426                }
427            }
428            meta_tools
429        };
430
431        let mut tool_configs = Self(vec![default_tool_config]);
432        tool_configs.extend(meta_tools.into_iter().map(|tool| {
433            output_format.update(Some(&tool));
434
435            ToolConfigBuilder::new(
436                tool.kind,
437                Some(tool),
438                false,
439                default_args,
440                module_path,
441                id,
442                meta,
443                valgrind_args,
444                default_entry_point,
445            )?
446            .build()
447        }))?;
448
449        output_format.update_from_meta(meta);
450        Ok(tool_configs)
451    }
452
453    /// Return true if there are any [`Tool`]s enabled
454    pub fn has_tools_enabled(&self) -> bool {
455        self.0.iter().any(|t| t.is_enabled)
456    }
457
458    /// Return true if there are multiple tools configured and are enabled
459    pub fn has_multiple(&self) -> bool {
460        self.0.len() > 1 && self.0.iter().filter(|f| f.is_enabled).count() > 1
461    }
462
463    /// Return all [`ToolOutputPath`]s of all enabled tools
464    pub fn output_paths(&self, output_path: &ToolOutputPath) -> Vec<ToolOutputPath> {
465        self.0
466            .iter()
467            .filter(|t| t.is_enabled)
468            .map(|t| output_path.to_tool_output(t.tool))
469            .collect()
470    }
471
472    /// Extend this collection of tools with the contents of an iterator
473    pub fn extend<I>(&mut self, iter: I) -> Result<()>
474    where
475        I: Iterator<Item = Result<ToolConfig>>,
476    {
477        for a in iter {
478            self.0.push(a?);
479        }
480
481        Ok(())
482    }
483
484    fn print_headline(&self, tool_config: &ToolConfig, output_format: &OutputFormat) {
485        if output_format.is_default()
486            && (self.has_multiple() || tool_config.tool != ValgrindTool::Callgrind)
487        {
488            let mut formatter = VerticalFormatter::new(output_format.clone());
489            formatter.format_tool_headline(tool_config.tool);
490            formatter.print_buffer();
491        }
492    }
493
494    /// Check for regressions as defined in [`RegressionConfig`] and print an error if a regression
495    /// occurred
496    ///
497    /// # Panics
498    ///
499    /// Checking performance regressions for other tools than callgrind and cachegrind is not
500    /// implemented and panics
501    fn check_and_print_regressions(
502        tool_regression_config: &ToolRegressionConfig,
503        tool_total: &ProfileTotal,
504    ) -> Vec<ToolRegression> {
505        match (tool_regression_config, &tool_total.summary) {
506            (
507                ToolRegressionConfig::Callgrind(callgrind_regression_config),
508                ToolMetricSummary::Callgrind(metrics_summary),
509            ) => callgrind_regression_config.check_and_print(metrics_summary),
510            (
511                ToolRegressionConfig::Cachegrind(cachegrind_regression_config),
512                ToolMetricSummary::Cachegrind(metrics_summary),
513            ) => cachegrind_regression_config.check_and_print(metrics_summary),
514            (
515                ToolRegressionConfig::Dhat(dhat_regression_config),
516                ToolMetricSummary::Dhat(metrics_summary),
517            ) => dhat_regression_config.check_and_print(metrics_summary),
518            (ToolRegressionConfig::None, _) => vec![],
519            _ => {
520                panic!("The summary type should match the regression config")
521            }
522        }
523    }
524
525    /// Run a benchmark when --load-baseline was given
526    pub fn run_loaded_vs_base(
527        &self,
528        title: &str,
529        baseline: &BaselineName,
530        loaded_baseline: &BaselineName,
531        mut benchmark_summary: BenchmarkSummary,
532        baselines: &Baselines,
533        config: &Config,
534        output_path: &ToolOutputPath,
535        output_format: &OutputFormat,
536    ) -> Result<BenchmarkSummary> {
537        for tool_config in self.0.iter().filter(|t| t.is_enabled) {
538            self.print_headline(tool_config, output_format);
539
540            let tool = tool_config.tool;
541            let output_path = output_path.to_tool_output(tool);
542
543            let mut profile = tool_config.parse(&config.meta, &output_path, None)?;
544
545            tool_config.print(config, output_format, &profile.summaries, baselines)?;
546            profile.summaries.total.regressions = Self::check_and_print_regressions(
547                &tool_config.regression_config,
548                &profile.summaries.total,
549            );
550
551            if ValgrindTool::Callgrind == tool {
552                if let ToolFlamegraphConfig::Callgrind(flamegraph_config) =
553                    &tool_config.flamegraph_config
554                {
555                    profile.flamegraphs = LoadBaselineFlamegraphGenerator {
556                        loaded_baseline: loaded_baseline.clone(),
557                        baseline: baseline.clone(),
558                    }
559                    .create(
560                        &Flamegraph::new(title.to_owned(), flamegraph_config.to_owned()),
561                        &output_path,
562                        (tool_config.entry_point == EntryPoint::Default)
563                            .then(Sentinel::default)
564                            .as_ref(),
565                        &config.meta.project_root,
566                    )?;
567                }
568            }
569
570            benchmark_summary.profiles.push(profile);
571
572            let log_path = output_path.to_log_output();
573            log_path.dump_log(log::Level::Info, &mut stderr())?;
574        }
575
576        Ok(benchmark_summary)
577    }
578
579    /// Run a benchmark with this configuration if not --load-baseline was given
580    #[allow(clippy::too_many_lines)]
581    pub fn run(
582        &self,
583        title: &str,
584        mut benchmark_summary: BenchmarkSummary,
585        baselines: &Baselines,
586        baseline_kind: &BaselineKind,
587        config: &Config,
588        executable: &Path,
589        executable_args: &[OsString],
590        run_options: &RunOptions,
591        output_path: &ToolOutputPath,
592        save_baseline: bool,
593        module_path: &ModulePath,
594        output_format: &OutputFormat,
595    ) -> Result<BenchmarkSummary> {
596        for tool_config in self.0.iter().filter(|t| t.is_enabled) {
597            // Print the headline as soon as possible, so if there are any errors, the errors shown
598            // in the terminal output can be associated with the tool
599            self.print_headline(tool_config, output_format);
600
601            let tool = tool_config.tool;
602
603            let nocapture = if tool_config.is_default {
604                config.meta.args.nocapture
605            } else {
606                NoCapture::False
607            };
608            let command = ToolCommand::new(tool, &config.meta, nocapture);
609
610            let output_path = output_path.to_tool_output(tool);
611
612            let parser =
613                parser_factory(tool_config, config.meta.project_root.clone(), &output_path);
614            let parsed_old = parser.parse_base()?;
615
616            let log_path = output_path.to_log_output();
617
618            if save_baseline {
619                output_path.clear()?;
620                log_path.clear()?;
621                if let Some(path) = output_path.to_xtree_output() {
622                    path.clear()?;
623                }
624                if let Some(path) = output_path.to_xleak_output() {
625                    path.clear()?;
626                }
627            }
628
629            // We're implicitly applying the default here: In the absence of a user provided sandbox
630            // we don't run the benchmarks in a sandbox. Everything from here on runs
631            // with the current directory set to the sandbox directory until the sandbox
632            // is reset.
633            let sandbox = run_options
634                .sandbox
635                .as_ref()
636                .map(|sandbox| Sandbox::setup(sandbox, &config.meta))
637                .transpose()?;
638
639            let mut child = run_options
640                .setup
641                .as_ref()
642                .map_or(Ok(None), |setup| setup.run(config, module_path))?;
643
644            if let Some(delay) = run_options.delay.as_ref() {
645                if let Err(error) = delay.run() {
646                    if let Some(mut child) = child.take() {
647                        // To avoid zombies
648                        child.kill()?;
649                        return Err(error);
650                    }
651                }
652            }
653
654            let output = command.run(
655                tool_config.clone(),
656                executable,
657                executable_args,
658                run_options.clone(),
659                &output_path,
660                module_path,
661                child,
662            )?;
663
664            if let Some(teardown) = run_options.teardown.as_ref() {
665                teardown.run(config, module_path)?;
666            }
667
668            // We print the no capture footer after the teardown to keep the output consistent with
669            // library benchmarks.
670            print_no_capture_footer(
671                nocapture,
672                run_options.stdout.as_ref(),
673                run_options.stderr.as_ref(),
674            );
675
676            if let Some(sandbox) = sandbox {
677                sandbox.reset()?;
678            }
679
680            let mut profile = tool_config.parse(&config.meta, &output_path, Some(parsed_old))?;
681
682            tool_config.print(config, output_format, &profile.summaries, baselines)?;
683            profile.summaries.total.regressions = Self::check_and_print_regressions(
684                &tool_config.regression_config,
685                &profile.summaries.total,
686            );
687
688            if tool_config.tool == ValgrindTool::Callgrind {
689                if save_baseline {
690                    let BaselineKind::Name(baseline) = baseline_kind.clone() else {
691                        panic!("A baseline with name should be present");
692                    };
693                    if let ToolFlamegraphConfig::Callgrind(flamegraph_config) =
694                        &tool_config.flamegraph_config
695                    {
696                        profile.flamegraphs = SaveBaselineFlamegraphGenerator { baseline }.create(
697                            &Flamegraph::new(title.to_owned(), flamegraph_config.to_owned()),
698                            &output_path,
699                            (tool_config.entry_point == EntryPoint::Default)
700                                .then(Sentinel::default)
701                                .as_ref(),
702                            &config.meta.project_root,
703                        )?;
704                    }
705                } else if let ToolFlamegraphConfig::Callgrind(flamegraph_config) =
706                    &tool_config.flamegraph_config
707                {
708                    profile.flamegraphs = BaselineFlamegraphGenerator {
709                        baseline_kind: baseline_kind.clone(),
710                    }
711                    .create(
712                        &Flamegraph::new(title.to_owned(), flamegraph_config.to_owned()),
713                        &output_path,
714                        (tool_config.entry_point == EntryPoint::Default)
715                            .then(Sentinel::default)
716                            .as_ref(),
717                        &config.meta.project_root,
718                    )?;
719                } else {
720                    // do nothing
721                }
722            }
723
724            benchmark_summary.profiles.push(profile);
725
726            output.dump_log(log::Level::Info);
727            log_path.dump_log(log::Level::Info, &mut stderr())?;
728        }
729
730        Ok(benchmark_summary)
731    }
732}
733
734impl From<Option<FlamegraphConfig>> for ToolFlamegraphConfig {
735    fn from(value: Option<FlamegraphConfig>) -> Self {
736        match value {
737            Some(config) => Self::Callgrind(config),
738            None => Self::None,
739        }
740    }
741}
742
743impl From<api::ToolFlamegraphConfig> for ToolFlamegraphConfig {
744    fn from(value: api::ToolFlamegraphConfig) -> Self {
745        match value {
746            api::ToolFlamegraphConfig::Callgrind(flamegraph_config) => {
747                Self::Callgrind(flamegraph_config.into())
748            }
749            api::ToolFlamegraphConfig::None => Self::None,
750        }
751    }
752}