use rudb_common::{Error, Result};
use crate::rid::{NO_PARENT, Rid};
const LAYOUT: u8 = 1;
pub const BUCKETS: usize = 32;
pub const BYTES: usize = 8 + 32 + 16 + BUCKETS * 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Degrees {
buckets: [u64; BUCKETS],
children: u64,
linked: u64,
highest: u64,
strides: u64,
stride: u128,
unique: bool,
}
impl Degrees {
#[must_use]
pub fn of(parents_of: &[Rid], parents: u64, unique: bool) -> Self {
let mut counts = vec![0_u32; usize::try_from(parents).unwrap_or(0)];
let mut linked = 0_u64;
let mut strides = 0_u64;
let mut stride = 0_u128;
let mut previous: Option<Rid> = None;
for parent in parents_of {
if *parent == NO_PARENT {
previous = None;
continue;
}
if let Some(held) = counts.get_mut(usize::try_from(*parent).unwrap_or(usize::MAX)) {
*held = held.saturating_add(1);
}
if let Some(before) = previous {
stride += u128::from(before.abs_diff(*parent));
strides += 1;
}
previous = Some(*parent);
linked += 1;
}
let mut buckets = [0_u64; BUCKETS];
let mut highest = 0_u64;
for count in &counts {
buckets[bucket(u64::from(*count))] += 1;
highest = highest.max(u64::from(*count));
}
Self {
buckets,
children: parents_of.len() as u64,
linked,
highest,
strides,
stride,
unique,
}
}
#[must_use]
pub fn children(&self) -> u64 {
self.children
}
#[must_use]
pub fn linked(&self) -> u64 {
self.linked
}
#[must_use]
pub fn parents(&self) -> u64 {
self.buckets.iter().sum()
}
#[must_use]
pub fn buckets(&self) -> &[u64; BUCKETS] {
&self.buckets
}
#[must_use]
pub fn mean(&self) -> f64 {
let parents = self.parents();
if parents == 0 {
return 0.0;
}
#[allow(clippy::cast_precision_loss)]
{
self.linked as f64 / parents as f64
}
}
#[must_use]
pub fn highest(&self) -> u64 {
self.highest
}
#[must_use]
pub fn percentile(&self, share: f64) -> u64 {
let parents = self.parents();
if parents == 0 {
return 0;
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
let want = (parents as f64 * share).ceil().max(1.0) as u64;
let mut seen = 0_u64;
for (at, count) in self.buckets.iter().enumerate() {
seen += count;
if seen >= want {
return bound(at).min(self.highest);
}
}
self.highest
}
#[must_use]
pub fn total(&self) -> bool {
self.linked == self.children
}
#[must_use]
pub fn unique(&self) -> bool {
self.unique
}
#[must_use]
pub fn locality(&self) -> Option<f64> {
if self.strides == 0 {
return None;
}
#[allow(clippy::cast_precision_loss)]
Some(self.stride as f64 / self.strides as f64)
}
pub fn write(&self, out: &mut Vec<u8>) {
let start = out.len();
out.push(LAYOUT);
out.push(u8::from(self.unique));
out.extend_from_slice(&[0; 6]);
for number in [self.children, self.linked, self.highest, self.strides] {
out.extend_from_slice(&number.to_le_bytes());
}
out.extend_from_slice(&self.stride.to_le_bytes());
for count in &self.buckets {
out.extend_from_slice(&count.to_le_bytes());
}
debug_assert_eq!(out.len() - start, BYTES, "a degree payload is a fixed {BYTES} bytes");
}
pub fn read(bytes: &[u8]) -> Result<Self> {
if bytes.len() != BYTES {
return Err(malformed("a degree payload is not the size its layout gives it"));
}
if bytes[0] != LAYOUT {
return Err(malformed(format!(
"degree layout {} is not one this build knows",
bytes[0]
)));
}
let at = |from: usize| -> u64 {
u64::from_le_bytes(bytes[from..from + 8].try_into().expect("eight bytes"))
};
let mut buckets = [0_u64; BUCKETS];
for (bucket, held) in buckets.iter_mut().enumerate() {
*held = at(56 + bucket * 8);
}
Ok(Self {
buckets,
children: at(8),
linked: at(16),
highest: at(24),
strides: at(32),
stride: u128::from_le_bytes(bytes[40..56].try_into().expect("sixteen bytes")),
unique: bytes[1] != 0,
})
}
}
fn bucket(degree: u64) -> usize {
if degree == 0 {
return 0;
}
(64 - degree.leading_zeros() as usize).min(BUCKETS - 1)
}
fn bound(at: usize) -> u64 {
match at {
0 => 0,
at if at >= BUCKETS - 1 => u64::MAX,
at => (1 << at) - 1,
}
}
fn malformed(message: impl Into<String>) -> Error {
Error::invalid_input(format!("invalid rudb graph statistics: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
fn of(parents_of: &[Rid], parents: u64) -> Degrees {
Degrees::of(parents_of, parents, true)
}
#[test]
fn a_uniform_fan_out_has_a_mean_a_maximum_and_a_percentile_that_agree() {
let parents_of = (0..4000_u64).map(|child| child / 4).collect::<Vec<_>>();
let held = of(&parents_of, 1000);
assert_eq!(held.parents(), 1000, "every parent is in a bucket");
assert_eq!(held.linked(), 4000);
assert!((held.mean() - 4.0).abs() < 1e-9, "four children each");
assert_eq!(held.highest(), 4, "and no parent has a fifth");
assert_eq!(held.percentile(0.99), 4, "so the tail is the mean");
assert!(held.total(), "every child found a parent");
assert!(held.unique());
}
#[test]
fn a_skewed_fan_out_has_the_same_mean_and_a_percentile_that_says_otherwise() {
let mut parents_of = vec![0_u64; 3001];
parents_of.extend(1..1000_u64);
let held = of(&parents_of, 1000);
assert_eq!(held.parents(), 1000);
assert!((held.mean() - 4.0).abs() < 1e-9, "the same mean as the uniform case");
assert_eq!(held.highest(), 3001, "and one parent holds nearly all of it");
assert_eq!(held.percentile(0.5), 1, "half the parents have one child");
assert!(held.percentile(1.0) >= 3001, "and the last percentile reaches the tail");
}
#[test]
fn a_childless_parent_is_bucket_zero_and_not_a_missing_row() {
let held = of(&[0, 0, 2], 4);
assert_eq!(held.parents(), 4, "all four are counted");
assert_eq!(held.buckets()[0], 2, "two of them have no children");
assert_eq!(held.buckets()[1], 1, "one has a single child");
assert_eq!(held.buckets()[2], 1, "and one has two");
assert_eq!(held.percentile(0.5), 0, "half the parents are childless");
}
#[test]
fn an_unmatched_child_costs_the_totality_certificate_and_not_the_uniqueness_one() {
let held = of(&[0, NO_PARENT, 1], 2);
assert_eq!(held.children(), 3, "three children");
assert_eq!(held.linked(), 2, "two of which found a parent");
assert!(!held.total(), "so the relationship is not total");
assert!(held.unique(), "which says nothing about the parent's key");
assert!(!Degrees::of(&[0, 1], 2, false).unique(), "and that is the caller's fact");
}
#[test]
fn a_clustered_child_gathers_near_and_a_scattered_one_gathers_far() {
let near = of(&(0..8_u64).map(|child| child / 2).collect::<Vec<_>>(), 4);
assert!((near.locality().expect("seven steps") - 3.0 / 7.0).abs() < 1e-9);
let far = of(&[0, 3, 0, 3, 0, 3, 0, 3], 4);
assert!((far.locality().expect("seven steps") - 3.0).abs() < 1e-9);
assert!(far.locality() > near.locality(), "which is the number section 6.4 wanted");
}
#[test]
fn an_unmatched_child_breaks_the_stride_rather_than_bridging_it() {
let held = of(&[0, NO_PARENT, 9], 10);
assert_eq!(held.locality(), None, "no two adjacent children are both linked");
}
#[test]
fn a_relationship_with_no_children_has_no_locality_rather_than_perfect_locality() {
let held = of(&[], 4);
assert_eq!(held.locality(), None);
assert_eq!(held.mean(), 0.0);
assert!(held.total(), "no child went unmatched, because there were none");
}
#[test]
fn what_is_written_is_what_is_read() {
let parents_of = (0..4000_u64).map(|child| (child * 7) % 1000).collect::<Vec<_>>();
let mut held = Degrees::of(&parents_of, 1000, false);
held.children += 1;
let mut out = Vec::new();
held.write(&mut out);
assert_eq!(out.len(), BYTES, "the payload is a fixed size");
assert_eq!(Degrees::read(&out).expect("reads back"), held);
}
#[test]
fn a_payload_of_the_wrong_size_or_the_wrong_layout_is_refused() {
let held = of(&[0, 1], 2);
let mut out = Vec::new();
held.write(&mut out);
assert!(Degrees::read(&out[..BYTES - 1]).is_err(), "short");
out.push(0);
assert!(Degrees::read(&out).is_err(), "long");
out.pop();
out[0] = LAYOUT + 1;
assert!(Degrees::read(&out).is_err(), "from a build that came after this one");
}
#[test]
fn the_buckets_double_and_the_last_one_holds_everything_past_it() {
assert_eq!(bucket(0), 0);
assert_eq!(bucket(1), 1);
assert_eq!(bucket(2), 2);
assert_eq!(bucket(3), 2);
assert_eq!(bucket(4), 3);
assert_eq!(bucket(7), 3);
assert_eq!(bucket(8), 4);
assert_eq!(bucket(u64::MAX), BUCKETS - 1, "and nothing falls off the end");
assert_eq!(bound(0), 0);
assert_eq!(bound(1), 1);
assert_eq!(bound(2), 3);
assert_eq!(bound(3), 7);
assert_eq!(bound(BUCKETS - 1), u64::MAX);
}
}