Skip to main content

fpm_rs/
benchmark_bundle.rs

1//! Normalized benchmark comparison bundles.
2
3use 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
24/// Current benchmark-bundle manifest format version.
25pub 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/// Manifest-described Parquet tables in a benchmark bundle.
33#[derive(Clone, Debug)]
34pub struct BenchmarkBundleTables {
35    /// One-row-per-run summary table artifact.
36    pub runs: BundleArtifact,
37    /// Per-acquisition-frame metrics table artifact.
38    pub frames: BundleArtifact,
39    /// Generated artifact-path table artifact.
40    pub artifacts: BundleArtifact,
41    /// Extensible benchmark metadata table artifact.
42    pub metadata: BundleArtifact,
43}
44
45/// Reopened benchmark suite with table metadata and lazily loaded result bundles.
46#[derive(Clone)]
47pub struct BenchmarkBundle {
48    /// Root directory containing the benchmark bundle.
49    pub path: PathBuf,
50    /// Path to the benchmark manifest JSON file.
51    pub manifest_path: PathBuf,
52    /// Stable benchmark-suite name.
53    pub name: String,
54    /// Optional human-readable benchmark label.
55    pub label: Option<String>,
56    /// Parquet table artifact descriptors.
57    pub tables: BenchmarkBundleTables,
58    /// Result bundles keyed by benchmark run ID.
59    pub results: BTreeMap<String, ResultBundle>,
60}
61
62/// Options controlling benchmark-bundle metadata.
63#[derive(Clone, Debug, Default)]
64pub struct BenchmarkBundleExportOptions {
65    /// Optional human-readable bundle label.
66    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
90/// Atomically writes benchmark tables plus referenced reconstruction result bundles.
91///
92/// Every record must have a unique run ID and every supplied result must correspond to a
93/// successful record. Existing complete destinations are rejected.
94pub 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
211/// Verifies and reopens a complete benchmark bundle and its nested result manifests.
212pub 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}