use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StorageProfile {
pub seek_latency_s: f64,
pub sequential_bandwidth_bps: f64,
pub block_bytes: u64,
}
impl StorageProfile {
#[inline]
pub const fn hdd_7200rpm() -> Self {
Self {
seek_latency_s: 0.009,
sequential_bandwidth_bps: 160.0 * 1_000_000.0,
block_bytes: 4096,
}
}
#[inline]
pub const fn ssd_nvme() -> Self {
Self {
seek_latency_s: 0.000_1,
sequential_bandwidth_bps: 3_500.0 * 1_000_000.0,
block_bytes: 4096,
}
}
#[inline]
pub const fn memory() -> Self {
Self {
seek_latency_s: 0.0,
sequential_bandwidth_bps: 20_000.0 * 1_000_000.0,
block_bytes: 64,
}
}
#[inline]
pub fn transfer_s(self, bytes: u64) -> f64 {
bytes as f64 / self.sequential_bandwidth_bps
}
#[inline]
pub fn read_s(self, bytes: u64) -> f64 {
self.seek_latency_s + self.transfer_s(bytes.max(self.block_bytes))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DistanceCost {
pub approx_s: f64,
pub exact_s: f64,
}
impl DistanceCost {
#[inline]
pub const fn new(approx_s: f64, exact_s: f64) -> Self {
Self { approx_s, exact_s }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct QueryCounters {
pub nodes_visited: u64,
pub approx_distances: u64,
pub exact_distances: u64,
pub seeks: u64,
pub bytes_read: u64,
pub sequential_runs: u64,
}
impl QueryCounters {
#[inline]
pub const fn new() -> Self {
Self {
nodes_visited: 0,
approx_distances: 0,
exact_distances: 0,
seeks: 0,
bytes_read: 0,
sequential_runs: 0,
}
}
#[inline]
pub fn visit_node(&mut self) {
self.nodes_visited += 1;
}
#[inline]
pub fn add_approx(&mut self, n: u64) {
self.approx_distances += n;
}
#[inline]
pub fn add_exact(&mut self, n: u64) {
self.exact_distances += n;
}
#[inline]
pub fn add_read(&mut self, bytes: u64, seeks: u64, runs: u64) {
self.bytes_read += bytes;
self.seeks += seeks;
self.sequential_runs += runs;
}
#[inline]
pub fn merge(&mut self, other: &QueryCounters) {
self.nodes_visited += other.nodes_visited;
self.approx_distances += other.approx_distances;
self.exact_distances += other.exact_distances;
self.seeks += other.seeks;
self.bytes_read += other.bytes_read;
self.sequential_runs += other.sequential_runs;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct QueryCost {
pub traversal_s: f64,
pub storage_access_s: f64,
pub distance_s: f64,
}
impl QueryCost {
pub const HOP_OVERHEAD_S: f64 = 50e-9;
#[inline]
pub fn estimate(
counters: &QueryCounters,
profile: StorageProfile,
cost: DistanceCost,
) -> Self {
let traversal_s = counters.nodes_visited as f64 * Self::HOP_OVERHEAD_S;
let storage_access_s = counters.seeks as f64 * profile.seek_latency_s
+ profile.transfer_s(counters.bytes_read);
let distance_s = counters.approx_distances as f64 * cost.approx_s
+ counters.exact_distances as f64 * cost.exact_s;
Self {
traversal_s,
storage_access_s,
distance_s,
}
}
#[inline]
pub fn total_s(self) -> f64 {
self.traversal_s + self.storage_access_s + self.distance_s
}
#[inline]
pub fn storage_fraction(self) -> f64 {
let total = self.total_s();
if total == 0.0 {
0.0
} else {
self.storage_access_s / total
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hdd_seek_dominates_single_vector_transfer() {
let hdd = StorageProfile::hdd_7200rpm();
let transfer = hdd.transfer_s(3 * 1024);
assert!(
hdd.seek_latency_s > transfer * 100.0,
"seek {} should dwarf transfer {}",
hdd.seek_latency_s,
transfer
);
}
#[test]
fn transfer_is_honest_and_read_floors_to_a_block() {
let hdd = StorageProfile::hdd_7200rpm();
assert_eq!(hdd.transfer_s(0), 0.0);
assert!(hdd.transfer_s(2 * 1024) > hdd.transfer_s(1024));
assert_eq!(hdd.read_s(1), hdd.read_s(hdd.block_bytes));
let big = hdd.block_bytes * 10;
assert!(hdd.read_s(big) > hdd.read_s(hdd.block_bytes));
}
#[test]
fn read_is_seek_plus_floored_transfer() {
let ssd = StorageProfile::ssd_nvme();
let bytes = 8192; assert_eq!(
ssd.read_s(bytes),
ssd.seek_latency_s + ssd.transfer_s(bytes)
);
}
#[test]
fn memory_profile_has_negligible_seek() {
let mem = StorageProfile::memory();
assert_eq!(mem.seek_latency_s, 0.0);
assert_eq!(mem.read_s(64), mem.transfer_s(64));
}
#[test]
fn counters_accumulate_and_merge() {
let mut a = QueryCounters::new();
a.visit_node();
a.add_approx(10);
a.add_exact(2);
a.add_read(6144, 2, 1);
assert_eq!(a.nodes_visited, 1);
assert_eq!(a.approx_distances, 10);
assert_eq!(a.exact_distances, 2);
assert_eq!(a.bytes_read, 6144);
assert_eq!(a.seeks, 2);
assert_eq!(a.sequential_runs, 1);
let mut b = QueryCounters::new();
b.add_read(1024, 1, 1);
b.merge(&a);
assert_eq!(b.bytes_read, 6144 + 1024);
assert_eq!(b.seeks, 3);
assert_eq!(b.nodes_visited, 1);
}
#[test]
fn estimate_decomposes_three_terms() {
let mut c = QueryCounters::new();
c.nodes_visited = 100;
c.approx_distances = 1000;
c.exact_distances = 50;
c.seeks = 50;
c.bytes_read = 50 * 3072;
let cost = DistanceCost::new(5e-9, 200e-9);
let qc = QueryCost::estimate(&c, StorageProfile::hdd_7200rpm(), cost);
assert_eq!(qc.traversal_s, 100.0 * QueryCost::HOP_OVERHEAD_S);
assert_eq!(qc.distance_s, 1000.0 * 5e-9 + 50.0 * 200e-9);
assert!(qc.storage_access_s > 0.4);
assert!((qc.total_s()
- (qc.traversal_s + qc.storage_access_s + qc.distance_s))
.abs()
< 1e-12);
}
#[test]
fn coalescing_seeks_lowers_cost_at_equal_bytes() {
let bytes = 64 * 3072;
let cost = DistanceCost::new(5e-9, 200e-9);
let hdd = StorageProfile::hdd_7200rpm();
let mut scattered = QueryCounters::new();
scattered.add_read(bytes, 64, 64);
let mut coalesced = QueryCounters::new();
coalesced.add_read(bytes, 1, 1);
let scattered_cost = QueryCost::estimate(&scattered, hdd, cost);
let coalesced_cost = QueryCost::estimate(&coalesced, hdd, cost);
assert!(scattered_cost.storage_access_s > coalesced_cost.storage_access_s);
assert!(scattered_cost.storage_fraction() > 0.9);
}
#[test]
fn memory_profile_collapses_the_seek_term() {
let mut c = QueryCounters::new();
c.nodes_visited = 100;
c.exact_distances = 100;
c.seeks = 100;
c.bytes_read = 100 * 3072;
let cost = DistanceCost::new(5e-9, 200e-9);
let hdd = QueryCost::estimate(&c, StorageProfile::hdd_7200rpm(), cost);
let mem = QueryCost::estimate(&c, StorageProfile::memory(), cost);
assert!(hdd.storage_fraction() > 0.9);
assert!(mem.storage_fraction() < 0.5);
assert!(mem.storage_access_s < hdd.storage_access_s / 100.0);
}
#[test]
fn zero_cost_has_zero_storage_fraction() {
let qc = QueryCost::estimate(
&QueryCounters::new(),
StorageProfile::hdd_7200rpm(),
DistanceCost::new(5e-9, 200e-9),
);
assert_eq!(qc.total_s(), 0.0);
assert_eq!(qc.storage_fraction(), 0.0);
}
}