#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::sync::atomic::Ordering::Relaxed;
use portable_atomic::AtomicU64;
#[derive(Debug, Default)]
pub struct ProbeStats {
probes: AtomicU64,
negative: AtomicU64,
}
impl ProbeStats {
pub fn probe(&self) {
self.probes.fetch_add(1, Relaxed);
}
pub fn negative(&self) {
self.negative.fetch_add(1, Relaxed);
}
pub fn probes(&self) -> u64 {
self.probes.load(Relaxed)
}
pub fn negatives(&self) -> u64 {
self.negative.load(Relaxed)
}
pub fn add(&self, counts: ProbeCounts) {
self.probes.fetch_add(counts.probes, Relaxed);
self.negative.fetch_add(counts.negatives, Relaxed);
}
pub fn decay(&self) {
for counter in [&self.probes, &self.negative] {
let mut current = counter.load(Relaxed);
while let Err(actual) =
counter.compare_exchange_weak(current, current / 2, Relaxed, Relaxed)
{
current = actual;
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ProbeCounts {
pub probes: u64,
pub negatives: u64,
}
impl core::ops::AddAssign for ProbeCounts {
fn add_assign(&mut self, other: Self) {
self.probes += other.probes;
self.negatives += other.negatives;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PlanCounts {
pub probes: ProbeCounts,
pub filter_queries: usize,
pub filter_skips: usize,
pub point_reads: usize,
}
pub fn inherited_counts(
first: &[u8],
last: &[u8],
inputs: &[crate::table::Table],
) -> crate::Result<ProbeCounts> {
use core::cmp::Ordering::{Equal, Greater, Less};
use core::ops::Bound::{Excluded, Included};
let Some(comparator) = inputs.first().map(|input| input.comparator.clone()) else {
return Ok(ProbeCounts::default());
};
let cmp = |a: &[u8], b: &[u8]| comparator.compare(a, b);
let mut starts: Vec<(&[u8], bool)> = Vec::new();
for input in inputs {
let range = &input.metadata.key_range;
let (min, max) = (range.min().as_ref(), range.max().as_ref());
if cmp(min, first) == Greater && cmp(min, last) != Greater {
starts.push((min, false));
}
if cmp(max, first) != Less && cmp(max, last) == Less {
starts.push((max, true));
}
}
starts.sort_by(|a, b| cmp(a.0, b.0).then(a.1.cmp(&b.1)));
starts.dedup_by(|a, b| cmp(a.0, b.0) == Equal && a.1 == b.1);
let mut by_age: Vec<&crate::table::Table> = inputs.iter().collect();
by_age.sort_by_key(|input| input.get_highest_seqno());
let mut inherited = ProbeCounts::default();
let mut lower = Included(first);
for index in 0..=starts.len() {
let next = starts.get(index).copied();
let upper = match next {
Some((key, true)) => Included(key),
Some((key, false)) => Excluded(key),
None => Included(last),
};
for input in &by_age {
if !input.has_filter()
|| !input.check_key_range_overlap_cmp(&(lower, upper), comparator.as_ref())
{
continue;
}
if let Some(share) = region_share(input, (lower, upper))? {
inherited += share;
break;
}
}
if let Some((key, past)) = next {
lower = if past { Excluded(key) } else { Included(key) };
}
}
Ok(inherited)
}
pub fn inherit_into(
outputs: &[crate::table::Table],
inputs: &[crate::table::Table],
) -> crate::Result<()> {
for output in outputs {
if let Some(stats) = output.probe_stats() {
let range = &output.metadata.key_range;
stats.add(inherited_counts(range.min(), range.max(), inputs)?);
}
}
Ok(())
}
pub fn fraction_of(
table: &crate::table::Table,
bounds: (core::ops::Bound<&[u8]>, core::ops::Bound<&[u8]>),
) -> crate::Result<f64> {
let Some(span) =
table.data_span(bounds, crate::SeqNo::MAX, crate::table::SpanEdge::ByLastKey)?
else {
return Ok(0.0);
};
let live = span.data_end - span.live_start;
if live == 0 {
return Ok(0.0);
}
#[expect(
clippy::cast_precision_loss,
reason = "a fraction of two byte offsets; the digits lost past 2^53 do not matter"
)]
let fraction = span.covered as f64 / live as f64;
Ok(fraction)
}
#[cfg(feature = "std")]
pub fn share_of(
table: &crate::table::Table,
bounds: (core::ops::Bound<&[u8]>, core::ops::Bound<&[u8]>),
) -> crate::Result<ProbeCounts> {
if table.probe_stats().is_none() {
return Ok(ProbeCounts::default());
}
Ok(region_share(table, bounds)?.unwrap_or_default())
}
fn region_share(
table: &crate::table::Table,
bounds: (core::ops::Bound<&[u8]>, core::ops::Bound<&[u8]>),
) -> crate::Result<Option<ProbeCounts>> {
let Some(span) =
table.data_span(bounds, crate::SeqNo::MAX, crate::table::SpanEdge::ByLastKey)?
else {
return Ok(None);
};
let live = span.data_end - span.live_start;
if live == 0 || span.covered == 0 {
return Ok(None);
}
let Some(stats) = table.probe_stats() else {
return Ok(Some(ProbeCounts::default()));
};
let share = |count: u64| {
#[expect(
clippy::cast_possible_truncation,
reason = "covered <= live, so the quotient never exceeds the u64 count"
)]
let share = (u128::from(count) * u128::from(span.covered) / u128::from(live)) as u64;
share
};
Ok(Some(ProbeCounts {
probes: share(stats.probes()),
negatives: share(stats.negatives()),
}))
}
#[cfg(test)]
mod tests;