use plugmem_arena::Slot;
use crate::config::{Config, MAX_SHARDS, MIN_SHARDS, SHARD_TARGET_BYTES};
use crate::index::bm25::DocLenSlot;
use crate::index::postings::IdListSlot;
use crate::model::{
EdgeHistorySlot, EdgeSlot, EntityByName, EntityRecord, FactAux, FactRecord, TemporalSlot,
};
fn shards_for(bytes: u64) -> usize {
let want = bytes
.div_ceil(SHARD_TARGET_BYTES as u64)
.clamp(MIN_SHARDS as u64, MAX_SHARDS as u64);
debug_assert!(want <= MAX_SHARDS as u64);
want.next_power_of_two() as usize
}
fn payload<T: Slot>(count: u64) -> u64 {
count.saturating_mul(T::SIZE as u64)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Population {
pub facts: u64,
pub entities: u64,
pub edges: u64,
pub edge_versions: u64,
pub terms: u64,
pub tags: u64,
pub documents: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShardLayout {
pub facts: usize,
pub entities: usize,
pub edges: usize,
pub temporal: usize,
pub postings: usize,
}
impl Default for ShardLayout {
fn default() -> Self {
Self {
facts: MIN_SHARDS,
entities: MIN_SHARDS,
edges: MIN_SHARDS,
temporal: MIN_SHARDS,
postings: MIN_SHARDS,
}
}
}
impl ShardLayout {
pub const GROWTH_MARGIN: usize = GROW_FACTOR;
pub const SHRINK_MARGIN: usize = SHRINK_FACTOR;
pub(crate) fn for_population(population: &Population) -> Self {
let Population {
facts,
entities,
edges,
edge_versions,
terms,
tags,
documents,
} = *population;
Self {
facts: shards_for(payload::<FactRecord>(facts).max(payload::<FactAux>(facts))),
entities: shards_for(
payload::<EntityRecord>(entities)
.max(payload::<EntityByName>(entities))
.max(payload::<IdListSlot>(entities)),
),
edges: shards_for(
payload::<EdgeSlot>(edges).max(payload::<EdgeHistorySlot>(edge_versions)),
),
temporal: shards_for(payload::<TemporalSlot>(facts)),
postings: shards_for(
payload::<IdListSlot>(terms)
.max(payload::<DocLenSlot>(documents))
.max(payload::<IdListSlot>(tags)),
),
}
}
pub(crate) fn of_config(cfg: &Config) -> Self {
Self {
facts: cfg.shards_facts,
entities: cfg.shards_entities,
edges: cfg.shards_edges,
temporal: cfg.shards_temporal,
postings: cfg.shards_postings,
}
}
pub(crate) fn apply(&self, cfg: &mut Config) {
cfg.shards_facts = self.facts;
cfg.shards_entities = self.entities;
cfg.shards_edges = self.edges;
cfg.shards_temporal = self.temporal;
cfg.shards_postings = self.postings;
}
fn group_earns_rebuild(have: usize, want: usize) -> bool {
want >= have.saturating_mul(GROW_FACTOR) || have >= want.saturating_mul(SHRINK_FACTOR)
}
pub(crate) fn compacted_groups_earn_rebuild(&self, target: &Self) -> bool {
[
(self.facts, target.facts),
(self.entities, target.entities),
(self.temporal, target.temporal),
(self.postings, target.postings),
]
.iter()
.any(|&(have, want)| Self::group_earns_rebuild(have, want))
}
pub(crate) fn edges_earn_rebuild(&self, target: &Self) -> bool {
Self::group_earns_rebuild(self.edges, target.edges)
}
pub(crate) fn realized(&self, target: &Self, edges_rebuilt: bool) -> Self {
Self {
facts: target.facts,
entities: target.entities,
temporal: target.temporal,
postings: target.postings,
edges: if edges_rebuilt {
target.edges
} else {
self.edges
},
}
}
}
const GROW_FACTOR: usize = 8;
const SHRINK_FACTOR: usize = 4;
impl super::Memory<'_> {
pub(crate) fn population(&self) -> Population {
Population {
facts: self.facts.len().saturating_sub(self.tombstones) as u64,
entities: self.entities.len() as u64,
edges: self.edges_out.len() as u64,
edge_versions: self.edges_hist_out.len() as u64,
terms: self.bm25.postings().keys() as u64,
tags: self.tags_idx.keys() as u64,
documents: self.bm25.doc_len_arena().len() as u64,
}
}
pub(crate) fn target_layout(&self) -> ShardLayout {
ShardLayout::for_population(&self.population())
}
pub fn shard_layout_is_stale(&self) -> bool {
let stored = ShardLayout::of_config(&self.cfg);
let target = self.target_layout();
stored.compacted_groups_earn_rebuild(&target) || stored.edges_earn_rebuild(&target)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn population(
facts: u64,
entities: u64,
edges: u64,
edge_versions: u64,
terms: u64,
tags: u64,
) -> Population {
Population {
facts,
entities,
edges,
edge_versions,
terms,
tags,
documents: facts,
}
}
fn layout(p: Population) -> (usize, usize, usize, usize, usize) {
let l = ShardLayout::for_population(&p);
(l.facts, l.entities, l.edges, l.temporal, l.postings)
}
#[test]
fn the_layout_rule_is_a_fixed_table() {
assert_eq!(layout(population(0, 0, 0, 0, 0, 0)), (4, 4, 4, 4, 4));
assert_eq!(layout(population(1, 1, 0, 0, 3, 1)), (4, 4, 4, 4, 4));
assert_eq!(
layout(population(1_000, 50, 20, 25, 1_500, 5)),
(4, 4, 4, 4, 4)
);
assert_eq!(
layout(population(5_000, 200, 100, 120, 6_000, 12)),
(4, 4, 4, 4, 4)
);
assert_eq!(
layout(population(100_000, 4_096, 30_000, 60_000, 34_000, 32)),
(32, 4, 16, 8, 8)
);
assert_eq!(
layout(population(910_051, 4_096, 287_972, 549_482, 34_604, 64)),
(256, 4, 128, 64, 64)
);
assert_eq!(
layout(population(
10_000_000, 40_000, 3_000_000, 6_000_000, 200_000, 128
)),
(2048, 4, 2048, 512, 1024)
);
}
#[test]
fn the_rule_does_not_depend_on_pointer_width() {
let big = population(100_000_000, 1_000, 0, 0, 1_000_000, 256);
assert!(u64::from(u32::MAX) < 100_000_000u64 * 48);
assert_eq!(layout(big).0, 32768);
let absurd = population(u64::MAX, u64::MAX, u64::MAX, u64::MAX, u64::MAX, u64::MAX);
assert_eq!(
layout(absurd),
(MAX_SHARDS, MAX_SHARDS, MAX_SHARDS, MAX_SHARDS, MAX_SHARDS)
);
}
#[test]
fn every_produced_count_is_a_usable_shard_count() {
let mut facts = 0u64;
while facts < 40_000_000 {
let l = ShardLayout::for_population(&population(facts, facts / 8, 0, facts, facts, 16));
for n in [l.facts, l.entities, l.edges, l.temporal, l.postings] {
assert!(n.is_power_of_two(), "{n} is not a power of two");
assert!((MIN_SHARDS..=MAX_SHARDS).contains(&n), "{n} out of range");
}
facts = (facts + 1) * 3 / 2;
}
}
fn at(n: usize) -> ShardLayout {
ShardLayout {
facts: n,
entities: n,
edges: n,
temporal: n,
postings: n,
}
}
#[test]
fn growth_is_eager_and_shrinking_is_lazy() {
assert!(!at(64).compacted_groups_earn_rebuild(&at(64)));
assert!(!at(64).compacted_groups_earn_rebuild(&at(128)));
assert!(!at(64).compacted_groups_earn_rebuild(&at(256)));
assert!(at(64).compacted_groups_earn_rebuild(&at(512)));
assert!(!at(64).compacted_groups_earn_rebuild(&at(32)));
assert!(at(64).compacted_groups_earn_rebuild(&at(16)));
assert!(!at(16).compacted_groups_earn_rebuild(&at(16)));
assert!(!at(128).compacted_groups_earn_rebuild(&at(128)));
let mut skewed = at(64);
skewed.postings = 4;
assert!(at(64).compacted_groups_earn_rebuild(&skewed));
}
#[test]
fn the_edge_arenas_are_judged_on_their_own() {
let mut edges_only = at(64);
edges_only.edges = 1024;
assert!(!at(64).compacted_groups_earn_rebuild(&edges_only));
assert!(at(64).edges_earn_rebuild(&edges_only));
}
#[test]
fn a_pass_claims_only_what_it_laid_out() {
let stored = at(64);
let target = at(512);
let without = stored.realized(&target, false);
assert_eq!(
(
without.facts,
without.entities,
without.temporal,
without.postings
),
(512, 512, 512, 512)
);
assert_eq!(without.edges, stored.edges);
assert_eq!(stored.realized(&target, true), target);
}
#[test]
fn the_clamp_holds_at_both_ends() {
assert_eq!(shards_for(0), MIN_SHARDS);
assert_eq!(shards_for(1), MIN_SHARDS);
assert_eq!(
shards_for(SHARD_TARGET_BYTES as u64 * MIN_SHARDS as u64),
MIN_SHARDS
);
assert_eq!(shards_for(u64::MAX), MAX_SHARDS);
assert_eq!(
shards_for(SHARD_TARGET_BYTES as u64 * MIN_SHARDS as u64 + 1),
MIN_SHARDS * 2
);
}
}