1use 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#[derive(Debug, Clone, PartialEq)]
34pub enum ToolFlamegraphConfig {
35 Callgrind(FlamegraphConfig),
37 None,
39}
40
41#[derive(Debug, Clone)]
43pub struct ToolConfig {
44 pub args: ToolArgs,
46 pub entry_point: EntryPoint,
48 pub flamegraph_config: ToolFlamegraphConfig,
50 pub frames: Vec<Glob>,
52 pub is_default: bool,
54 pub is_enabled: bool,
56 pub regression_config: ToolRegressionConfig,
58 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#[derive(Debug, Clone)]
77pub struct ToolConfigs(pub Vec<ToolConfig>);
78
79impl ToolConfig {
80 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 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 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 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 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 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 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 pub fn has_tools_enabled(&self) -> bool {
455 self.0.iter().any(|t| t.is_enabled)
456 }
457
458 pub fn has_multiple(&self) -> bool {
460 self.0.len() > 1 && self.0.iter().filter(|f| f.is_enabled).count() > 1
461 }
462
463 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 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 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 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 #[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 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 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 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 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 }
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}