use alloc::vec::Vec;
use crate::throughput::{MemoryAccess, ThroughputKey, ThroughputMode, ThroughputValue};
pub const MIN_WORKING_SET: u64 = 8 * 1024;
pub fn working_set_sweep(cap: u64) -> Vec<u64> {
if cap < MIN_WORKING_SET {
return alloc::vec![cap];
}
let mut sizes = Vec::new();
let mut bytes = MIN_WORKING_SET;
while bytes <= cap {
sizes.push(bytes);
match bytes.checked_mul(2) {
Some(next) => bytes = next,
None => break,
}
}
sizes
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub struct MemoryPoint {
pub bytes: u64,
pub value: ThroughputValue,
}
#[derive(PartialEq, Clone, Debug)]
pub struct MemoryCurve {
access: MemoryAccess,
points: Vec<MemoryPoint>,
}
impl MemoryCurve {
pub fn new(access: MemoryAccess, points: impl IntoIterator<Item = MemoryPoint>) -> Self {
let mut curve = Self {
access,
points: Vec::new(),
};
curve.points = points
.into_iter()
.filter(|point| {
let rate = curve.rate(point);
point.bytes > 0 && rate.is_finite() && rate > 0.0
})
.collect();
curve.points.sort_unstable_by_key(|point| point.bytes);
curve.points.dedup_by_key(|point| point.bytes);
curve
}
pub fn points(&self) -> &[MemoryPoint] {
&self.points
}
pub fn ceiling_at(&self, bytes: u64) -> Option<f64> {
let first = self.points.first()?;
let last = self.points.last()?;
if bytes <= first.bytes {
return Some(self.rate(first));
}
if bytes >= last.bytes {
return Some(self.rate(last));
}
let above = self.points.partition_point(|point| point.bytes <= bytes);
let (low, high) = (&self.points[above - 1], &self.points[above]);
let span = log2(high.bytes) - log2(low.bytes);
let weight = (log2(bytes) - log2(low.bytes)) / span;
Some(self.rate(low) + weight * (self.rate(high) - self.rate(low)))
}
fn rate(&self, point: &MemoryPoint) -> f64 {
point.value.bytes_per_s(&ThroughputKey {
mode: ThroughputMode::MemoryWorkingSet {
access: self.access,
bytes: point.bytes,
},
})
}
}
fn log2(bytes: u64) -> f64 {
let bytes = bytes.max(1);
let exponent = 63 - bytes.leading_zeros();
let mantissa = bytes as f64 / (1u64 << exponent) as f64;
exponent as f64 + (mantissa - 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;
const MB: u64 = 1024 * 1024;
fn curve(points: &[(u64, f64)]) -> MemoryCurve {
MemoryCurve::new(
MemoryAccess::Read,
points.iter().map(|&(bytes, bytes_per_s)| MemoryPoint {
bytes,
value: ThroughputValue {
ops_count: (bytes / 4) as usize,
duration: Duration::from_secs_f64(bytes as f64 / bytes_per_s),
},
}),
)
}
#[test]
fn sweep_covers_powers_of_two_up_to_the_cap() {
let min = MIN_WORKING_SET;
let expected = alloc::vec![min, 2 * min, 4 * min, 8 * min];
assert_eq!(working_set_sweep(8 * min), expected);
assert_eq!(working_set_sweep(12 * min), expected);
assert_eq!(working_set_sweep(min / 4), alloc::vec![min / 4]);
}
#[test]
fn ceiling_interpolates_between_measured_points() {
let curve = curve(&[(MB, 100.0), (4 * MB, 200.0)]);
assert!((curve.ceiling_at(2 * MB).unwrap() - 150.0).abs() < 1e-6);
assert!((curve.ceiling_at(MB).unwrap() - 100.0).abs() < 1e-6);
assert!((curve.ceiling_at(4 * MB).unwrap() - 200.0).abs() < 1e-6);
}
#[test]
fn ceiling_clamps_outside_the_sweep() {
let curve = curve(&[(MB, 100.0), (4 * MB, 200.0)]);
assert!((curve.ceiling_at(1).unwrap() - 100.0).abs() < 1e-6);
assert!((curve.ceiling_at(u64::MAX).unwrap() - 200.0).abs() < 1e-6);
}
#[test]
fn unusable_points_are_dropped() {
let curve = MemoryCurve::new(
MemoryAccess::Read,
[MemoryPoint {
bytes: MB,
value: ThroughputValue::ZERO,
}],
);
assert!(curve.points().is_empty());
assert_eq!(curve.ceiling_at(MB), None);
}
#[test]
fn log2_is_exact_on_powers_of_two_and_monotonic_between() {
assert_eq!(log2(1), 0.0);
assert_eq!(log2(MB), 20.0);
assert_eq!(log2(1 << 63), 63.0);
assert_eq!(log2(0), 0.0);
assert!(log2(3 * MB) > log2(2 * MB));
assert!(log2(3 * MB) < log2(4 * MB));
}
}