use super::{SegmentTier, TierThresholds};
use crate::graph::refine::{RefinementPass, RefinementPasses};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SegmentStats {
pub tier: SegmentTier,
pub row_count: u32,
pub dimensions: u32,
pub scheme: u16,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct StoreStats {
pub graph_segment_count: u32,
pub total_graph_rows: u64,
pub largest_graph_segment_rows: u64,
pub scan_segment_count: u32,
pub total_scan_rows: u64,
pub scan_graph_min_rows: u32,
}
pub const CONSOLIDATE_MIN_SEGMENTS: u32 = 3;
pub const CONSOLIDATE_DOMINANT_TENTHS: u128 = 7;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorePlan {
Stay,
Consolidate,
ConsolidateScan,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RefinementPlan {
Stay,
Apply(RefinementPass),
}
#[must_use]
pub fn decide_refinement(applied: RefinementPasses) -> RefinementPlan {
RefinementPass::ALL
.into_iter()
.find(|pass| !applied.contains(*pass))
.map_or(RefinementPlan::Stay, RefinementPlan::Apply)
}
#[must_use]
pub const fn decide_store(store: StoreStats) -> StorePlan {
if store.graph_segment_count >= CONSOLIDATE_MIN_SEGMENTS {
return StorePlan::Consolidate;
}
if store.graph_segment_count == 2 {
let largest = store.largest_graph_segment_rows as u128;
let total = store.total_graph_rows as u128;
if largest * 10 < total * CONSOLIDATE_DOMINANT_TENTHS {
return StorePlan::Consolidate;
}
}
if store.scan_segment_count >= 2
&& store.scan_graph_min_rows != 0
&& store.total_scan_rows >= store.scan_graph_min_rows as u64
{
return StorePlan::ConsolidateScan;
}
StorePlan::Stay
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TierPlan {
Stay(SegmentTier),
Transition {
from: SegmentTier,
to: SegmentTier,
},
}
#[must_use]
pub const fn decide(segment: SegmentStats, _store: StoreStats) -> TierPlan {
let thresholds = super::thresholds::for_bucket(segment.dimensions, segment.scheme);
decide_with_thresholds(segment, thresholds)
}
pub(crate) const fn decide_with_thresholds(
segment: SegmentStats,
thresholds: TierThresholds,
) -> TierPlan {
match segment.tier {
SegmentTier::ActiveScan => TierPlan::Stay(SegmentTier::ActiveScan),
SegmentTier::SealedGraph => TierPlan::Stay(SegmentTier::SealedGraph),
SegmentTier::SealedScan => {
if segment.row_count >= thresholds.graph_min_rows {
TierPlan::Transition {
from: SegmentTier::SealedScan,
to: SegmentTier::SealedGraph,
}
} else {
TierPlan::Stay(SegmentTier::SealedScan)
}
}
}
}
#[cfg(test)]
mod tests {
use super::{SegmentStats, StorePlan, StoreStats, TierPlan, decide, decide_store};
use crate::tier::{PROVISIONAL_TIER_THRESHOLDS, SegmentTier};
#[test]
fn policy_decides_graph_above_the_threshold_and_scan_below() {
let threshold = PROVISIONAL_TIER_THRESHOLDS.graph_min_rows;
let below = SegmentStats {
tier: SegmentTier::SealedScan,
row_count: threshold - 1,
dimensions: 128,
scheme: 4,
};
let above = SegmentStats {
row_count: threshold,
..below
};
assert_eq!(
decide(below, StoreStats::default()),
TierPlan::Stay(SegmentTier::SealedScan)
);
assert_eq!(
decide(above, StoreStats::default()),
TierPlan::Transition {
from: SegmentTier::SealedScan,
to: SegmentTier::SealedGraph,
}
);
}
#[test]
fn policy_never_demotes_a_segment_that_already_has_a_graph() {
let segment = SegmentStats {
tier: SegmentTier::SealedGraph,
row_count: PROVISIONAL_TIER_THRESHOLDS.graph_min_rows - 1,
dimensions: 128,
scheme: 4,
};
assert_eq!(
decide(segment, StoreStats::default()),
TierPlan::Stay(SegmentTier::SealedGraph)
);
}
#[test]
fn store_policy_consolidates_at_three_or_more_graph_segments() {
for (count, expected) in [
(0, StorePlan::Stay),
(1, StorePlan::Stay),
(3, StorePlan::Consolidate),
(4, StorePlan::Consolidate),
(100, StorePlan::Consolidate),
] {
assert_eq!(
decide_store(StoreStats {
graph_segment_count: count,
total_graph_rows: 1_000_000,
largest_graph_segment_rows: 999_999,
..StoreStats::default()
}),
expected,
"graph_segment_count={count}"
);
}
}
#[test]
fn store_policy_at_two_segments_consolidates_only_without_a_dominant_segment() {
assert_eq!(
decide_store(StoreStats {
graph_segment_count: 2,
total_graph_rows: 1_000,
largest_graph_segment_rows: 500,
..StoreStats::default()
}),
StorePlan::Consolidate
);
assert_eq!(
decide_store(StoreStats {
graph_segment_count: 2,
total_graph_rows: 1_000,
largest_graph_segment_rows: 699,
..StoreStats::default()
}),
StorePlan::Consolidate
);
assert_eq!(
decide_store(StoreStats {
graph_segment_count: 2,
total_graph_rows: 1_000,
largest_graph_segment_rows: 700,
..StoreStats::default()
}),
StorePlan::Stay
);
assert_eq!(
decide_store(StoreStats {
graph_segment_count: 2,
total_graph_rows: 1_000,
largest_graph_segment_rows: 950,
..StoreStats::default()
}),
StorePlan::Stay
);
assert_eq!(
decide_store(StoreStats {
graph_segment_count: 2,
total_graph_rows: u64::MAX,
largest_graph_segment_rows: u64::MAX / 2,
..StoreStats::default()
}),
StorePlan::Consolidate
);
}
#[test]
fn store_policy_consolidates_scan_segments_only_after_graph_threshold() {
assert_eq!(
decide_store(StoreStats {
scan_segment_count: 7,
total_scan_rows: 28_672,
scan_graph_min_rows: 30_000,
..StoreStats::default()
}),
StorePlan::Stay
);
assert_eq!(
decide_store(StoreStats {
scan_segment_count: 8,
total_scan_rows: 32_768,
scan_graph_min_rows: 30_000,
..StoreStats::default()
}),
StorePlan::ConsolidateScan
);
}
}