use std::collections::{BTreeMap, HashSet};
use crate::curve::{self, BlobOrdering};
pub const DEFAULT_COALESCE_GAP_BYTES: u64 = 2 * 1024 * 1024;
pub const CANDIDATES: [BlobOrdering; 4] = [
BlobOrdering::SpatialMajor,
BlobOrdering::Hilbert3,
BlobOrdering::TimeMajor,
BlobOrdering::Morton3,
];
#[derive(Debug, Clone, Copy)]
pub struct TileSample {
pub z: u8,
pub x: u32,
pub y: u32,
pub hilbert: u64,
pub time_start: i64,
pub tb: i64,
pub len: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct SimOptions {
pub coalesce_gap_bytes: u64,
}
impl Default for SimOptions {
fn default() -> Self {
Self { coalesce_gap_bytes: DEFAULT_COALESCE_GAP_BYTES }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueryCost {
pub reads: u64,
pub bytes_read: u64,
pub bytes_needed: u64,
}
impl QueryCost {
const ZERO: QueryCost = QueryCost { reads: 0, bytes_read: 0, bytes_needed: 0 };
}
#[derive(Debug, Clone, Copy)]
pub struct OrderingCost {
pub ordering: BlobOrdering,
pub scrub: QueryCost,
pub pan: QueryCost,
pub total_reads: u64,
pub total_bytes_read: u64,
pub cost: u64,
}
pub const SELECTABLE: [BlobOrdering; 3] = [
BlobOrdering::SpatialMajor,
BlobOrdering::Hilbert3,
BlobOrdering::TimeMajor,
];
pub fn evaluate(samples: &[TileSample], opts: SimOptions) -> Vec<OrderingCost> {
let gap = opts.coalesce_gap_bytes;
if samples.is_empty() {
return CANDIDATES
.iter()
.map(|&ordering| OrderingCost {
ordering,
scrub: QueryCost::ZERO,
pan: QueryCost::ZERO,
total_reads: 0,
total_bytes_read: 0,
cost: 0,
})
.collect();
}
let (tb_min, tb_max) = samples
.iter()
.fold((i64::MAX, i64::MIN), |(lo, hi), s| (lo.min(s.tb), hi.max(s.tb)));
let tb_span = tb_max - tb_min;
let scrub = scrub_hits(samples);
let pan = pan_hits(samples);
let mut costs: Vec<OrderingCost> = CANDIDATES
.iter()
.map(|&ordering| {
let order = linearize(samples, ordering, tb_min, tb_span);
let sc = simulate_query(&order, samples, &scrub, gap);
let pn = simulate_query(&order, samples, &pan, gap);
let total_reads = sc.reads + pn.reads;
let total_bytes_read = sc.bytes_read + pn.bytes_read;
OrderingCost {
ordering,
scrub: sc,
pan: pn,
total_reads,
total_bytes_read,
cost: total_bytes_read.saturating_add(total_reads.saturating_mul(gap)),
}
})
.collect();
costs.sort_by(|a, b| a.cost.cmp(&b.cost));
costs
}
pub fn measured_ordering(samples: &[TileSample], opts: SimOptions) -> BlobOrdering {
evaluate(samples, opts)
.into_iter()
.map(|c| c.ordering)
.find(|o| *o != BlobOrdering::Morton3)
.unwrap_or(BlobOrdering::SpatialMajor)
}
fn linearize(samples: &[TileSample], ordering: BlobOrdering, tb_min: i64, tb_span: i64) -> Vec<u32> {
let mut idx: Vec<u32> = (0..samples.len() as u32).collect();
idx.sort_by_key(|&i| {
let s = &samples[i as usize];
(
curve::space_time_key(
ordering, s.z, s.x, s.y, s.hilbert, s.time_start, s.tb, tb_min, tb_span,
),
s.z,
s.x,
s.y,
s.time_start,
)
});
idx
}
fn simulate_query(order: &[u32], samples: &[TileSample], hit: &[bool], gap_bytes: u64) -> QueryCost {
let mut reads = 0u64;
let mut bytes_read = 0u64;
let mut bytes_needed = 0u64;
let mut in_run = false;
let mut run_bytes = 0u64; let mut gap_since_hit = 0u64;
for &i in order {
let s = &samples[i as usize];
let h = hit[i as usize];
if h {
bytes_needed += s.len;
}
if !in_run {
if h {
in_run = true;
run_bytes = s.len;
gap_since_hit = 0;
}
} else if h {
run_bytes += gap_since_hit + s.len;
gap_since_hit = 0;
} else {
gap_since_hit += s.len;
if gap_since_hit > gap_bytes {
reads += 1;
bytes_read += run_bytes;
in_run = false;
run_bytes = 0;
gap_since_hit = 0;
}
}
}
if in_run {
reads += 1;
bytes_read += run_bytes;
}
QueryCost { reads, bytes_read, bytes_needed }
}
fn scrub_hits(samples: &[TileSample]) -> Vec<bool> {
let mut hs: Vec<u64> = samples.iter().map(|s| s.hilbert).collect();
hs.sort_unstable();
hs.dedup();
let m = hs.len();
if m == 0 {
return vec![false; samples.len()];
}
let lo = m * 3 / 8;
let hi = (m * 5 / 8).max(lo + 1);
let band: HashSet<u64> = hs[lo..hi].iter().copied().collect();
samples.iter().map(|s| band.contains(&s.hilbert)).collect()
}
fn pan_hits(samples: &[TileSample]) -> Vec<bool> {
let mut by_tb: BTreeMap<i64, u64> = BTreeMap::new();
for s in samples {
*by_tb.entry(s.tb).or_insert(0) += s.len;
}
let mut best_tb = 0i64;
let mut best_w = 0u64;
let mut found = false;
for (&tb, &w) in by_tb.iter() {
if !found || w > best_w {
best_w = w;
best_tb = tb;
found = true;
}
}
samples.iter().map(|s| s.tb == best_tb).collect()
}
#[cfg(test)]
mod tests {
use super::*;
const OPTS: SimOptions = SimOptions { coalesce_gap_bytes: 1500 };
const LEN: u64 = 1000;
fn s(x: u32, y: u32, hilbert: u64, tb: i64, len: u64) -> TileSample {
TileSample { z: 8, x, y, hilbert, time_start: tb, tb, len }
}
#[test]
fn deep_time_playback_prefers_spatial() {
let mut samples = Vec::new();
for t in 0..24i64 {
samples.push(s(0, 0, 0, t, LEN));
samples.push(s(1, 0, 1, t, LEN));
}
assert_eq!(measured_ordering(&samples, OPTS), BlobOrdering::SpatialMajor);
}
#[test]
fn single_bucket_resolves_to_spatial() {
let samples: Vec<TileSample> = (0..16u32).map(|i| s(i, 0, i as u64, 0, LEN)).collect();
assert_eq!(measured_ordering(&samples, OPTS), BlobOrdering::SpatialMajor);
}
#[test]
fn morton3_is_reported_but_never_selected() {
for spread in [1u32, 8, 32] {
let mut samples = Vec::new();
for t in 0..8i64 {
for x in 0..spread {
samples.push(s(x, x % 3, (x as u64) * 7 + 1, t, LEN + x as u64));
}
}
let ranked = evaluate(&samples, OPTS);
assert!(ranked.iter().any(|c| c.ordering == BlobOrdering::Morton3), "morton3 reported");
let expected = ranked
.iter()
.map(|c| c.ordering)
.find(|o| *o != BlobOrdering::Morton3)
.unwrap();
assert_eq!(measured_ordering(&samples, OPTS), expected);
assert_ne!(measured_ordering(&samples, OPTS), BlobOrdering::Morton3);
}
}
#[test]
fn cost_is_bytes_plus_reads_times_gap() {
let samples: Vec<TileSample> = (0..12u32).map(|i| s(i, 0, i as u64, (i % 4) as i64, LEN)).collect();
for c in evaluate(&samples, OPTS) {
assert_eq!(c.cost, c.total_bytes_read + c.total_reads * OPTS.coalesce_gap_bytes);
}
}
#[test]
fn evaluate_is_deterministic() {
let mut samples = Vec::new();
for t in 0..10i64 {
for x in 0..5u32 {
samples.push(s(x, x % 2, (x as u64) * 3 + 1, t, 70 + t as u64));
}
}
let a = evaluate(&samples, SimOptions::default());
let b = evaluate(&samples, SimOptions::default());
let key = |v: &[OrderingCost]| {
v.iter().map(|c| (c.ordering.as_str(), c.cost)).collect::<Vec<_>>()
};
assert_eq!(key(&a), key(&b));
}
#[test]
fn empty_input_is_safe_and_spatial() {
assert_eq!(measured_ordering(&[], SimOptions::default()), BlobOrdering::SpatialMajor);
assert_eq!(evaluate(&[], SimOptions::default()).len(), 4);
}
#[test]
fn linearize_matches_spatial_major_hand_sort() {
let samples = vec![s(1, 0, 5, 2, 10), s(0, 0, 1, 1, 10), s(1, 0, 5, 0, 10)];
let order = linearize(&samples, BlobOrdering::SpatialMajor, 0, 2);
assert_eq!(order, vec![1, 2, 0]);
}
}