1use std::{
4 collections::{BTreeMap, BTreeSet},
5 fs::{self, File},
6 io::{BufReader, BufWriter, Write},
7 path::{Component, Path, PathBuf},
8};
9
10use polars::prelude::{DataFrame, DataType, ParquetReader, SerReader};
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 Error, Result,
15 benchmark::BenchmarkRecord,
16 reconstruction::{BundleArtifact, BundleExportOptions, ReconstructionResult, ResultBundle},
17 tabular::{
18 benchmark_artifacts_dataframe, benchmark_frames_dataframe, benchmark_metadata_dataframe,
19 benchmark_runs_dataframe,
20 parquet::{ParquetWriteOptions, sha256, write_parquet_file},
21 },
22};
23
24pub const BENCHMARK_BUNDLE_FORMAT_VERSION: u32 = 1;
26
27const RUNS: &str = "tables.runs";
28const FRAMES: &str = "tables.frames";
29const ARTIFACTS: &str = "tables.artifacts";
30const METADATA: &str = "tables.metadata";
31
32#[derive(Clone, Debug)]
34pub struct BenchmarkBundleTables {
35 pub runs: BundleArtifact,
37 pub frames: BundleArtifact,
39 pub artifacts: BundleArtifact,
41 pub metadata: BundleArtifact,
43}
44
45#[derive(Clone)]
47pub struct BenchmarkBundle {
48 pub path: PathBuf,
50 pub manifest_path: PathBuf,
52 pub name: String,
54 pub label: Option<String>,
56 pub tables: BenchmarkBundleTables,
58 pub results: BTreeMap<String, ResultBundle>,
60}
61
62#[derive(Clone, Debug, Default)]
64pub struct BenchmarkBundleExportOptions {
65 pub label: Option<String>,
67}
68
69#[derive(Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71struct BenchmarkManifest {
72 benchmark_bundle_format_version: u32,
73 name: String,
74 label: Option<String>,
75 crate_version: String,
76 artifacts: Vec<BenchmarkManifestArtifact>,
77 results: BTreeMap<String, PathBuf>,
78}
79
80#[derive(Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82struct BenchmarkManifestArtifact {
83 role: String,
84 relative_path: PathBuf,
85 media_type: String,
86 byte_size: u64,
87 sha256: String,
88}
89
90pub fn write_benchmark_bundle(
95 path: impl AsRef<Path>,
96 name: impl Into<String>,
97 records: &[BenchmarkRecord],
98 results: &BTreeMap<String, ReconstructionResult>,
99 options: BenchmarkBundleExportOptions,
100) -> Result<BenchmarkBundle> {
101 validate_records(records, results)?;
102 let name = name.into();
103 if name.is_empty() {
104 return Err(Error::InvalidParameter {
105 name: "benchmark name",
106 reason: "must be non-empty".into(),
107 });
108 }
109 let (final_path, workspace) = unique_paths(path.as_ref())?;
110 if let Some(parent) = final_path.parent() {
111 fs::create_dir_all(parent)?;
112 }
113 fs::create_dir(&workspace)?;
114 fs::create_dir(workspace.join("results"))?;
115 write_json(
116 &workspace.join("run-state.json"),
117 &serde_json::json!({
118 "benchmark_bundle_format_version": BENCHMARK_BUNDLE_FORMAT_VERSION,
119 "phase": "exporting",
120 }),
121 )?;
122
123 let export = (|| -> Result<(Vec<BenchmarkManifestArtifact>, BTreeMap<String, PathBuf>)> {
124 let mut result_paths = BTreeMap::new();
125 for record in records {
126 let Some(result) = results.get(&record.run_id) else {
127 continue;
128 };
129 let relative = PathBuf::from("results").join(&record.run_id);
130 result.write_bundle(
131 workspace.join(&relative),
132 BundleExportOptions {
133 run_id: Some(record.run_id.clone()),
134 label: Some(format!("{}: {}", record.case_id, record.algorithm)),
135 include_previews: false,
136 },
137 )?;
138 result_paths.insert(record.run_id.clone(), relative);
139 }
140 let relative_strings = result_paths
141 .iter()
142 .map(|(run_id, path)| (run_id.clone(), path.to_string_lossy().into_owned()))
143 .collect();
144 let mut artifacts = Vec::with_capacity(4);
145 let writer_options = ParquetWriteOptions::default();
146 write_benchmark_table(
147 &workspace,
148 "tables/runs.parquet",
149 RUNS,
150 &name,
151 benchmark_runs_dataframe(records)?,
152 &writer_options,
153 &mut artifacts,
154 )?;
155 write_benchmark_table(
156 &workspace,
157 "tables/frames.parquet",
158 FRAMES,
159 &name,
160 benchmark_frames_dataframe(records)?,
161 &writer_options,
162 &mut artifacts,
163 )?;
164 write_benchmark_table(
165 &workspace,
166 "tables/artifacts.parquet",
167 ARTIFACTS,
168 &name,
169 benchmark_artifacts_dataframe(records, &relative_strings)?,
170 &writer_options,
171 &mut artifacts,
172 )?;
173 write_benchmark_table(
174 &workspace,
175 "tables/metadata.parquet",
176 METADATA,
177 &name,
178 benchmark_metadata_dataframe(records)?,
179 &writer_options,
180 &mut artifacts,
181 )?;
182 Ok((artifacts, result_paths))
183 })();
184 let (artifacts, result_paths) = match export {
185 Ok(value) => value,
186 Err(error) => {
187 let _ = write_json(
188 &workspace.join("run-state.json"),
189 &serde_json::json!({
190 "benchmark_bundle_format_version": BENCHMARK_BUNDLE_FORMAT_VERSION,
191 "phase": "failed",
192 }),
193 );
194 return Err(error);
195 }
196 };
197 fs::remove_file(workspace.join("run-state.json"))?;
198 let manifest = BenchmarkManifest {
199 benchmark_bundle_format_version: BENCHMARK_BUNDLE_FORMAT_VERSION,
200 name,
201 label: options.label,
202 crate_version: env!("CARGO_PKG_VERSION").into(),
203 artifacts,
204 results: result_paths,
205 };
206 write_json(&workspace.join("manifest.json"), &manifest)?;
207 fs::rename(workspace, &final_path)?;
208 read_benchmark_bundle(final_path)
209}
210
211pub fn read_benchmark_bundle(path: impl AsRef<Path>) -> Result<BenchmarkBundle> {
213 let path = path.as_ref();
214 if path
215 .file_name()
216 .and_then(|value| value.to_str())
217 .is_some_and(|value| value.ends_with(".inprogress"))
218 {
219 return Err(Error::IncompleteBundle(format!(
220 "{} is an in-progress benchmark workspace",
221 path.display()
222 )));
223 }
224 let manifest_path = path.join("manifest.json");
225 let manifest: BenchmarkManifest =
226 serde_json::from_reader(BufReader::new(File::open(&manifest_path)?))
227 .map_err(|error| Error::InvalidManifest(error.to_string()))?;
228 if manifest.benchmark_bundle_format_version != BENCHMARK_BUNDLE_FORMAT_VERSION {
229 return Err(Error::UnsupportedBundleVersion {
230 actual: manifest.benchmark_bundle_format_version,
231 supported: BENCHMARK_BUNDLE_FORMAT_VERSION,
232 });
233 }
234 if manifest.name.is_empty() || manifest.crate_version.is_empty() {
235 return Err(Error::InvalidManifest(
236 "benchmark name and crate_version must be non-empty".into(),
237 ));
238 }
239 let root = path.canonicalize()?;
240 let mut handles = BTreeMap::new();
241 for artifact in &manifest.artifacts {
242 if ![RUNS, FRAMES, ARTIFACTS, METADATA].contains(&artifact.role.as_str()) {
243 return Err(Error::UnsupportedArtifactRole(artifact.role.clone()));
244 }
245 validate_relative(&artifact.relative_path)?;
246 let artifact_path = path.join(&artifact.relative_path);
247 let canonical_artifact = artifact_path.canonicalize().map_err(|error| {
248 if error.kind() == std::io::ErrorKind::NotFound {
249 Error::MissingArtifact {
250 role: artifact.role.clone(),
251 }
252 } else {
253 Error::Io(error)
254 }
255 })?;
256 if !canonical_artifact.starts_with(&root) {
257 return Err(Error::InvalidRelativePath(
258 artifact.relative_path.display().to_string(),
259 ));
260 }
261 if fs::metadata(&artifact_path)?.len() != artifact.byte_size {
262 return Err(Error::InvalidManifest(format!(
263 "benchmark artifact {} has the wrong size",
264 artifact.role
265 )));
266 }
267 if artifact.media_type != "application/vnd.apache.parquet"
268 || artifact.sha256.len() != 64
269 || !artifact.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
270 {
271 return Err(Error::InvalidManifest(format!(
272 "benchmark artifact {} has invalid media type or digest metadata",
273 artifact.role
274 )));
275 }
276 if sha256(&artifact_path)? != artifact.sha256 {
277 return Err(Error::ArtifactHashMismatch {
278 role: artifact.role.clone(),
279 });
280 }
281 if handles
282 .insert(
283 artifact.role.clone(),
284 BundleArtifact {
285 role: artifact.role.clone(),
286 path: artifact_path,
287 media_type: artifact.media_type.clone(),
288 byte_size: artifact.byte_size,
289 sha256: artifact.sha256.clone(),
290 dtype: None,
291 shape: None,
292 },
293 )
294 .is_some()
295 {
296 return Err(Error::InvalidManifest(format!(
297 "duplicate benchmark artifact role {}",
298 artifact.role
299 )));
300 }
301 }
302 let required = |role: &str| {
303 handles
304 .get(role)
305 .cloned()
306 .ok_or_else(|| Error::MissingArtifact { role: role.into() })
307 };
308 let tables = BenchmarkBundleTables {
309 runs: required(RUNS)?,
310 frames: required(FRAMES)?,
311 artifacts: required(ARTIFACTS)?,
312 metadata: required(METADATA)?,
313 };
314 let successful_run_ids = validate_benchmark_tables(&tables, &manifest.results)?;
315 let manifest_run_ids = manifest.results.keys().cloned().collect::<BTreeSet<_>>();
316 if successful_run_ids != manifest_run_ids {
317 return Err(Error::InvalidManifest(
318 "successful benchmark runs and nested result bundles differ".into(),
319 ));
320 }
321 let mut results = BTreeMap::new();
322 for (run_id, relative) in &manifest.results {
323 validate_relative(relative)?;
324 let result_path = path.join(relative);
325 let canonical_result = result_path.canonicalize().map_err(|error| {
326 if error.kind() == std::io::ErrorKind::NotFound {
327 Error::MissingArtifact {
328 role: format!("results.{run_id}"),
329 }
330 } else {
331 Error::Io(error)
332 }
333 })?;
334 if !canonical_result.starts_with(&root) {
335 return Err(Error::InvalidRelativePath(relative.display().to_string()));
336 }
337 let result = ResultBundle::read(result_path)?;
338 if result.run_id != *run_id {
339 return Err(Error::InconsistentRunId {
340 expected: run_id.clone(),
341 actual: result.run_id,
342 });
343 }
344 results.insert(run_id.clone(), result);
345 }
346 Ok(BenchmarkBundle {
347 path: path.into(),
348 manifest_path,
349 name: manifest.name,
350 label: manifest.label,
351 tables,
352 results,
353 })
354}
355
356fn validate_benchmark_tables(
357 tables: &BenchmarkBundleTables,
358 result_paths: &BTreeMap<String, PathBuf>,
359) -> Result<BTreeSet<String>> {
360 let runs = read_table(&tables.runs.path)?;
361 validate_schema(
362 RUNS,
363 &runs,
364 &[
365 ("run_id", DataType::String, false),
366 ("case_id", DataType::String, true),
367 ("algorithm", DataType::String, false),
368 ("algorithm_configuration", DataType::String, true),
369 ("dataset_name", DataType::String, true),
370 ("dataset_version", DataType::String, true),
371 ("random_seed", DataType::UInt64, true),
372 ("frame_count", DataType::UInt64, true),
373 ("completed_iterations", DataType::UInt64, true),
374 ("elapsed_seconds", DataType::Float64, false),
375 ("final_objective", DataType::Float64, true),
376 ("success", DataType::Boolean, false),
377 ("error", DataType::String, true),
378 ("repetition", DataType::UInt64, true),
379 ("warmup", DataType::Boolean, true),
380 ("benchmark_group", DataType::String, true),
381 ("reference_run_id", DataType::String, true),
382 ],
383 )?;
384 let run_ids = runs.column("run_id")?.str()?;
385 let case_ids = runs.column("case_id")?.str()?;
386 let successes = runs.column("success")?.bool()?;
387 let errors = runs.column("error")?.str()?;
388 let completed_iterations = runs.column("completed_iterations")?.u64()?;
389 let final_objectives = runs.column("final_objective")?.f64()?;
390 let frame_counts = runs.column("frame_count")?.u64()?;
391 let mut all_run_ids = BTreeSet::new();
392 let mut successful_run_ids = BTreeSet::new();
393 let mut expected_frame_counts = BTreeMap::new();
394 for row in 0..runs.height() {
395 let missing = |column: &str| Error::InvalidParquetSchema {
396 role: RUNS.into(),
397 reason: format!("required benchmark column {column} contains null"),
398 };
399 let run_id = run_ids.get(row).ok_or_else(|| missing("run_id"))?;
400 let case_id = case_ids.get(row).ok_or_else(|| missing("case_id"))?;
401 let success = successes.get(row).ok_or_else(|| missing("success"))?;
402 if run_id.is_empty() || case_id.is_empty() || !all_run_ids.insert(run_id.to_owned()) {
403 return Err(Error::InvalidParquetSchema {
404 role: RUNS.into(),
405 reason: "run_id and case_id must be non-empty and run_id unique".into(),
406 });
407 }
408 let valid_status = if success {
409 errors.get(row).is_none() && completed_iterations.get(row).is_some()
410 } else {
411 errors.get(row).is_some_and(|value| !value.is_empty())
412 && completed_iterations.get(row).is_none()
413 && final_objectives.get(row).is_none()
414 };
415 if !valid_status {
416 return Err(Error::InvalidParquetSchema {
417 role: RUNS.into(),
418 reason: "success, error, and result fields are inconsistent".into(),
419 });
420 }
421 if success {
422 successful_run_ids.insert(run_id.to_owned());
423 }
424 expected_frame_counts.insert(
425 run_id.to_owned(),
426 frame_counts
427 .get(row)
428 .ok_or_else(|| missing("frame_count"))?,
429 );
430 }
431
432 for (artifact, role, schema) in [
433 (
434 &tables.frames,
435 FRAMES,
436 &[
437 ("run_id", DataType::String, false),
438 ("frame_index", DataType::UInt64, false),
439 ("original_frame_index", DataType::UInt64, false),
440 ("original_illumination_index", DataType::UInt64, true),
441 ("normalized_l2", DataType::Float64, true),
442 ][..],
443 ),
444 (
445 &tables.artifacts,
446 ARTIFACTS,
447 &[
448 ("run_id", DataType::String, false),
449 ("role", DataType::String, false),
450 ("relative_path", DataType::String, false),
451 ][..],
452 ),
453 (
454 &tables.metadata,
455 METADATA,
456 &[
457 ("run_id", DataType::String, false),
458 ("key", DataType::String, false),
459 ("value", DataType::String, false),
460 ][..],
461 ),
462 ] {
463 let dataframe = read_table(&artifact.path)?;
464 validate_schema(role, &dataframe, schema)?;
465 for run_id in dataframe.column("run_id")?.str()?.iter().flatten() {
466 if !all_run_ids.contains(run_id) {
467 return Err(Error::InconsistentRunId {
468 expected: "one of the IDs in tables.runs".into(),
469 actual: run_id.into(),
470 });
471 }
472 }
473 }
474
475 let frames = read_table(&tables.frames.path)?;
476 let mut next_frame = BTreeMap::<String, u64>::new();
477 for (run_id, frame_index) in frames
478 .column("run_id")?
479 .str()?
480 .iter()
481 .flatten()
482 .zip(frames.column("frame_index")?.u64()?.into_no_null_iter())
483 {
484 let expected = next_frame.entry(run_id.into()).or_default();
485 if frame_index != *expected {
486 return Err(Error::InvalidParquetSchema {
487 role: FRAMES.into(),
488 reason: "frame indexes must be ordered, unique, and zero-based per run".into(),
489 });
490 }
491 *expected += 1;
492 }
493 if expected_frame_counts
494 .iter()
495 .any(|(run_id, &count)| next_frame.get(run_id).copied().unwrap_or(0) != count)
496 {
497 return Err(Error::InvalidParquetSchema {
498 role: FRAMES.into(),
499 reason: "frame rows must match each run's declared frame_count".into(),
500 });
501 }
502
503 let artifacts = read_table(&tables.artifacts.path)?;
504 let artifact_run_ids = artifacts.column("run_id")?.str()?;
505 let artifact_roles = artifacts.column("role")?.str()?;
506 let artifact_paths = artifacts.column("relative_path")?.str()?;
507 let mut artifact_ids = BTreeSet::new();
508 for ((run_id, role), relative_path) in artifact_run_ids
509 .iter()
510 .flatten()
511 .zip(artifact_roles.iter().flatten())
512 .zip(artifact_paths.iter().flatten())
513 {
514 if role != "result_bundle"
515 || !artifact_ids.insert(run_id.to_owned())
516 || validate_relative(Path::new(relative_path)).is_err()
517 || result_paths
518 .get(run_id)
519 .is_none_or(|expected| expected != Path::new(relative_path))
520 {
521 return Err(Error::InvalidParquetSchema {
522 role: ARTIFACTS.into(),
523 reason: "result artifacts must have one safe result_bundle path per run".into(),
524 });
525 }
526 }
527 if artifact_ids != successful_run_ids {
528 return Err(Error::InvalidParquetSchema {
529 role: ARTIFACTS.into(),
530 reason: "artifact rows and successful runs differ".into(),
531 });
532 }
533
534 let metadata = read_table(&tables.metadata.path)?;
535 let mut metadata_keys = BTreeSet::new();
536 for (run_id, key) in metadata
537 .column("run_id")?
538 .str()?
539 .iter()
540 .flatten()
541 .zip(metadata.column("key")?.str()?.iter().flatten())
542 {
543 if !metadata_keys.insert((run_id.to_owned(), key.to_owned())) {
544 return Err(Error::InvalidParquetSchema {
545 role: METADATA.into(),
546 reason: "metadata keys must be unique per run".into(),
547 });
548 }
549 }
550 Ok(successful_run_ids)
551}
552
553fn read_table(path: &Path) -> Result<DataFrame> {
554 Ok(ParquetReader::new(File::open(path)?).finish()?)
555}
556
557fn validate_schema(
558 role: &str,
559 dataframe: &DataFrame,
560 expected: &[(&str, DataType, bool)],
561) -> Result<()> {
562 if dataframe.width() != expected.len() {
563 return Err(Error::InvalidParquetSchema {
564 role: role.into(),
565 reason: format!(
566 "found {} columns, expected {}",
567 dataframe.width(),
568 expected.len()
569 ),
570 });
571 }
572 for &(name, ref dtype, nullable) in expected {
573 let column = dataframe
574 .column(name)
575 .map_err(|error| Error::InvalidParquetSchema {
576 role: role.into(),
577 reason: error.to_string(),
578 })?;
579 if column.dtype() != dtype || (!nullable && column.null_count() != 0) {
580 return Err(Error::InvalidParquetSchema {
581 role: role.into(),
582 reason: format!(
583 "column {name} has dtype {:?} and {} nulls; expected {dtype:?}",
584 column.dtype(),
585 column.null_count()
586 ),
587 });
588 }
589 }
590 Ok(())
591}
592
593fn validate_records(
594 records: &[BenchmarkRecord],
595 results: &BTreeMap<String, ReconstructionResult>,
596) -> Result<()> {
597 if records.is_empty() {
598 return Err(Error::InvalidParameter {
599 name: "benchmark records",
600 reason: "must contain at least one run".into(),
601 });
602 }
603 let mut run_ids = BTreeSet::new();
604 for record in records {
605 if record.format_version != crate::benchmark::BENCHMARK_RECORD_FORMAT_VERSION
606 || record.run_id.is_empty()
607 || record.case_id.is_empty()
608 || record.dataset_name.is_empty()
609 || record.algorithm.is_empty()
610 || record.crate_version.is_empty()
611 || !run_ids.insert(record.run_id.as_str())
612 {
613 return Err(Error::InvalidParameter {
614 name: "benchmark records",
615 reason:
616 "records must use the supported version, required identities must be non-empty, and run_id must be unique"
617 .into(),
618 });
619 }
620 if !record.elapsed_seconds.is_finite()
621 || record.elapsed_seconds < 0.0
622 || record.frame_count != record.frames.len()
623 || record.success != record.error.is_none()
624 || record
625 .error
626 .as_ref()
627 .is_some_and(|message| message.is_empty())
628 {
629 return Err(Error::InvalidParameter {
630 name: "benchmark records",
631 reason: "run status, timing, frame count, or error message is inconsistent".into(),
632 });
633 }
634 let run_path = Path::new(&record.run_id);
635 if run_path.components().count() != 1
636 || !matches!(run_path.components().next(), Some(Component::Normal(_)))
637 {
638 return Err(Error::InvalidParameter {
639 name: "benchmark run_id",
640 reason: "must be safe as one result-directory name".into(),
641 });
642 }
643 if record.success != results.contains_key(&record.run_id) {
644 return Err(Error::InvalidParameter {
645 name: "benchmark results",
646 reason: format!(
647 "successful run {} must have exactly one ReconstructionResult",
648 record.run_id
649 ),
650 });
651 }
652 for (index, frame) in record.frames.iter().enumerate() {
653 if frame.frame_index != index {
654 return Err(Error::InvalidParameter {
655 name: "benchmark frame records",
656 reason: "frame indexes must be unique, ordered, and zero-based".into(),
657 });
658 }
659 }
660 }
661 if results
662 .keys()
663 .any(|run_id| !run_ids.contains(run_id.as_str()))
664 {
665 return Err(Error::InvalidParameter {
666 name: "benchmark results",
667 reason: "result map contains an unknown run_id".into(),
668 });
669 }
670 Ok(())
671}
672
673fn validate_relative(path: &Path) -> Result<()> {
674 if path.as_os_str().is_empty()
675 || path.is_absolute()
676 || path
677 .components()
678 .any(|component| !matches!(component, Component::Normal(_)))
679 {
680 return Err(Error::InvalidRelativePath(path.display().to_string()));
681 }
682 Ok(())
683}
684
685fn write_json(path: &Path, value: &impl Serialize) -> Result<()> {
686 if let Some(parent) = path.parent() {
687 fs::create_dir_all(parent)?;
688 }
689 let file = File::create(path)?;
690 let mut writer = BufWriter::new(file);
691 serde_json::to_writer_pretty(&mut writer, value)?;
692 writer.flush()?;
693 writer.get_ref().sync_all()?;
694 Ok(())
695}
696
697fn write_benchmark_table(
698 workspace: &Path,
699 relative_path: &str,
700 role: &str,
701 name: &str,
702 mut dataframe: DataFrame,
703 options: &ParquetWriteOptions,
704 artifacts: &mut Vec<BenchmarkManifestArtifact>,
705) -> Result<()> {
706 let table_path = workspace.join(relative_path);
707 write_parquet_file(&table_path, role, name, &mut dataframe, options)?;
708 artifacts.push(BenchmarkManifestArtifact {
709 role: role.into(),
710 relative_path: relative_path.into(),
711 media_type: "application/vnd.apache.parquet".into(),
712 byte_size: fs::metadata(&table_path)?.len(),
713 sha256: sha256(&table_path)?,
714 });
715 Ok(())
716}
717
718fn unique_paths(requested: &Path) -> Result<(PathBuf, PathBuf)> {
719 let name = requested
720 .file_name()
721 .and_then(|value| value.to_str())
722 .ok_or_else(|| Error::InvalidParameter {
723 name: "benchmark bundle path",
724 reason: "must name a UTF-8 directory".into(),
725 })?;
726 for suffix in 0_u64.. {
727 let final_path = if suffix == 0 {
728 requested.to_owned()
729 } else {
730 requested.with_file_name(format!("{name}-{suffix}"))
731 };
732 let final_name = final_path
733 .file_name()
734 .and_then(|value| value.to_str())
735 .ok_or_else(|| Error::InvalidParameter {
736 name: "benchmark bundle path",
737 reason: "must name a UTF-8 directory".into(),
738 })?;
739 let workspace = final_path.with_file_name(format!("{final_name}.inprogress"));
740 if !final_path.exists() && !workspace.exists() {
741 return Ok((final_path, workspace));
742 }
743 }
744 Err(Error::InvalidParameter {
745 name: "benchmark bundle path",
746 reason: "could not choose a unique directory".into(),
747 })
748}