1use crate::{
4 CcActionContext, CcActionInput, CcBypassReason, CcCompilerFamily, MAX_INPUT_BYTES,
5 MAX_MANIFEST_ENTRIES, MAX_PREDICTED_INPUTS, normalize_components,
6};
7use mbx_cache_core::{
8 CacheDigest, FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity,
9 FileSnapshot, RecordedFileDigest, digest_file, digest_file_validated,
10};
11use std::collections::{BTreeMap, BTreeSet};
12use std::io::Read;
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16pub const INCLUDE_MANIFEST_PREFIX: &str = "@include-manifest:";
18
19const ASSEMBLER_INPUT_DIRECTIVES: &[&[u8]] = &[b".include", b".incbin", b".sinclude"];
22
23const SCAN_CHUNK_BYTES: usize = 64 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
27pub struct CcDepfile {
28 pub files: Vec<PathBuf>,
30}
31
32impl CcDepfile {
33 pub fn read(path: &Path) -> Result<Self, CcBypassReason> {
35 let contents =
36 std::fs::read_to_string(path).map_err(|error| CcBypassReason::DepfileRead {
37 path: path.to_path_buf(),
38 message: error.to_string(),
39 })?;
40 Self::parse(&contents)
41 }
42
43 pub fn read_for(path: &Path, family: CcCompilerFamily) -> Result<Self, CcBypassReason> {
45 if family.is_msvc() {
46 Self::read_msvc(path)
47 } else {
48 Self::read(path)
49 }
50 }
51
52 pub fn read_msvc(path: &Path) -> Result<Self, CcBypassReason> {
54 let contents = std::fs::read(path).map_err(|error| CcBypassReason::DepfileRead {
55 path: path.to_path_buf(),
56 message: error.to_string(),
57 })?;
58 let value: serde_json::Value = serde_json::from_slice(&contents)
59 .map_err(|error| CcBypassReason::MalformedDepfile(error.to_string()))?;
60 let data = value
61 .get("Data")
62 .and_then(serde_json::Value::as_object)
63 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Data object".into()))?;
64 if data
65 .get("ImportedModules")
66 .and_then(serde_json::Value::as_array)
67 .is_some_and(|modules| !modules.is_empty())
68 || data.get("ProvidedModule").is_some_and(|module| {
69 !module.is_null() && module.as_str().is_none_or(|s| !s.is_empty())
70 })
71 {
72 return Err(CcBypassReason::MalformedDepfile(
73 "C++ module dependencies are not modeled".into(),
74 ));
75 }
76 let includes = data
77 .get("Includes")
78 .and_then(serde_json::Value::as_array)
79 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Includes array".into()))?;
80 let files = includes
81 .iter()
82 .map(|entry| {
83 entry.as_str().map(PathBuf::from).ok_or_else(|| {
84 CcBypassReason::MalformedDepfile("non-string include path".into())
85 })
86 })
87 .collect::<Result<Vec<_>, _>>()?;
88 Ok(Self { files })
89 }
90
91 pub fn parse(contents: &str) -> Result<Self, CcBypassReason> {
97 let joined = join_continuations(contents)?;
98 let (_, prerequisites) = joined
99 .lines()
100 .find_map(|line| line.split_once(RULE_SEPARATOR))
101 .ok_or_else(|| CcBypassReason::MalformedDepfile("no dependency rule".into()))?;
102 let files = split_prerequisites(prerequisites)?;
103 Ok(Self { files })
104 }
105}
106
107const RULE_SEPARATOR: &str = ": ";
108
109impl CcDepfile {
110 pub fn render(
120 targets: &[crate::DepfileTarget],
121 files: &[PathBuf],
122 source: &Path,
123 phony_targets: bool,
124 ) -> String {
125 let mut rendered = String::new();
126 for (index, target) in targets.iter().enumerate() {
127 if index > 0 {
128 rendered.push(' ');
129 }
130 if target.quoted {
131 rendered.push_str(&escape_make_word(&target.name));
132 } else {
133 rendered.push_str(&target.name);
134 }
135 }
136 rendered.push(':');
137 for file in files {
138 rendered.push_str(" \\\n ");
139 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
140 }
141 rendered.push('\n');
142 if phony_targets {
143 for file in files.iter().filter(|file| file.as_path() != source) {
144 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
145 rendered.push_str(":\n");
146 }
147 }
148 rendered
149 }
150}
151
152fn escape_make_word(word: &str) -> String {
154 let mut escaped = String::with_capacity(word.len());
155 for character in word.chars() {
156 match character {
157 ' ' => escaped.push_str("\\ "),
158 '#' => escaped.push_str("\\#"),
159 '$' => escaped.push_str("$$"),
160 other => escaped.push(other),
161 }
162 }
163 escaped
164}
165
166fn join_continuations(contents: &str) -> Result<String, CcBypassReason> {
168 let mut joined = String::with_capacity(contents.len());
169 let mut continued = false;
170 for line in contents.lines() {
171 let trimmed = line.strip_suffix('\r').unwrap_or(line);
172 let (text, continues) = match trimmed.strip_suffix('\\') {
173 Some(text) => (text, true),
174 None => (trimmed, false),
175 };
176 if continued {
177 joined.push(' ');
178 }
179 joined.push_str(text.trim_end_matches(['\t']));
180 if !continues {
181 joined.push('\n');
182 }
183 continued = continues;
184 }
185 if continued {
186 return Err(CcBypassReason::MalformedDepfile(
187 "unterminated line continuation".into(),
188 ));
189 }
190 Ok(joined)
191}
192
193fn split_prerequisites(value: &str) -> Result<Vec<PathBuf>, CcBypassReason> {
198 if memchr::memchr2(b'\\', b'$', value.as_bytes()).is_none() {
201 return Ok(value
202 .split([' ', '\t'])
203 .filter(|word| !word.is_empty())
204 .map(PathBuf::from)
205 .collect());
206 }
207 split_escaped_prerequisites(value)
208}
209
210fn split_escaped_prerequisites(value: &str) -> Result<Vec<PathBuf>, CcBypassReason> {
211 let mut files = Vec::new();
212 let mut current = String::new();
213 let mut characters = value.chars().peekable();
214 while let Some(character) = characters.next() {
215 match character {
216 ' ' | '\t' => {
217 if !current.is_empty() {
218 files.push(PathBuf::from(std::mem::take(&mut current)));
219 }
220 }
221 '\\' => match characters.next() {
222 Some(' ') => current.push(' '),
223 Some('#') => current.push('#'),
224 Some(other) => {
225 return Err(CcBypassReason::MalformedDepfile(format!(
226 "unmodeled escape \\{other}"
227 )));
228 }
229 None => {
230 return Err(CcBypassReason::MalformedDepfile(
231 "trailing escape character".into(),
232 ));
233 }
234 },
235 '$' => match characters.next() {
236 Some('$') => current.push('$'),
237 Some(other) => {
238 return Err(CcBypassReason::MalformedDepfile(format!(
239 "unmodeled variable reference ${other}"
240 )));
241 }
242 None => {
243 return Err(CcBypassReason::MalformedDepfile(
244 "trailing variable reference".into(),
245 ));
246 }
247 },
248 other => current.push(other),
249 }
250 }
251 if !current.is_empty() {
252 files.push(PathBuf::from(current));
253 }
254 Ok(files)
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct CcDiscoveredInputs {
260 working_dir: PathBuf,
261 pub inputs: Vec<CcActionInput>,
263 identities: Vec<Option<FileIdentity>>,
267}
268
269impl CcDiscoveredInputs {
270 pub fn collect(
277 working_dir: &Path,
278 files: BTreeSet<PathBuf>,
279 directories: BTreeSet<PathBuf>,
280 digests: &dyn FileDigestCache,
281 ) -> Result<Self, CcBypassReason> {
282 if !working_dir.is_absolute() {
283 return Err(CcBypassReason::RelativeWorkingDirectory(
284 working_dir.to_path_buf(),
285 ));
286 }
287 let directories = minimal_manifest_directories(directories);
288 if files.len() + directories.len() > MAX_PREDICTED_INPUTS {
289 return Err(CcBypassReason::TooManyInputs);
290 }
291 let working_dir = normalize_components(working_dir);
292 let mut inputs = Vec::with_capacity(files.len() + directories.len());
293 let mut total_bytes = 0_u64;
294 let mut identified = Vec::with_capacity(files.len());
301 for path in files {
302 let metadata = std::fs::metadata(&path).map_err(|error| CcBypassReason::InputRead {
303 path: path.clone(),
304 message: error.to_string(),
305 })?;
306 if !metadata.is_file() {
307 return Err(CcBypassReason::InputRead {
308 path,
309 message: "input is not a regular file".into(),
310 });
311 }
312 total_bytes = total_bytes.saturating_add(metadata.len());
313 if total_bytes > MAX_INPUT_BYTES {
314 return Err(CcBypassReason::TooManyInputs);
315 }
316 let identity = FileIdentity::for_digest_cache(&path, &metadata).map_err(|error| {
317 CcBypassReason::InputRead {
318 path: path.clone(),
319 message: error.to_string(),
320 }
321 })?;
322 identified.push((path, identity));
323 }
324 let queries = identified
325 .iter()
326 .filter_map(|(_, identity)| identity.clone())
327 .collect::<Vec<_>>();
328 let mut recorded = digests
329 .resolve(FileDigestScope::CcInput, &queries)
330 .into_iter();
331 let mut identities = Vec::with_capacity(inputs.capacity());
332 let mut fresh = Vec::new();
333 for (path, identity) in identified {
334 let resolution = identity
335 .as_ref()
336 .and_then(|_| recorded.next())
337 .unwrap_or(FileDigestResolution::Unresolved);
338 let cached = match resolution {
339 FileDigestResolution::Digest(digest)
340 if identity.as_ref().is_some_and(|identity| {
341 identity.len == digest.size && identity.still_describes().unwrap_or(false)
342 }) =>
343 {
344 Some(Ok(digest))
345 }
346 FileDigestResolution::EmbeddedTimestampMacro
347 if identity
348 .as_ref()
349 .is_some_and(|identity| identity.still_describes().unwrap_or(false)) =>
350 {
351 Some(Err(CcBypassReason::EmbeddedTimestampMacro(path.clone())))
352 }
353 FileDigestResolution::Digest(_)
354 | FileDigestResolution::EmbeddedTimestampMacro
355 | FileDigestResolution::Unresolved => None,
356 };
357 let digest = if let Some(cached) = cached {
358 identities.push(identity);
359 cached?
360 } else {
361 let observed =
362 digest_file_validated(FileDigestScope::CcInput, &path).map_err(|error| {
363 CcBypassReason::InputRead {
364 path: path.clone(),
365 message: error.to_string(),
366 }
367 })?;
368 let (resolution, observed_identity) = if let Some(observed) = observed {
369 let identity = observed.cache_identity.clone();
370 if let FileDigestResolution::Digest(digest) = &observed.resolution
371 && let Some(file) = observed.cache_identity
372 && file.len == digest.size
373 {
374 fresh.push(RecordedFileDigest {
375 file,
376 digest: digest.clone(),
377 });
378 }
379 (observed.resolution, identity)
380 } else {
381 let resolution =
382 digest_file(FileDigestScope::CcInput, &path).map_err(|error| {
383 CcBypassReason::InputRead {
384 path: path.clone(),
385 message: error.to_string(),
386 }
387 })?;
388 (resolution, None)
389 };
390 let FileDigestResolution::Digest(digest) = resolution else {
391 return Err(CcBypassReason::EmbeddedTimestampMacro(path));
392 };
393 identities.push(observed_identity);
394 digest
395 };
396 inputs.push(CcActionInput { path, digest });
397 }
398 if !fresh.is_empty() {
399 digests.record(FileDigestScope::CcInput, fresh);
400 }
401 let mut manifest_entries = 0_usize;
402 for directory in directories {
403 let digest = include_manifest(&directory, &mut manifest_entries)?;
404 inputs.push(CcActionInput {
405 path: PathBuf::from(format!("{INCLUDE_MANIFEST_PREFIX}{}", directory.display())),
406 digest,
407 });
408 identities.push(None);
409 }
410 Ok(Self {
411 working_dir,
412 inputs,
413 identities,
414 })
415 }
416
417 pub fn files(&self) -> impl Iterator<Item = &CcActionInput> {
419 self.inputs
420 .iter()
421 .filter(|input| !is_manifest_input(&input.path))
422 }
423
424 pub fn verify_not_modified_since(&self, started_at: SystemTime) -> Result<(), CcBypassReason> {
431 self.verify_not_modified_since_with_snapshots(started_at, &BTreeMap::new())
432 }
433
434 pub fn verify_not_modified_since_with_snapshots(
437 &self,
438 started_at: SystemTime,
439 before: &BTreeMap<PathBuf, FileSnapshot>,
440 ) -> Result<(), CcBypassReason> {
441 for input in self.files() {
442 if let Some(previous) = before.get(&input.path)
443 && previous.proves_content_change()
444 {
445 let metadata =
446 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
447 path: input.path.clone(),
448 message: error.to_string(),
449 })?;
450 let identity = FileIdentity::for_digest_cache(&input.path, &metadata)
451 .map_err(|error| CcBypassReason::InputRead {
452 path: input.path.clone(),
453 message: error.to_string(),
454 })?
455 .or_else(|| FileIdentity::describe(&input.path, &metadata));
456 if previous.matches(identity.as_ref(), &input.digest) {
457 continue;
458 }
459 return Err(CcBypassReason::InputModifiedDuringCompilation(
460 input.path.clone(),
461 ));
462 }
463 let metadata =
464 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
465 path: input.path.clone(),
466 message: error.to_string(),
467 })?;
468 let modified = metadata
469 .modified()
470 .map_err(|error| CcBypassReason::InputRead {
471 path: input.path.clone(),
472 message: error.to_string(),
473 })?;
474 if modified >= started_at {
475 return Err(CcBypassReason::InputModifiedDuringCompilation(
476 input.path.clone(),
477 ));
478 }
479 }
480 Ok(())
481 }
482
483 pub fn verify_not_modified_since_with_identities(
485 &self,
486 started_at: SystemTime,
487 before: &BTreeMap<PathBuf, FileIdentity>,
488 ) -> Result<(), CcBypassReason> {
489 let snapshots = before
490 .iter()
491 .map(|(path, identity)| (path.clone(), identity.clone().into()))
492 .collect();
493 self.verify_not_modified_since_with_snapshots(started_at, &snapshots)
494 }
495
496 pub fn verify(&self) -> Result<(), CcBypassReason> {
505 for (index, input) in self.inputs.iter().enumerate() {
506 if is_manifest_input(&input.path) {
507 continue;
508 }
509 let read_error = |error: std::io::Error| CcBypassReason::InputRead {
510 path: input.path.clone(),
511 message: error.to_string(),
512 };
513 if let Some(Some(identity)) = self.identities.get(index)
514 && identity.can_skip_content_verification()
515 && identity.still_describes().map_err(read_error)?
516 {
517 continue;
518 }
519 let matches = input.digest.matches_file(&input.path).map_err(|error| {
520 CcBypassReason::InputRead {
521 path: input.path.clone(),
522 message: error.to_string(),
523 }
524 })?;
525 if !matches {
526 return Err(CcBypassReason::InputChanged(input.path.clone()));
527 }
528 }
529 Ok(())
530 }
531
532 pub fn apply_to(self, context: &mut CcActionContext) -> Result<(), CcBypassReason> {
535 if normalize_components(&context.working_dir) != self.working_dir {
536 return Err(CcBypassReason::DiscoveryWorkingDirectory);
537 }
538 context.inputs.extend(self.inputs);
539 Ok(())
540 }
541}
542
543fn minimal_manifest_directories(directories: BTreeSet<PathBuf>) -> Vec<PathBuf> {
551 let mut directories = directories
552 .into_iter()
553 .map(|directory| {
554 let normalized = manifest_directory_identity(&directory);
555 (directory, normalized)
556 })
557 .collect::<Vec<_>>();
558 directories.sort_by(|(left, left_normalized), (right, right_normalized)| {
559 left_normalized
560 .as_deref()
561 .unwrap_or(left)
562 .components()
563 .count()
564 .cmp(
565 &right_normalized
566 .as_deref()
567 .unwrap_or(right)
568 .components()
569 .count(),
570 )
571 .then_with(|| left_normalized.cmp(right_normalized))
572 .then_with(|| left.cmp(right))
573 });
574
575 let mut minimal = Vec::<(PathBuf, Option<PathBuf>)>::new();
576 for (directory, normalized) in directories {
577 if !minimal
578 .iter()
579 .any(|(_, ancestor)| match (ancestor, &normalized) {
580 (Some(ancestor), Some(normalized)) => manifest_covers(ancestor, normalized),
581 _ => false,
582 })
583 {
584 minimal.push((directory, normalized));
585 }
586 }
587 minimal
588 .into_iter()
589 .map(|(directory, _)| directory)
590 .collect()
591}
592
593fn manifest_directory_identity(directory: &Path) -> Option<PathBuf> {
597 let mut normalized = PathBuf::new();
598 for component in directory.components() {
599 match component {
600 std::path::Component::CurDir => {}
601 std::path::Component::ParentDir => {
602 let metadata = std::fs::symlink_metadata(&normalized).ok()?;
603 if !metadata.is_dir() || metadata.file_type().is_symlink() {
604 return None;
605 }
606 normalized.pop();
607 }
608 component => normalized.push(component.as_os_str()),
609 }
610 }
611 Some(normalized)
612}
613
614fn manifest_covers(ancestor: &Path, descendant: &Path) -> bool {
621 let Ok(relative) = descendant.strip_prefix(ancestor) else {
622 return false;
623 };
624 if relative.as_os_str().is_empty() {
625 return true;
626 }
627 let mut current = ancestor.to_path_buf();
628 for component in relative.components() {
629 current.push(component);
630 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
631 return false;
632 };
633 if !metadata.is_dir() || metadata.file_type().is_symlink() {
634 return false;
635 }
636 }
637 true
638}
639
640fn is_manifest_input(path: &Path) -> bool {
641 path.to_str()
642 .is_some_and(|path| path.starts_with(INCLUDE_MANIFEST_PREFIX))
643}
644
645pub fn manifest_snapshot(
653 directories: &BTreeSet<PathBuf>,
654) -> Result<BTreeMap<PathBuf, CacheDigest>, CcBypassReason> {
655 let mut budget = 0_usize;
656 minimal_manifest_directories(directories.iter().cloned().collect())
657 .into_iter()
658 .map(|directory| {
659 include_manifest(&directory, &mut budget).map(|digest| (directory, digest))
660 })
661 .collect()
662}
663
664const INCLUDABLE_EXTENSIONS: &[&str] = &[
675 "c", "c++", "cc", "cpp", "cxx", "def", "gch", "h", "h++", "hh", "hpp", "hxx", "inc", "inl",
676 "ipp", "pch", "s", "tcc",
677];
678
679fn is_includable(name: &str) -> bool {
688 match name.rsplit_once('.') {
689 Some((stem, extension)) if !stem.is_empty() => INCLUDABLE_EXTENSIONS
690 .binary_search(&extension.to_ascii_lowercase().as_str())
691 .is_ok(),
692 _ => !name.starts_with('.'),
694 }
695}
696
697fn include_manifest(directory: &Path, budget: &mut usize) -> Result<CacheDigest, CcBypassReason> {
710 include_manifest_with(directory, budget, &|path| std::fs::read_dir(path))
711}
712
713fn include_manifest_with(
714 directory: &Path,
715 budget: &mut usize,
716 read_dir: &impl Fn(&Path) -> std::io::Result<std::fs::ReadDir>,
717) -> Result<CacheDigest, CcBypassReason> {
718 let mut names = Vec::new();
719 let mut pending = vec![(directory.to_path_buf(), String::new())];
720 while let Some((current, prefix)) = pending.pop() {
721 let entries = match read_dir(¤t) {
722 Ok(entries) => entries,
723 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
724 Err(error) => {
725 return Err(CcBypassReason::InputRead {
726 path: current,
727 message: error.to_string(),
728 });
729 }
730 };
731 for entry in entries {
732 let entry = entry.map_err(|error| CcBypassReason::InputRead {
733 path: current.clone(),
734 message: error.to_string(),
735 })?;
736 let name = entry.file_name();
737 let Some(name) = name.to_str() else {
738 return Err(CcBypassReason::NonUtf8Path(entry.path()));
739 };
740 let relative = if prefix.is_empty() {
741 name.to_string()
742 } else {
743 format!("{prefix}/{name}")
744 };
745 let file_type = entry
746 .file_type()
747 .map_err(|error| CcBypassReason::InputRead {
748 path: entry.path(),
749 message: error.to_string(),
750 })?;
751 if file_type.is_dir() {
752 pending.push((entry.path(), relative));
753 continue;
754 }
755 if !is_includable(name) {
756 continue;
757 }
758 *budget += 1;
759 if *budget > MAX_MANIFEST_ENTRIES {
760 return Err(CcBypassReason::TooManyInputs);
761 }
762 names.push(relative);
763 }
764 }
765 names.sort();
766 let digest = CacheDigest::blake3(names.join("\n").as_bytes());
767 Ok(digest)
768}
769
770pub(crate) fn contains_assembler_input_directive(path: &Path) -> Result<bool, CcBypassReason> {
777 let needles = ASSEMBLER_INPUT_DIRECTIVES;
778 let file = std::fs::File::open(path).map_err(|error| CcBypassReason::InputRead {
779 path: path.to_path_buf(),
780 message: error.to_string(),
781 })?;
782 let longest = needles
783 .iter()
784 .map(|needle| needle.len())
785 .max()
786 .unwrap_or_default();
787 let mut reader = std::io::BufReader::new(file);
788 let mut window = Vec::with_capacity(SCAN_CHUNK_BYTES + longest);
789 let mut chunk = vec![0_u8; SCAN_CHUNK_BYTES];
790 loop {
791 let read = reader
792 .read(&mut chunk)
793 .map_err(|error| CcBypassReason::InputRead {
794 path: path.to_path_buf(),
795 message: error.to_string(),
796 })?;
797 if read == 0 {
798 return Ok(false);
799 }
800 window.extend_from_slice(&chunk[..read]);
801 if needles.iter().any(|needle| {
805 memchr::memchr2_iter(needle[1], needle[1].to_ascii_uppercase(), &window).any(|offset| {
806 offset.checked_sub(1).is_some_and(|start| {
807 window[start..]
808 .get(..needle.len())
809 .is_some_and(|candidate| candidate.eq_ignore_ascii_case(needle))
810 })
811 })
812 }) {
813 return Ok(true);
814 }
815 let keep = window.len().saturating_sub(longest.saturating_sub(1));
816 window.drain(..keep);
817 }
818}
819
820#[cfg(test)]
821#[path = "depfile_tests.rs"]
822mod tests;
823
824#[cfg(test)]
825mod plain_depfile_tests {
826 use super::*;
827
828 #[test]
829 fn plain_paths_preserve_whitespace_unicode_and_make_escapes() {
830 for input in [
831 "",
832 " \t ",
833 "src/main.c include/header.h",
834 "\t路径/é.c a#b.h\t",
835 "a\u{a0}b.h",
836 r"a\ b.h a\#b.h a$$b.h",
837 r"bad\q.h",
838 "bad$",
839 "bad\\",
840 ] {
841 let expected = split_escaped_prerequisites(input);
842 let actual = split_prerequisites(input);
843 match (actual, expected) {
844 (Ok(actual), Ok(expected)) => assert_eq!(actual, expected, "{input}"),
845 (Err(actual), Err(expected)) => {
846 assert_eq!(format!("{actual:?}"), format!("{expected:?}"))
847 }
848 results => panic!("mismatched results for {input}: {results:?}"),
849 }
850 }
851 }
852
853 #[test]
854 #[ignore = "manual performance measurement; run in release mode"]
855 fn benchmark_plain_depfile() {
856 for count in [256, 4096] {
857 for escaped in [false, true] {
858 let prerequisites = (0..count)
859 .map(|index| {
860 format!(
861 " /workspace/{}/include/library-{index}/header-{index}.h",
862 if escaped {
863 r"some\ directory"
864 } else {
865 "some-directory"
866 }
867 )
868 })
869 .collect::<String>();
870 let mut before = Vec::new();
871 let mut after = Vec::new();
872 for round in 0..15 {
873 for optimized in if round % 2 == 0 {
874 [false, true]
875 } else {
876 [true, false]
877 } {
878 let start = std::time::Instant::now();
879 for _ in 0..50 {
880 let result = if optimized {
881 split_prerequisites(std::hint::black_box(&prerequisites))
882 } else {
883 split_escaped_prerequisites(std::hint::black_box(&prerequisites))
884 };
885 std::hint::black_box(result.unwrap());
886 }
887 let sample = start.elapsed().as_secs_f64() * 1e6 / 50.0;
888 if optimized {
889 after.push(sample);
890 } else {
891 before.push(sample);
892 }
893 }
894 }
895 before.sort_by(f64::total_cmp);
896 after.sort_by(f64::total_cmp);
897 eprintln!(
898 "{count} paths, escaped={escaped}: before {:.3} us, after {:.3} us",
899 before[7], after[7]
900 );
901 }
902 }
903 }
904}