1use std::collections::HashMap;
4use std::fmt::{Display, Write as FmtWrite};
5use std::fs::{DirEntry, File};
6use std::io::{BufRead, BufReader, Write};
7use std::os::unix::fs::MetadataExt;
8use std::path::{Path, PathBuf};
9
10use anyhow::{Context, Result};
11use lazy_static::lazy_static;
12use log::log_enabled;
13use regex::Regex;
14
15use crate::api::ValgrindTool;
16use crate::runner::callgrind::parser::parse_header;
17use crate::runner::common::ModulePath;
18use crate::runner::summary::BaselineKind;
19use crate::util::truncate_str_utf8;
20
21lazy_static! {
22 static ref CALLGRIND_ORIG_FILENAME_RE: Regex = Regex::new(
28 "^(?<type>[.](out|log))(?<base>[.](old|base@[^.-]+))?(?<pid>[.][#][0-9]+)?(?<part>[.][0-9]+)?(?<thread>-[0-9]+)?$"
29 )
30 .expect("Regex should compile");
31
32 static ref BBV_ORIG_FILENAME_RE: Regex = Regex::new(
36 "^(?<type>[.](?:out|log))(?<base>[.](old|base@[^.]+))?(?<bbv_type>[.](?:bb|pc))?(?<pid>[.][#][0-9]+)?(?<thread>[.][0-9]+)?$"
37 )
38 .expect("Regex should compile");
39
40 static ref GENERIC_ORIG_FILENAME_RE: Regex = Regex::new(
43 "^(?<type>[.](?:out|log|xtree|xleak))(?<base>[.](old|base@[^.]+))?(?<pid>[.][#][0-9]+)?$"
44 )
45 .expect("Regex should compile");
46
47 static ref REAL_FILENAME_RE: Regex = Regex::new(
48 "^(?:[.](?<pid>[0-9]+))?(?:[.]t(?<tid>[0-9]+))?(?:[.]p(?<part>[0-9]+))?(?:[.](?<bbv>bb|pc))?(?:[.](?<type>out|log|xtree|xleak))(?:[.](?<base>old|base@[^.]+))?$"
49 )
50 .expect("Regex should compile");
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum ToolOutputPathKind {
56 Out,
58 OldOut,
60 BaseOut(String),
62 Log,
64 OldLog,
66 BaseLog(String),
68 Xtree,
70 OldXtree,
72 BaseXtree(String),
74 Xleak,
76 OldXleak,
78 BaseXleak(String),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ToolOutputPath {
85 pub baseline_kind: BaselineKind,
87 pub dir: PathBuf,
89 pub kind: ToolOutputPathKind,
91 pub modifiers: Vec<String>,
93 pub name: String,
95 pub tool: ValgrindTool,
97}
98
99impl ToolOutputPath {
100 pub fn new(
106 kind: ToolOutputPathKind,
107 tool: ValgrindTool,
108 baseline_kind: &BaselineKind,
109 base_dir: &Path,
110 module: &ModulePath,
111 name: &str,
112 ) -> Self {
113 let current = base_dir;
114 let module_path: PathBuf = module.to_string().split("::").collect();
115 let sanitized_name = sanitize_filename::sanitize_with_options(
116 name,
117 sanitize_filename::Options {
118 windows: false,
119 truncate: false,
120 replacement: "_",
121 },
122 );
123 let sanitized_name = truncate_str_utf8(&sanitized_name, 200);
124 Self {
125 kind,
126 tool,
127 baseline_kind: baseline_kind.clone(),
128 dir: current
129 .join(base_dir)
130 .join(module_path)
131 .join(sanitized_name),
132 name: sanitized_name.to_owned(),
133 modifiers: vec![],
134 }
135 }
136
137 pub fn with_init(
141 kind: ToolOutputPathKind,
142 tool: ValgrindTool,
143 baseline_kind: &BaselineKind,
144 base_dir: &Path,
145 module: &str,
146 name: &str,
147 ) -> Result<Self> {
148 let output = Self::new(
149 kind,
150 tool,
151 baseline_kind,
152 base_dir,
153 &ModulePath::new(module),
154 name,
155 );
156 output.init()?;
157 Ok(output)
158 }
159
160 pub fn init(&self) -> Result<()> {
162 std::fs::create_dir_all(&self.dir).with_context(|| {
163 format!(
164 "Failed to create benchmark directory: '{}'",
165 self.dir.display()
166 )
167 })
168 }
169
170 pub fn clear(&self) -> Result<()> {
172 for entry in self.real_paths()? {
173 std::fs::remove_file(&entry).with_context(|| {
174 format!("Failed to remove benchmark file: '{}'", entry.display())
175 })?;
176 }
177 Ok(())
178 }
179
180 pub fn shift(&self) -> Result<()> {
182 match self.baseline_kind {
183 BaselineKind::Old => {
184 self.to_base_path().clear()?;
185 for entry in self.real_paths()? {
186 let extension = entry.extension().expect("An extension should be present");
187 let mut extension = extension.to_owned();
188 extension.push(".old");
189 let new_path = entry.with_extension(extension);
190 std::fs::rename(&entry, &new_path).with_context(|| {
191 format!(
192 "Failed to move benchmark file from '{}' to '{}'",
193 entry.display(),
194 new_path.display()
195 )
196 })?;
197 }
198 Ok(())
199 }
200 BaselineKind::Name(_) => self.clear(),
201 }
202 }
203
204 pub fn exists(&self) -> bool {
206 self.real_paths().is_ok_and(|p| !p.is_empty())
207 }
208
209 pub fn is_multiple(&self) -> bool {
211 self.real_paths().is_ok_and(|p| p.len() > 1)
212 }
213
214 #[must_use]
216 pub fn to_base_path(&self) -> Self {
217 Self {
218 kind: match (&self.kind, &self.baseline_kind) {
219 (ToolOutputPathKind::Out, BaselineKind::Old) => ToolOutputPathKind::OldOut,
220 (
221 ToolOutputPathKind::Out | ToolOutputPathKind::BaseOut(_),
222 BaselineKind::Name(name),
223 ) => ToolOutputPathKind::BaseOut(name.to_string()),
224 (ToolOutputPathKind::Log, BaselineKind::Old) => ToolOutputPathKind::OldLog,
225 (
226 ToolOutputPathKind::Log | ToolOutputPathKind::BaseLog(_),
227 BaselineKind::Name(name),
228 ) => ToolOutputPathKind::BaseLog(name.to_string()),
229 (ToolOutputPathKind::Xtree, BaselineKind::Old) => ToolOutputPathKind::OldXtree,
230 (
231 ToolOutputPathKind::Xtree | ToolOutputPathKind::BaseXtree(_),
232 BaselineKind::Name(name),
233 ) => ToolOutputPathKind::BaseXtree(name.to_string()),
234 (ToolOutputPathKind::Xleak, BaselineKind::Old) => ToolOutputPathKind::OldXleak,
235 (
236 ToolOutputPathKind::Xleak | ToolOutputPathKind::BaseXleak(_),
237 BaselineKind::Name(name),
238 ) => ToolOutputPathKind::BaseXleak(name.to_string()),
239 (kind, _) => kind.clone(),
240 },
241 tool: self.tool,
242 baseline_kind: self.baseline_kind.clone(),
243 name: self.name.clone(),
244 dir: self.dir.clone(),
245 modifiers: self.modifiers.clone(),
246 }
247 }
248
249 #[must_use]
256 pub fn to_tool_output(&self, tool: ValgrindTool) -> Self {
257 let kind = if tool.has_output_file() {
258 match &self.kind {
259 ToolOutputPathKind::Log
260 | ToolOutputPathKind::OldLog
261 | ToolOutputPathKind::Xtree
262 | ToolOutputPathKind::OldXtree
263 | ToolOutputPathKind::Xleak
264 | ToolOutputPathKind::OldXleak => ToolOutputPathKind::Out,
265 ToolOutputPathKind::BaseLog(name)
266 | ToolOutputPathKind::BaseXtree(name)
267 | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseOut(name.clone()),
268 kind => kind.clone(),
269 }
270 } else {
271 match &self.kind {
272 ToolOutputPathKind::Out
273 | ToolOutputPathKind::OldOut
274 | ToolOutputPathKind::Xtree
275 | ToolOutputPathKind::OldXtree
276 | ToolOutputPathKind::Xleak
277 | ToolOutputPathKind::OldXleak => ToolOutputPathKind::Log,
278 ToolOutputPathKind::BaseOut(name)
279 | ToolOutputPathKind::BaseXtree(name)
280 | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseLog(name.clone()),
281 kind => kind.clone(),
282 }
283 };
284 Self {
285 tool,
286 kind,
287 baseline_kind: self.baseline_kind.clone(),
288 name: self.name.clone(),
289 dir: self.dir.clone(),
290 modifiers: self.modifiers.clone(),
291 }
292 }
293
294 #[must_use]
298 pub fn to_log_output(&self) -> Self {
299 Self {
300 kind: match &self.kind {
301 ToolOutputPathKind::Out
302 | ToolOutputPathKind::OldOut
303 | ToolOutputPathKind::Xleak
304 | ToolOutputPathKind::OldXleak
305 | ToolOutputPathKind::Xtree
306 | ToolOutputPathKind::OldXtree => ToolOutputPathKind::Log,
307 ToolOutputPathKind::BaseOut(name)
308 | ToolOutputPathKind::BaseXtree(name)
309 | ToolOutputPathKind::BaseXleak(name) => ToolOutputPathKind::BaseLog(name.clone()),
310 kind => kind.clone(),
311 },
312 tool: self.tool,
313 baseline_kind: self.baseline_kind.clone(),
314 name: self.name.clone(),
315 dir: self.dir.clone(),
316 modifiers: self.modifiers.clone(),
317 }
318 }
319
320 #[must_use]
324 pub fn to_xtree_output(&self) -> Option<Self> {
325 self.tool.has_xtree_file().then(|| Self {
326 kind: match &self.kind {
327 ToolOutputPathKind::Out
328 | ToolOutputPathKind::OldOut
329 | ToolOutputPathKind::Xleak
330 | ToolOutputPathKind::OldXleak
331 | ToolOutputPathKind::Log
332 | ToolOutputPathKind::OldLog => ToolOutputPathKind::Xtree,
333 ToolOutputPathKind::BaseOut(name)
334 | ToolOutputPathKind::BaseLog(name)
335 | ToolOutputPathKind::BaseXleak(name) => {
336 ToolOutputPathKind::BaseXtree(name.clone())
337 }
338 kind => kind.clone(),
339 },
340 tool: self.tool,
341 baseline_kind: self.baseline_kind.clone(),
342 name: self.name.clone(),
343 dir: self.dir.clone(),
344 modifiers: self.modifiers.clone(),
345 })
346 }
347
348 #[must_use]
352 pub fn to_xleak_output(&self) -> Option<Self> {
353 self.tool.has_xleak_file().then(|| Self {
354 kind: match &self.kind {
355 ToolOutputPathKind::Out
356 | ToolOutputPathKind::OldOut
357 | ToolOutputPathKind::Xtree
358 | ToolOutputPathKind::OldXtree
359 | ToolOutputPathKind::Log
360 | ToolOutputPathKind::OldLog => ToolOutputPathKind::Xleak,
361 ToolOutputPathKind::BaseOut(name)
362 | ToolOutputPathKind::BaseLog(name)
363 | ToolOutputPathKind::BaseXtree(name) => {
364 ToolOutputPathKind::BaseXleak(name.clone())
365 }
366 kind => kind.clone(),
367 },
368 tool: self.tool,
369 baseline_kind: self.baseline_kind.clone(),
370 name: self.name.clone(),
371 dir: self.dir.clone(),
372 modifiers: self.modifiers.clone(),
373 })
374 }
375
376 pub fn log_path_of(&self, path: &Path) -> Option<PathBuf> {
380 let file_name = path.strip_prefix(&self.dir).ok()?;
381 if let Some(suffix) = self.strip_prefix(&file_name.to_string_lossy()) {
382 let caps = REAL_FILENAME_RE.captures(suffix)?;
383 if let Some(kind) = caps.name("type") {
384 match kind.as_str() {
385 "out" | "xtree" | "xleak" => {
386 let mut string = self.prefix();
387 for s in [
388 caps.name("pid").map(|c| format!(".{}", c.as_str())),
389 Some(".log".to_owned()),
390 caps.name("base").map(|c| format!(".{}", c.as_str())),
391 ]
392 .iter()
393 .filter_map(|s| s.as_ref())
394 {
395 string.push_str(s);
396 }
397
398 return Some(self.dir.join(string));
399 }
400 _ => return Some(path.to_owned()),
401 }
402 }
403 }
404
405 None
406 }
407
408 pub fn dump_log<W>(&self, log_level: log::Level, writer: &mut W) -> Result<()>
410 where
411 W: Write,
412 {
413 if log_enabled!(log_level) {
414 for path in self.real_paths()? {
415 log::log!(
416 log_level,
417 "{} log output '{}':",
418 self.tool.id(),
419 path.display()
420 );
421
422 let file = File::open(&path).with_context(|| {
423 format!(
424 "Error opening {} output file '{}'",
425 self.tool.id(),
426 path.display()
427 )
428 })?;
429
430 let mut reader = BufReader::new(file);
431 std::io::copy(&mut reader, writer)?;
432 }
433 }
434 Ok(())
435 }
436
437 pub fn extension(&self) -> String {
441 match (&self.kind, self.modifiers.is_empty()) {
442 (ToolOutputPathKind::Out, true) => "out".to_owned(),
443 (ToolOutputPathKind::Out, false) => format!("out.{}", self.modifiers.join(".")),
444 (ToolOutputPathKind::Log, true) => "log".to_owned(),
445 (ToolOutputPathKind::Log, false) => format!("log.{}", self.modifiers.join(".")),
446 (ToolOutputPathKind::OldOut, true) => "out.old".to_owned(),
447 (ToolOutputPathKind::OldOut, false) => format!("out.old.{}", self.modifiers.join(".")),
448 (ToolOutputPathKind::OldLog, true) => "log.old".to_owned(),
449 (ToolOutputPathKind::OldLog, false) => format!("log.old.{}", self.modifiers.join(".")),
450 (ToolOutputPathKind::BaseLog(name), true) => {
451 format!("log.base@{name}")
452 }
453 (ToolOutputPathKind::BaseLog(name), false) => {
454 format!("log.base@{name}.{}", self.modifiers.join("."))
455 }
456 (ToolOutputPathKind::BaseOut(name), true) => format!("out.base@{name}"),
457 (ToolOutputPathKind::BaseOut(name), false) => {
458 format!("out.base@{name}.{}", self.modifiers.join("."))
459 }
460 (ToolOutputPathKind::Xtree, true) => "xtree".to_owned(),
461 (ToolOutputPathKind::Xtree, false) => format!("xtree.{}", self.modifiers.join(".")),
462 (ToolOutputPathKind::OldXtree, true) => "xtree.old".to_owned(),
463 (ToolOutputPathKind::OldXtree, false) => {
464 format!("xtree.old.{}", self.modifiers.join("."))
465 }
466 (ToolOutputPathKind::BaseXtree(name), true) => format!("xtree.base@{name}"),
467 (ToolOutputPathKind::BaseXtree(name), false) => {
468 format!("xtree.base@{name}.{}", self.modifiers.join("."))
469 }
470 (ToolOutputPathKind::Xleak, true) => "xleak".to_owned(),
471 (ToolOutputPathKind::Xleak, false) => format!("xleak.{}", self.modifiers.join(".")),
472 (ToolOutputPathKind::OldXleak, true) => "xleak.old".to_owned(),
473 (ToolOutputPathKind::OldXleak, false) => {
474 format!("xleak.old.{}", self.modifiers.join("."))
475 }
476 (ToolOutputPathKind::BaseXleak(name), true) => format!("xleak.base@{name}"),
477 (ToolOutputPathKind::BaseXleak(name), false) => {
478 format!("xleak.base@{name}.{}", self.modifiers.join("."))
479 }
480 }
481 }
482
483 #[must_use]
485 pub fn with_modifiers<I, T>(&self, modifiers: T) -> Self
486 where
487 I: Into<String>,
488 T: IntoIterator<Item = I>,
489 {
490 Self {
491 kind: self.kind.clone(),
492 tool: self.tool,
493 baseline_kind: self.baseline_kind.clone(),
494 dir: self.dir.clone(),
495 name: self.name.clone(),
496 modifiers: modifiers.into_iter().map(Into::into).collect(),
497 }
498 }
499
500 pub fn to_path(&self) -> PathBuf {
506 self.dir.join(format!(
507 "{}.{}.{}",
508 self.tool.id(),
509 self.name,
510 self.extension()
511 ))
512 }
513
514 pub fn walk_dir(&self) -> Result<impl Iterator<Item = DirEntry>> {
516 std::fs::read_dir(&self.dir)
517 .with_context(|| {
518 format!(
519 "Failed opening benchmark directory: '{}'",
520 self.dir.display()
521 )
522 })
523 .map(|i| i.into_iter().filter_map(Result::ok))
524 }
525
526 pub fn strip_prefix<'a>(&self, file_name: &'a str) -> Option<&'a str> {
528 file_name.strip_prefix(format!("{}.{}", self.tool.id(), self.name).as_str())
529 }
530
531 pub fn prefix(&self) -> String {
533 format!("{}.{}", self.tool.id(), self.name)
534 }
535
536 #[allow(clippy::case_sensitive_file_extension_comparisons)]
540 pub fn real_paths(&self) -> Result<Vec<PathBuf>> {
541 let mut paths = vec![];
542 for entry in self.walk_dir()? {
543 let file_name = entry.file_name();
544 let file_name = file_name.to_string_lossy();
545
546 if let Some(suffix) = self.strip_prefix(&file_name) {
549 let is_match = match &self.kind {
550 ToolOutputPathKind::Out => suffix.ends_with(".out"),
551 ToolOutputPathKind::Log => suffix.ends_with(".log"),
552 ToolOutputPathKind::OldOut => suffix.ends_with(".out.old"),
553 ToolOutputPathKind::OldLog => suffix.ends_with(".log.old"),
554 ToolOutputPathKind::BaseLog(name) => {
555 suffix.ends_with(format!(".log.base@{name}").as_str())
556 }
557 ToolOutputPathKind::BaseOut(name) => {
558 suffix.ends_with(format!(".out.base@{name}").as_str())
559 }
560 ToolOutputPathKind::Xtree => suffix.ends_with(".xtree"),
561 ToolOutputPathKind::OldXtree => suffix.ends_with(".xtree.old"),
562 ToolOutputPathKind::BaseXtree(name) => {
563 suffix.ends_with(format!(".xtree.base@{name}").as_str())
564 }
565 ToolOutputPathKind::Xleak => suffix.ends_with(".xleak"),
566 ToolOutputPathKind::OldXleak => suffix.ends_with(".xleak.old"),
567 ToolOutputPathKind::BaseXleak(name) => {
568 suffix.ends_with(format!(".xleak.base@{name}").as_str())
569 }
570 };
571
572 if is_match {
573 paths.push(entry.path());
574 }
575 }
576 }
577 Ok(paths)
578 }
579
580 pub fn real_paths_with_modifier(&self) -> Result<Vec<(PathBuf, Option<String>)>> {
582 let mut paths = vec![];
583 for entry in self.walk_dir()? {
584 let file_name = entry.file_name().to_string_lossy().to_string();
585
586 if let Some(suffix) = self.strip_prefix(&file_name) {
589 let modifiers = match &self.kind {
590 ToolOutputPathKind::Out => suffix.strip_suffix(".out"),
591 ToolOutputPathKind::Log => suffix.strip_suffix(".log"),
592 ToolOutputPathKind::OldOut => suffix.strip_suffix(".out.old"),
593 ToolOutputPathKind::OldLog => suffix.strip_suffix(".log.old"),
594 ToolOutputPathKind::BaseLog(name) => {
595 suffix.strip_suffix(format!(".log.base@{name}").as_str())
596 }
597 ToolOutputPathKind::BaseOut(name) => {
598 suffix.strip_suffix(format!(".out.base@{name}").as_str())
599 }
600 ToolOutputPathKind::Xtree => suffix.strip_suffix(".xtree"),
601 ToolOutputPathKind::OldXtree => suffix.strip_suffix(".xtree.old"),
602 ToolOutputPathKind::BaseXtree(name) => {
603 suffix.strip_suffix(format!(".xtree.base@{name}").as_str())
604 }
605 ToolOutputPathKind::Xleak => suffix.strip_suffix(".xleak"),
606 ToolOutputPathKind::OldXleak => suffix.strip_suffix(".xleak.old"),
607 ToolOutputPathKind::BaseXleak(name) => {
608 suffix.strip_suffix(format!(".xleak.base@{name}").as_str())
609 }
610 };
611
612 paths.push((
613 entry.path(),
614 modifiers.and_then(|s| (!s.is_empty()).then(|| s.to_owned())),
615 ));
616 }
617 }
618 Ok(paths)
619 }
620
621 #[allow(clippy::too_many_lines)]
633 pub fn sanitize_callgrind(&self) -> Result<()> {
634 type Grouped = (PathBuf, Option<u64>);
636 type Group =
638 HashMap<Option<String>, HashMap<Option<i32>, HashMap<Option<usize>, Vec<Grouped>>>>;
639
640 let mut groups: HashMap<String, Group> = HashMap::new();
647
648 for entry in self.walk_dir()? {
649 let file_name = entry.file_name();
650 let file_name = file_name.to_string_lossy();
651
652 let Some(haystack) = self.strip_prefix(&file_name) else {
653 continue;
654 };
655
656 if let Some(caps) = CALLGRIND_ORIG_FILENAME_RE.captures(haystack) {
657 if entry.metadata()?.size() == 0 {
660 std::fs::remove_file(entry.path())?;
661 continue;
662 }
663
664 let base = if let Some(base) = caps.name("base") {
667 if base.as_str() == ".old" {
668 continue;
669 }
670
671 Some(base.as_str().to_owned())
672 } else {
673 None
674 };
675
676 let out_type = caps
677 .name("type")
678 .expect("A out|log type should be present")
679 .as_str();
680
681 if out_type == ".out" {
682 let properties = parse_header(
683 &mut BufReader::new(File::open(entry.path())?)
684 .lines()
685 .map(Result::unwrap),
686 )?;
687 if let Some(bases) = groups.get_mut(out_type) {
688 if let Some(pids) = bases.get_mut(&base) {
689 if let Some(threads) = pids.get_mut(&properties.pid) {
690 if let Some(parts) = threads.get_mut(&properties.thread) {
691 parts.push((entry.path(), properties.part));
692 } else {
693 threads.insert(
694 properties.thread,
695 vec![(entry.path(), properties.part)],
696 );
697 }
698 } else {
699 pids.insert(
700 properties.pid,
701 HashMap::from([(
702 properties.thread,
703 vec![(entry.path(), properties.part)],
704 )]),
705 );
706 }
707 } else {
708 bases.insert(
709 base.clone(),
710 HashMap::from([(
711 properties.pid,
712 HashMap::from([(
713 properties.thread,
714 vec![(entry.path(), properties.part)],
715 )]),
716 )]),
717 );
718 }
719 } else {
720 groups.insert(
721 out_type.to_owned(),
722 HashMap::from([(
723 base.clone(),
724 HashMap::from([(
725 properties.pid,
726 HashMap::from([(
727 properties.thread,
728 vec![(entry.path(), properties.part)],
729 )]),
730 )]),
731 )]),
732 );
733 }
734 } else {
735 let pid = caps.name("pid").map(|m| {
736 m.as_str()[2..]
737 .parse::<i32>()
738 .expect("The pid from the match should be number")
739 });
740
741 if let Some(bases) = groups.get_mut(out_type) {
744 if let Some(pids) = bases.get_mut(&base) {
745 if let Some(threads) = pids.get_mut(&pid) {
746 if let Some(parts) = threads.get_mut(&None) {
747 parts.push((entry.path(), None));
748 } else {
749 threads.insert(None, vec![(entry.path(), None)]);
750 }
751 } else {
752 pids.insert(
753 pid,
754 HashMap::from([(None, vec![(entry.path(), None)])]),
755 );
756 }
757 } else {
758 bases.insert(
759 base.clone(),
760 HashMap::from([(
761 pid,
762 HashMap::from([(None, vec![(entry.path(), None)])]),
763 )]),
764 );
765 }
766 } else {
767 groups.insert(
768 out_type.to_owned(),
769 HashMap::from([(
770 base.clone(),
771 HashMap::from([(
772 pid,
773 HashMap::from([(None, vec![(entry.path(), None)])]),
774 )]),
775 )]),
776 );
777 }
778 }
779 }
780 }
781
782 for (out_type, types) in groups {
783 for (base, bases) in types {
784 let multiple_pids = bases.len() > 1;
785
786 for (pid, threads) in bases {
787 let multiple_threads = threads.len() > 1;
788
789 for (thread, parts) in &threads {
790 let multiple_parts = parts.len() > 1;
791
792 for (orig_path, part) in parts {
793 let mut new_file_name = self.prefix();
794
795 if multiple_pids {
796 if let Some(pid) = pid {
797 write!(new_file_name, ".{pid}").unwrap();
798 }
799 }
800
801 if multiple_threads {
802 if let Some(thread) = thread {
803 let width = threads.len().ilog10() as usize + 1;
804 write!(new_file_name, ".t{thread:0width$}").unwrap();
805 }
806
807 if !multiple_parts {
808 if let Some(part) = part {
809 let width = parts.len().ilog10() as usize + 1;
810 write!(new_file_name, ".p{part:0width$}").unwrap();
811 }
812 }
813 }
814
815 if multiple_parts {
816 if !multiple_threads {
817 if let Some(thread) = thread {
818 let width = threads.len().ilog10() as usize + 1;
819 write!(new_file_name, ".t{thread:0width$}").unwrap();
820 }
821 }
822
823 if let Some(part) = part {
824 let width = parts.len().ilog10() as usize + 1;
825 write!(new_file_name, ".p{part:0width$}").unwrap();
826 }
827 }
828
829 new_file_name.push_str(&out_type);
830 if let Some(base) = &base {
831 new_file_name.push_str(base);
832 }
833
834 let from = orig_path;
835 let to = from.with_file_name(new_file_name);
836
837 std::fs::rename(from, to)?;
838 }
839 }
840 }
841 }
842 }
843
844 Ok(())
845 }
846
847 #[allow(clippy::case_sensitive_file_extension_comparisons)]
862 #[allow(clippy::too_many_lines)]
863 pub fn sanitize_bbv(&self) -> Result<()> {
864 type Grouped = (PathBuf, String);
866 type Group =
868 HashMap<Option<String>, HashMap<Option<String>, HashMap<Option<String>, Vec<Grouped>>>>;
869
870 let mut groups: HashMap<String, Group> = HashMap::new();
872 for entry in self.walk_dir()? {
873 let file_name = entry.file_name();
874 let file_name = file_name.to_string_lossy();
875
876 let Some(haystack) = self.strip_prefix(&file_name) else {
877 continue;
878 };
879
880 if let Some(caps) = BBV_ORIG_FILENAME_RE.captures(haystack) {
881 if entry.metadata()?.size() == 0 {
882 std::fs::remove_file(entry.path())?;
883 continue;
884 }
885
886 let base = if let Some(base) = caps.name("base") {
888 if base.as_str() == ".old" {
889 continue;
890 }
891
892 Some(base.as_str().to_owned())
893 } else {
894 None
895 };
896
897 let out_type = caps.name("type").unwrap().as_str();
898 let bbv_type = caps.name("bbv_type").map(|m| m.as_str().to_owned());
899 let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
900
901 let thread = caps
902 .name("thread")
903 .map_or_else(|| ".1".to_owned(), |t| t.as_str().to_owned());
904
905 if let Some(bases) = groups.get_mut(out_type) {
906 if let Some(bbv_types) = bases.get_mut(&base) {
907 if let Some(pids) = bbv_types.get_mut(&bbv_type) {
908 if let Some(threads) = pids.get_mut(&pid) {
909 threads.push((entry.path(), thread));
910 } else {
911 pids.insert(pid, vec![(entry.path(), thread)]);
912 }
913 } else {
914 bbv_types.insert(
915 bbv_type.clone(),
916 HashMap::from([(pid, vec![(entry.path(), thread)])]),
917 );
918 }
919 } else {
920 bases.insert(
921 base.clone(),
922 HashMap::from([(
923 bbv_type.clone(),
924 HashMap::from([(pid, vec![(entry.path(), thread)])]),
925 )]),
926 );
927 }
928 } else {
929 groups.insert(
930 out_type.to_owned(),
931 HashMap::from([(
932 base.clone(),
933 HashMap::from([(
934 bbv_type.clone(),
935 HashMap::from([(pid, vec![(entry.path(), thread)])]),
936 )]),
937 )]),
938 );
939 }
940 }
941 }
942
943 for (out_type, bases) in groups {
944 for (base, bbv_types) in bases {
945 for (bbv_type, pids) in &bbv_types {
946 let multiple_pids = pids.len() > 1;
947
948 for (pid, threads) in pids {
949 let multiple_threads = threads.len() > 1;
950
951 for (orig_path, thread) in threads {
952 let mut new_file_name = self.prefix();
953
954 if multiple_pids {
955 if let Some(pid) = pid.as_ref() {
956 write!(new_file_name, "{pid}").unwrap();
957 }
958 }
959
960 if multiple_threads
961 && bbv_type.as_ref().is_some_and(|b| b.starts_with(".bb"))
962 {
963 let width = threads.len().ilog10() as usize + 1;
964
965 let thread = thread[1..]
966 .parse::<usize>()
967 .expect("The thread from the regex should be a number");
968
969 write!(new_file_name, ".t{thread:0width$}").unwrap();
970 }
971
972 if let Some(bbv_type) = &bbv_type {
973 new_file_name.push_str(bbv_type);
974 }
975
976 new_file_name.push_str(&out_type);
977
978 if let Some(base) = &base {
979 new_file_name.push_str(base);
980 }
981
982 let from = orig_path;
983 let to = from.with_file_name(new_file_name);
984
985 std::fs::rename(from, to)?;
986 }
987 }
988 }
989 }
990 }
991
992 Ok(())
993 }
994
995 pub fn sanitize_generic(&self) -> Result<()> {
1000 type Group = HashMap<Option<String>, Vec<(PathBuf, Option<String>)>>;
1002
1003 let mut groups: HashMap<String, Group> = HashMap::new();
1005 for entry in self.walk_dir()? {
1006 let file_name = entry.file_name();
1007 let file_name = file_name.to_string_lossy();
1008
1009 let Some(haystack) = self.strip_prefix(&file_name) else {
1010 continue;
1011 };
1012
1013 if let Some(caps) = GENERIC_ORIG_FILENAME_RE.captures(haystack) {
1014 if entry.metadata()?.size() == 0 {
1015 std::fs::remove_file(entry.path())?;
1016 continue;
1017 }
1018
1019 let base = if let Some(base) = caps.name("base") {
1021 if base.as_str() == ".old" {
1022 continue;
1023 }
1024
1025 Some(base.as_str().to_owned())
1026 } else {
1027 None
1028 };
1029
1030 let out_type = caps.name("type").unwrap().as_str();
1031 let pid = caps.name("pid").map(|p| format!(".{}", &p.as_str()[2..]));
1032
1033 if let Some(bases) = groups.get_mut(out_type) {
1034 if let Some(pids) = bases.get_mut(&base) {
1035 pids.push((entry.path(), pid));
1036 } else {
1037 bases.insert(base, vec![(entry.path(), pid)]);
1038 }
1039 } else {
1040 groups.insert(
1041 out_type.to_owned(),
1042 HashMap::from([(base, vec![(entry.path(), pid)])]),
1043 );
1044 }
1045 }
1046 }
1047
1048 for (out_type, bases) in groups {
1049 for (base, pids) in bases {
1050 let multiple_pids = pids.len() > 1;
1051 for (orig_path, pid) in pids {
1052 let mut new_file_name = self.prefix();
1053
1054 if multiple_pids {
1055 if let Some(pid) = pid.as_ref() {
1056 write!(new_file_name, "{pid}").unwrap();
1057 }
1058 }
1059
1060 new_file_name.push_str(&out_type);
1061
1062 if let Some(base) = &base {
1063 new_file_name.push_str(base);
1064 }
1065
1066 let from = orig_path;
1067 let to = from.with_file_name(new_file_name);
1068
1069 std::fs::rename(from, to)?;
1070 }
1071 }
1072 }
1073
1074 Ok(())
1075 }
1076
1077 pub fn sanitize(&self) -> Result<()> {
1082 match self.tool {
1083 ValgrindTool::Callgrind => self.sanitize_callgrind()?,
1084 ValgrindTool::BBV => self.sanitize_bbv()?,
1085 _ => self.sanitize_generic()?,
1086 }
1087
1088 Ok(())
1089 }
1090}
1091
1092impl Display for ToolOutputPath {
1093 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1094 f.write_fmt(format_args!("{}", self.to_path().display()))
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100
1101 use rstest::rstest;
1102
1103 use super::*;
1104
1105 #[rstest]
1106 #[case::out(".out")]
1107 #[case::out_with_pid(".out.#1234")]
1108 #[case::out_with_part(".out.1")]
1109 #[case::out_with_thread(".out-01")]
1110 #[case::out_with_part_and_thread(".out.1-01")]
1111 #[case::out_base(".out.base@default")]
1112 #[case::out_base_with_pid(".out.base@default.#1234")]
1113 #[case::out_base_with_part(".out.base@default.1")]
1114 #[case::out_base_with_thread(".out.base@default-01")]
1115 #[case::out_base_with_part_and_thread(".out.base@default.1-01")]
1116 #[case::log(".log")]
1117 #[case::log_with_pid(".log.#1234")]
1118 #[case::log_base(".log.base@default")]
1119 #[case::log_base_with_pid(".log.base@default.#1234")]
1120 fn test_callgrind_filename_regex(#[case] haystack: &str) {
1121 assert!(CALLGRIND_ORIG_FILENAME_RE.is_match(haystack));
1122 }
1123
1124 #[rstest]
1125 #[case::bb_out(".out.bb")]
1126 #[case::bb_out_with_pid(".out.bb.#1234")]
1127 #[case::bb_out_with_pid_and_thread(".out.bb.#1234.1")]
1128 #[case::bb_out_with_thread(".out.bb.1")]
1129 #[case::pc_out(".out.pc")]
1130 #[case::log(".log")]
1131 #[case::log_with_pid(".log.#1234")]
1132 fn test_bbv_filename_regex(#[case] haystack: &str) {
1133 assert!(BBV_ORIG_FILENAME_RE.is_match(haystack));
1134 }
1135
1136 #[rstest]
1137 #[case::out(".out", vec![("type", "out")])]
1138 #[case::pid_out(".2049595.out", vec![("pid", "2049595"), ("type", "out")])]
1139 #[case::pid_thread_out(".2049595.t1.out", vec![("pid", "2049595"), ("tid", "1"), ("type", "out")])]
1140 #[case::pid_thread_part_out(".2049595.t1.p1.out", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out")])]
1141 #[case::out_old(".out.old", vec![("type", "out"), ("base", "old")])]
1142 #[case::pid_out_old(".2049595.out.old", vec![("pid", "2049595"), ("type", "out"), ("base", "old")])]
1143 #[case::pid_thread_out_old(".2049595.t1.out.old", vec![("pid", "2049595"), ("tid", "1"), ("type", "out"), ("base", "old")])]
1144 #[case::pid_thread_part_out_old(".2049595.t1.p1.out.old", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out"), ("base", "old")])]
1145 #[case::out_base(".out.base@name", vec![("type", "out"), ("base", "base@name")])]
1146 #[case::pid_out_base(".2049595.out.base@name", vec![("pid", "2049595"), ("type", "out"), ("base", "base@name")])]
1147 #[case::pid_thread_out_base(".2049595.t1.out.base@name", vec![("pid", "2049595"), ("tid", "1"), ("type", "out"), ("base", "base@name")])]
1148 #[case::pid_thread_part_out_base(".2049595.t1.p1.out.base@name", vec![("pid", "2049595"), ("tid", "1"), ("part", "1"), ("type", "out"), ("base", "base@name")])]
1149 #[case::bb_out(".bb.out", vec![("bbv", "bb"), ("type", "out")])]
1150 #[case::pc_out(".pc.out", vec![("bbv", "pc"), ("type", "out")])]
1151 #[case::pid_bb_out(".123.bb.out", vec![("pid", "123"), ("bbv", "bb"), ("type", "out")])]
1152 #[case::pid_thread_bb_out(".123.t1.bb.out", vec![("pid", "123"), ("tid", "1"), ("bbv", "bb"), ("type", "out")])]
1153 #[case::log(".log", vec![("type", "log")])]
1154 #[case::xtree(".xtree", vec![("type", "xtree")])]
1155 #[case::xtree_old(".xtree.old", vec![("type", "xtree"), ("base", "old")])]
1156 #[case::xleak(".xleak", vec![("type", "xleak")])]
1157 #[case::xleak_old(".xleak.old", vec![("type", "xleak"), ("base", "old")])]
1158 fn test_real_file_name_regex(#[case] haystack: &str, #[case] expected: Vec<(&str, &str)>) {
1159 assert!(REAL_FILENAME_RE.is_match(haystack));
1160
1161 let caps = REAL_FILENAME_RE.captures(haystack).unwrap();
1162 for (name, value) in expected {
1163 assert_eq!(caps.name(name).unwrap().as_str(), value);
1164 }
1165 }
1166
1167 #[rstest]
1168 #[case::out(
1169 ValgrindTool::Callgrind,
1170 "callgrind.bench_thread_in_subprocess.two.out",
1171 "callgrind.bench_thread_in_subprocess.two.log"
1172 )]
1173 #[case::out_old(
1174 ValgrindTool::Callgrind,
1175 "callgrind.bench_thread_in_subprocess.two.out.old",
1176 "callgrind.bench_thread_in_subprocess.two.log.old"
1177 )]
1178 #[case::pid_out(
1179 ValgrindTool::Callgrind,
1180 "callgrind.bench_thread_in_subprocess.two.123.out",
1181 "callgrind.bench_thread_in_subprocess.two.123.log"
1182 )]
1183 #[case::pid_tid_out(
1184 ValgrindTool::Callgrind,
1185 "callgrind.bench_thread_in_subprocess.two.123.t1.out",
1186 "callgrind.bench_thread_in_subprocess.two.123.log"
1187 )]
1188 #[case::pid_tid_part_out(
1189 ValgrindTool::Callgrind,
1190 "callgrind.bench_thread_in_subprocess.two.123.t1.p2.out",
1191 "callgrind.bench_thread_in_subprocess.two.123.log"
1192 )]
1193 #[case::pid_out_old(
1194 ValgrindTool::Callgrind,
1195 "callgrind.bench_thread_in_subprocess.two.123.out.old",
1196 "callgrind.bench_thread_in_subprocess.two.123.log.old"
1197 )]
1198 #[case::pid_tid_part_out_old(
1199 ValgrindTool::Callgrind,
1200 "callgrind.bench_thread_in_subprocess.two.123.t1.p2.out.old",
1201 "callgrind.bench_thread_in_subprocess.two.123.log.old"
1202 )]
1203 #[case::bb_out(
1204 ValgrindTool::BBV,
1205 "exp-bbv.bench_thread_in_subprocess.two.bb.out",
1206 "exp-bbv.bench_thread_in_subprocess.two.log"
1207 )]
1208 #[case::bb_pid_out(
1209 ValgrindTool::BBV,
1210 "exp-bbv.bench_thread_in_subprocess.two.123.bb.out",
1211 "exp-bbv.bench_thread_in_subprocess.two.123.log"
1212 )]
1213 #[case::bb_pid_tid_out(
1214 ValgrindTool::BBV,
1215 "exp-bbv.bench_thread_in_subprocess.two.123.t1.bb.out",
1216 "exp-bbv.bench_thread_in_subprocess.two.123.log"
1217 )]
1218 #[case::xtree(
1219 ValgrindTool::Memcheck,
1220 "memcheck.bench_thread_in_subprocess.two.xtree",
1221 "memcheck.bench_thread_in_subprocess.two.log"
1222 )]
1223 #[case::xtree_old(
1224 ValgrindTool::Memcheck,
1225 "memcheck.bench_thread_in_subprocess.two.xtree.old",
1226 "memcheck.bench_thread_in_subprocess.two.log.old"
1227 )]
1228 #[case::xtree_pid(
1229 ValgrindTool::Memcheck,
1230 "memcheck.bench_thread_in_subprocess.two.123.xtree",
1231 "memcheck.bench_thread_in_subprocess.two.123.log"
1232 )]
1233 #[case::xleak(
1234 ValgrindTool::Memcheck,
1235 "memcheck.bench_thread_in_subprocess.two.xleak",
1236 "memcheck.bench_thread_in_subprocess.two.log"
1237 )]
1238 #[case::xleak_old(
1239 ValgrindTool::Memcheck,
1240 "memcheck.bench_thread_in_subprocess.two.xleak.old",
1241 "memcheck.bench_thread_in_subprocess.two.log.old"
1242 )]
1243 #[case::xleak_pid(
1244 ValgrindTool::Memcheck,
1245 "memcheck.bench_thread_in_subprocess.two.123.xleak",
1246 "memcheck.bench_thread_in_subprocess.two.123.log"
1247 )]
1248 fn test_tool_output_path_log_path_of(
1249 #[case] tool: ValgrindTool,
1250 #[case] input: PathBuf,
1251 #[case] expected: PathBuf,
1252 ) {
1253 let output_path = ToolOutputPath::new(
1254 ToolOutputPathKind::Out,
1255 tool,
1256 &BaselineKind::Old,
1257 &PathBuf::from("/root"),
1258 &ModulePath::new("hello::world"),
1259 "bench_thread_in_subprocess.two",
1260 );
1261 let expected = output_path.dir.join(expected);
1262 let actual = output_path
1263 .log_path_of(&output_path.dir.join(input))
1264 .unwrap();
1265
1266 assert_eq!(actual, expected);
1267 }
1268
1269 #[test]
1270 fn test_tool_output_path_log_path_of_when_log_then_same() {
1271 let output_path = ToolOutputPath::new(
1272 ToolOutputPathKind::Log,
1273 ValgrindTool::Callgrind,
1274 &BaselineKind::Old,
1275 &PathBuf::from("/root"),
1276 &ModulePath::new("hello::world"),
1277 "bench_thread_in_subprocess.two",
1278 );
1279 let path = PathBuf::from(
1280 "/root/hello/world/bench_thread_in_subprocess.two/callgrind.\
1281 bench_thread_in_subprocess.two.log",
1282 );
1283
1284 assert_eq!(output_path.log_path_of(&path), Some(path));
1285 }
1286
1287 #[test]
1288 fn test_tool_output_path_log_path_of_when_not_in_dir_then_none() {
1289 let output_path = ToolOutputPath::new(
1290 ToolOutputPathKind::Out,
1291 ValgrindTool::Callgrind,
1292 &BaselineKind::Old,
1293 &PathBuf::from("/root"),
1294 &ModulePath::new("hello::world"),
1295 "bench_thread_in_subprocess.two",
1296 );
1297
1298 assert!(output_path
1299 .log_path_of(&PathBuf::from(
1300 "/root/not/here/bench_thread_in_subprocess.two/callgrind.\
1301 bench_thread_in_subprocess.two.out"
1302 ))
1303 .is_none());
1304 }
1305}