use anyhow::Result;
use serde::{Deserialize, Serialize};
use stt_core::curve::{self, BlobOrdering};
use stt_core::ordering_sim::{self, SimOptions, TileSample};
use crate::packed::PackedTileset;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderingCostRow {
pub ordering: String,
pub scrub_reads: u64,
pub scrub_bytes: u64,
pub pan_reads: u64,
pub pan_bytes: u64,
pub total_reads: u64,
pub total_bytes: u64,
pub total_cost: u64,
pub recommended: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderAuditReport {
pub native_tiles: usize,
pub coalesce_gap_bytes: u64,
pub orderings: Vec<OrderingCostRow>,
pub recommended: String,
pub auto_choice: String,
pub current: Option<String>,
}
pub fn order_audit(tileset: &PackedTileset) -> Result<OrderAuditReport> {
let (_min_z, max_z) = tileset.zoom_range();
let bucket_ms = (tileset.temporal_bucket_ms().max(1)) as i64;
let samples: Vec<TileSample> = tileset
.entries()
.iter()
.filter(|e| e.zoom == max_z)
.map(|e| TileSample {
z: e.zoom,
x: e.x,
y: e.y,
hilbert: e.hilbert,
time_start: e.time_start,
tb: e.time_start.div_euclid(bucket_ms),
len: e.length as u64,
})
.collect();
let opts = SimOptions::default();
let ranked = ordering_sim::evaluate(&samples, opts);
let recommended = ordering_sim::measured_ordering(&samples, opts);
let orderings: Vec<OrderingCostRow> = ranked
.iter()
.map(|c| OrderingCostRow {
ordering: c.ordering.as_str().to_string(),
scrub_reads: c.scrub.reads,
scrub_bytes: c.scrub.bytes_read,
pan_reads: c.pan.reads,
pan_bytes: c.pan.bytes_read,
total_reads: c.total_reads,
total_bytes: c.total_bytes_read,
total_cost: c.cost,
recommended: c.ordering == recommended,
})
.collect();
let auto_choice = if samples.is_empty() {
BlobOrdering::SpatialMajor
} else {
let (mut x_min, mut x_max, mut y_min, mut y_max) = (u32::MAX, 0u32, u32::MAX, 0u32);
let (mut tb_min, mut tb_max) = (i64::MAX, i64::MIN);
for s in &samples {
x_min = x_min.min(s.x);
x_max = x_max.max(s.x);
y_min = y_min.min(s.y);
y_max = y_max.max(s.y);
tb_min = tb_min.min(s.tb);
tb_max = tb_max.max(s.tb);
}
let space_bits = curve::bits_for((x_max - x_min).max(y_max - y_min) as u64 + 1);
let time_bits = curve::bits_for((tb_max - tb_min).max(0) as u64 + 1);
BlobOrdering::choose(space_bits, time_bits)
};
Ok(OrderAuditReport {
native_tiles: samples.len(),
coalesce_gap_bytes: opts.coalesce_gap_bytes,
orderings,
recommended: recommended.as_str().to_string(),
auto_choice: auto_choice.as_str().to_string(),
current: tileset.manifest().blob_ordering.clone(),
})
}
fn mib(bytes: u64) -> String {
if bytes >= 1024 * 1024 {
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{} KiB", bytes / 1024)
} else {
format!("{bytes} B")
}
}
pub fn format_text(r: &OrderAuditReport) -> String {
use std::fmt::Write;
let mut s = String::new();
let _ = writeln!(
s,
"Blob-ordering audit — {} native tiles, {} coalescing gap",
r.native_tiles,
mib(r.coalesce_gap_bytes)
);
let _ = writeln!(s);
let _ = writeln!(
s,
" {:<11} {:>10} {:>12} {:>12}",
"ordering", "range rds", "bytes read", "cost"
);
for row in &r.orderings {
let mark = if row.recommended { " * " } else { " " };
let note = if row.ordering == "morton3" { " (research only)" } else { "" };
let _ = writeln!(
s,
"{}{:<11} {:>10} {:>12} {:>12}{}",
mark,
row.ordering,
row.total_reads,
mib(row.total_bytes),
mib(row.total_cost),
note
);
}
let _ = writeln!(s);
let _ = writeln!(
s,
" cost = bytes read + reads × {} gap (the reader's own request/byte trade)",
mib(r.coalesce_gap_bytes)
);
let _ = writeln!(s, " recommended : {} (measured — lowest cost)", r.recommended);
let _ = writeln!(s, " auto picks : {}", r.auto_choice);
match &r.current {
Some(c) if *c == r.recommended => {
let _ = writeln!(s, " current : {c} (already the measured best)");
}
Some(c) => {
let _ = writeln!(
s,
" current : {c} — rebuild with `--blob-ordering measured` to switch to {}",
r.recommended
);
}
None => {
let _ = writeln!(s, " current : not recorded (pre-2026-07 archive)");
}
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use stt_core::metadata::Metadata;
use stt_core::pack::PackWriter;
use stt_core::tile::TileId;
#[test]
fn audit_ranks_and_recommends_over_a_real_archive() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("audit");
let mut w = PackWriter::create(&out, BlobOrdering::Auto, 1 << 20)
.unwrap()
.with_format_version(stt_core::pack::PACKED_FORMAT_VERSION_V1);
let bucket = 3_600_000i64;
for x in 0..3u32 {
for b in 0..24i64 {
let t = b * bucket;
let id = TileId::new(10, 4_000 + x, 5_000, t as u64);
let payload = format!("t-{x}-{b}").into_bytes();
w.add_tile_full(&id, t, t + bucket - 1, Some(t), 1, None, &payload).unwrap();
}
}
let meta = Metadata::new("audit").with_temporal_bucket_ms(bucket as u64);
w.finalize(&meta).unwrap();
let ts = PackedTileset::open(&out).unwrap();
let r = order_audit(&ts).unwrap();
assert_eq!(r.orderings.len(), 4);
for pair in r.orderings.windows(2) {
assert!(pair[0].total_cost <= pair[1].total_cost);
}
assert_ne!(r.recommended, "morton3");
assert!(r.orderings.iter().filter(|o| o.recommended).count() == 1);
let rec = r.orderings.iter().find(|o| o.recommended).unwrap();
assert_eq!(rec.ordering, r.recommended);
for o in &r.orderings {
if o.ordering != "morton3" {
assert!(o.total_cost >= rec.total_cost);
}
}
let current = r.current.clone().expect("built-with-Auto archive records blobOrdering");
assert!(matches!(current.as_str(), "spatial" | "time-major" | "hilbert3" | "morton3"));
assert!(matches!(r.auto_choice.as_str(), "spatial" | "time-major" | "hilbert3" | "morton3"));
assert!(format_text(&r).contains("recommended :"));
}
}