metrale_bench/gate/
record_path.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: File names for gate records under `.benchmarks/<id>/`: the
4//! default variant, model variants, shards and same-day re-runs.
5//!
6//! Owner: bench gate (records).
7//! Invariants: none beyond the types.
8
9use std::path::{Path, PathBuf};
10
11use sha2::{Digest, Sha256};
12
13use super::gate_dir;
14use super::record::{GateBaseline, GateRecord};
15
16/// 2026-09-26: `YYYY-MM-DD` (UTC) from unix seconds.
17pub fn date_of(unix_secs: u64) -> String {
18    let days = (unix_secs / 86_400) as i64 + 719_468;
19    let era = days.div_euclid(146_097);
20    let doe = days.rem_euclid(146_097) as u64;
21    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
22    let y = yoe as i64 + era * 400;
23    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
24    let mp = (5 * doy + 2) / 153;
25    let d = doy - (153 * mp + 2) / 5 + 1;
26    let m = if mp < 10 { mp + 3 } else { mp - 9 };
27    let y = if m <= 2 { y + 1 } else { y };
28    format!("{y:04}-{m:02}-{d:02}")
29}
30
31/// 2026-09-26: `HHMMSS` (UTC) from unix seconds.
32pub fn time_of_day(unix_secs: u64) -> String {
33    let secs = unix_secs % 86_400;
34    format!("{:02}{:02}{:02}", secs / 3_600, (secs / 60) % 60, secs % 60)
35}
36
37/// 2026-09-26: `.benchmarks/<id>/YYYY-MM-DD-<sha>.json` under `root`.
38pub fn record_path(root: &Path, benchmark_id: &str, unix_secs: u64, sha: &str) -> PathBuf {
39    gate_dir(root, benchmark_id).join(format!("{}-{sha}.json", date_of(unix_secs)))
40}
41
42/// 2026-09-26: The re-run name beside `canonical`: the day segment gains the
43/// run's UTC time of day, `YYYY-MM-DDTHHMMSSZ-<sha>[-<variant>][-s<i>of<n>].json`,
44/// and the rest of the name is kept. A lexical sort orders it after
45/// `canonical` (`-` sorts before `T`).
46///
47/// Panics when the file name does not start with the UTC day of `unix_secs`.
48pub fn rerun_path(canonical: &Path, unix_secs: u64) -> PathBuf {
49    let name = canonical
50        .file_name()
51        .map(|n| n.to_string_lossy().into_owned())
52        .unwrap_or_default();
53    let day = date_of(unix_secs);
54    let tail = name
55        .strip_prefix(day.as_str())
56        .unwrap_or_else(|| panic!("record name {name:?} does not start with its day {day}"));
57    canonical.with_file_name(format!("{day}T{}Z{tail}", time_of_day(unix_secs)))
58}
59
60/// 2026-09-26: The file-name tail for one shard's record,
61/// `-s<index>of<count>`, 0-based like the `--param shard=i/n` that ran it.
62/// Empty for a whole-draw run.
63pub fn shard_suffix(shard: Option<(usize, usize)>) -> String {
64    shard.map_or(String::new(), |(i, n)| format!("-s{i}of{n}"))
65}
66
67/// 2026-09-26: A file-name-safe slug of a checkpoint id: ASCII alphanumerics
68/// and `.` kept and lowercased, each run of other characters one `-`, edge
69/// `-` trimmed. Two ids can share a slug.
70pub fn variant_slug(model: &str) -> String {
71    let mut out = String::with_capacity(model.len());
72    for c in model.chars() {
73        let mapped = if c.is_ascii_alphanumeric() || c == '.' {
74            c.to_ascii_lowercase()
75        } else {
76            '-'
77        };
78        if mapped == '-' && out.ends_with('-') {
79            continue;
80        }
81        out.push(mapped);
82    }
83    out.trim_matches('-').to_string()
84}
85
86/// 2026-09-26: The variant's slug, with the first 16 hex digits of the id's
87/// SHA-256 appended when another checkpoint in `baseline` has the same slug.
88fn variant_file_slug(baseline: &GateBaseline, model: &str) -> String {
89    let slug = variant_slug(model);
90    let collides = baseline
91        .hardware
92        .values()
93        .flat_map(|hardware| hardware.models.keys())
94        .any(|other| other != model && variant_slug(other) == slug);
95    if !collides {
96        return slug;
97    }
98    let digest = metrale_closure::hex_lower(&Sha256::digest(model.as_bytes()));
99    format!("{slug}-{}", &digest[..16])
100}
101
102/// 2026-09-26: Where `record` is written: `<date>-<sha><shard>.json` for its
103/// hardware's default model, or when no baseline with hardware entries
104/// resolves; otherwise `<date>-<sha>-<variant slug><shard>.json`. A record
105/// whose hardware has no entry counts as default when it is any hardware's
106/// default.
107pub fn record_path_for(root: &Path, record: &GateRecord) -> PathBuf {
108    let shard = shard_suffix(record.shard());
109    let legacy = gate_dir(root, &record.benchmark_id).join(format!(
110        "{}-{}{shard}.json",
111        date_of(record.recorded_at),
112        record.git_sha
113    ));
114    let Ok(baseline) = super::bench::baseline_for(root, &record.benchmark_id) else {
115        return legacy;
116    };
117    if baseline.hardware.is_empty() {
118        return legacy;
119    }
120    let hardware = record.hardware.gate_key();
121    let is_default = match baseline.hardware.get(&hardware) {
122        Some(hw) => hw.default == record.target_model,
123        None => baseline
124            .hardware
125            .values()
126            .any(|hw| hw.default == record.target_model),
127    };
128    if is_default {
129        legacy
130    } else {
131        gate_dir(root, &record.benchmark_id).join(format!(
132            "{}-{}-{}{shard}.json",
133            date_of(record.recorded_at),
134            record.git_sha,
135            variant_file_slug(&baseline, &record.target_model)
136        ))
137    }
138}