use crate::comparator::UserComparator;
use crate::range_tombstone::RangeTombstone;
use crate::{BlobFile, Table, blob_tree::FragmentationMap};
use alloc::sync::Arc;
use portable_atomic::{AtomicU64, Ordering::Relaxed};
#[cfg(feature = "std")]
use parking_lot::Mutex;
#[cfg(not(feature = "std"))]
use spin::Mutex;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Work<T> {
pub moved: T,
pub carried: T,
pub reprotected: T,
pub reencoded: T,
pub read: T,
pub written: T,
}
impl<T> Work<T> {
fn zip<U, V>(self, other: Work<U>, f: impl Fn(T, U) -> V) -> Work<V> {
Work {
moved: f(self.moved, other.moved),
carried: f(self.carried, other.carried),
reprotected: f(self.reprotected, other.reprotected),
reencoded: f(self.reencoded, other.reencoded),
read: f(self.read, other.read),
written: f(self.written, other.written),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WorkEstimate {
pub expected: Work<u64>,
pub ceiling: Work<Option<u64>>,
}
impl WorkEstimate {
fn exact(work: Work<u64>) -> Self {
Self {
expected: work,
ceiling: work.zip(work, |figure, _| Some(figure)),
}
}
#[must_use]
pub fn contains(&self, measured: &Work<u64>) -> bool {
let within = measured.zip(self.ceiling, |figure, ceiling| {
ceiling.is_none_or(|ceiling| figure <= ceiling)
});
within.moved
&& within.carried
&& within.reprotected
&& within.reencoded
&& within.read
&& within.written
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WorkReport {
pub estimate: WorkEstimate,
pub measured: Work<u64>,
}
impl WorkReport {
#[must_use]
pub fn error(&self) -> Work<i128> {
self.measured
.zip(self.estimate.expected, |measured, expected| {
i128::from(measured) - i128::from(expected)
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Installed {
pub written: u64,
pub carried: u64,
pub read: u64,
}
#[derive(Default)]
pub struct WorkLedger {
estimate: Mutex<WorkEstimate>,
moved: AtomicU64,
carried: AtomicU64,
written: AtomicU64,
read: AtomicU64,
}
impl WorkLedger {
pub(crate) fn plan(&self, estimate: WorkEstimate) {
*self.estimate.lock() = estimate;
}
pub(crate) fn moved(&self, bytes: u64) {
self.moved.fetch_add(bytes, Relaxed);
}
pub(crate) fn read(&self, bytes: u64) {
self.read.fetch_add(bytes, Relaxed);
}
pub(crate) fn installed(&self, installed: Installed) {
self.written.fetch_add(installed.written, Relaxed);
self.carried.fetch_add(installed.carried, Relaxed);
self.read.fetch_add(installed.read, Relaxed);
}
pub(crate) fn report(&self) -> WorkReport {
let written = self.written.load(Relaxed);
let carried = self.carried.load(Relaxed);
debug_assert!(
carried <= written,
"every carried byte is a byte of an output"
);
WorkReport {
estimate: *self.estimate.lock(),
measured: Work {
moved: self.moved.load(Relaxed),
carried,
reprotected: 0,
reencoded: written - carried,
read: self.read.load(Relaxed),
written,
},
}
}
}
pub struct InputPacer {
#[cfg(feature = "std")]
limiter: Arc<crate::rate_limiter::RateLimiter>,
work: Arc<WorkLedger>,
}
impl InputPacer {
pub(crate) fn new(
limiter: &Arc<crate::rate_limiter::RateLimiter>,
work: &Arc<WorkLedger>,
) -> Self {
#[cfg(not(feature = "std"))]
let _ = limiter;
Self {
#[cfg(feature = "std")]
limiter: Arc::clone(limiter),
work: Arc::clone(work),
}
}
}
impl crate::table::util::ReadPacer for InputPacer {
fn active(&self) -> bool {
#[cfg(feature = "std")]
return self.limiter.steers_by_latency();
#[cfg(not(feature = "std"))]
false
}
fn pace(&self, _offset: u64, _len: u64) {}
#[cfg(feature = "std")]
fn read_took(&self, elapsed: core::time::Duration) {
self.limiter.record_read_latency(elapsed);
}
fn read_bytes(&self, len: u64) {
self.work.read(len);
}
}
pub fn estimate_move<'a>(tables: impl IntoIterator<Item = &'a Table>) -> WorkEstimate {
WorkEstimate::exact(Work {
moved: tables.into_iter().map(Table::file_len).sum(),
..Work::default()
})
}
pub struct MergePlan<'a> {
pub inputs: &'a [Table],
pub blob_files: &'a [BlobFile],
pub gc_stats: &'a FragmentationMap,
pub user_code: bool,
pub range_tombstones: &'a [RangeTombstone],
pub comparator: &'a dyn UserComparator,
}
pub fn estimate_merge(plan: &MergePlan<'_>, carries: impl Fn(&Table) -> bool) -> WorkEstimate {
let mut expected = Work::<u64>::default();
let mut carried_ceiling = 0;
for (at, table) in plan.inputs.iter().enumerate() {
let size = table.file_len();
expected.written += size;
carried_ceiling += size;
expected.read += size;
if !carries(table) || !unchanged_by_merge(plan, at, table) {
continue;
}
let groups = table.data_section_len();
expected.carried += groups;
expected.read += groups;
}
for blob_file in plan.blob_files {
let frames = blob_file.meta().total_compressed_bytes;
let garbage = plan
.gc_stats
.get(&blob_file.id())
.map_or(0, |entry| entry.on_disk_bytes);
let live = frames.saturating_sub(garbage);
expected.carried += live;
expected.written += live;
expected.read += frames;
carried_ceiling += frames;
}
expected.reencoded = expected.written - expected.carried;
WorkEstimate {
expected,
ceiling: Work {
moved: Some(0),
carried: Some(carried_ceiling),
reprotected: Some(0),
reencoded: None,
read: None,
written: None,
},
}
}
fn unchanged_by_merge(plan: &MergePlan<'_>, at: usize, table: &Table) -> bool {
let meta = &table.metadata;
let (min, max) = (meta.key_range.min(), meta.key_range.max());
let below = |a: &[u8], b: &[u8]| plan.comparator.compare(a, b) == core::cmp::Ordering::Less;
!plan.user_code
&& meta.tombstone_count == 0
&& meta.weak_tombstone_count == 0
&& meta.range_tombstone_count == 0
&& meta.key_count == Some(meta.item_count)
&& plan.inputs.iter().enumerate().all(|(other_at, other)| {
let other = &other.metadata.key_range;
other_at == at || below(other.max(), min) || below(max, other.min())
})
&& plan
.range_tombstones
.iter()
.all(|rt| below(max, &rt.start) || !below(min, &rt.end))
}
#[cfg(test)]
mod tests;