use crate::version::Version;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum StorageStatus {
Healthy,
FullCompactionAvailable,
TightCompactionAvailable,
ReadOnlyOutOfSpace,
CompactionInProgress,
}
#[must_use]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct StorageStats {
pub used_bytes: u64,
pub capacity_bytes: Option<u64>,
pub available_bytes: Option<u64>,
pub compaction_possible: bool,
pub full_compaction_bytes: u64,
pub tight_compaction_bytes: u64,
pub item_count: u64,
pub table_count: u64,
pub avg_entry_on_disk_bytes: u64,
pub avg_key_bytes: Option<u64>,
pub avg_value_bytes: Option<u64>,
pub reclaimable_bytes_estimate: u64,
pub status: StorageStatus,
}
#[must_use]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct ApproximateRangeStats {
pub bytes: u64,
pub key_count: u64,
}
#[must_use]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct SegmentStats {
pub table_id: crate::TableId,
pub level: usize,
pub used_bytes: u64,
pub item_count: u64,
pub reads: u64,
pub last_access_secs: u64,
}
#[must_use]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LevelStats {
pub level: usize,
pub segment_count: usize,
pub used_bytes: u64,
pub item_count: u64,
pub reads: u64,
pub last_access_secs: u64,
pub segments: Vec<SegmentStats>,
}
#[must_use]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct RangeCardinality {
pub rows: u64,
pub selectivity: f64,
}
pub trait StorageStatistics {
fn storage_stats(&self) -> crate::Result<StorageStats>;
fn level_segment_stats(&self) -> crate::Result<Vec<LevelStats>>;
fn compaction_debt(&self, strategy: &dyn crate::compaction::CompactionStrategy) -> u64;
#[cfg(feature = "metrics")]
fn cache_stats(&self) -> crate::CacheStats;
}
impl<T: crate::AbstractTree + ?Sized> StorageStatistics for T {
fn storage_stats(&self) -> crate::Result<StorageStats> {
crate::AbstractTree::storage_stats(self)
}
fn level_segment_stats(&self) -> crate::Result<Vec<LevelStats>> {
crate::AbstractTree::level_segment_stats(self)
}
fn compaction_debt(&self, strategy: &dyn crate::compaction::CompactionStrategy) -> u64 {
crate::AbstractTree::compaction_debt(self, strategy)
}
#[cfg(feature = "metrics")]
fn cache_stats(&self) -> crate::CacheStats {
crate::AbstractTree::cache_stats(self)
}
}
impl StorageStats {
#[must_use]
pub fn estimated_remaining_entries(&self, budget_bytes: u64) -> u64 {
if self.avg_entry_on_disk_bytes == 0 {
0
} else {
budget_bytes / self.avg_entry_on_disk_bytes
}
}
}
pub(crate) fn compute_used_bytes(version: &Version) -> crate::Result<u64> {
let mut used_bytes = 0u64;
for table in version.iter_tables() {
used_bytes += table.fs.metadata(&table.path)?.len;
}
for blob in version.blob_files.iter() {
used_bytes += blob.0.fs.metadata(&blob.0.path)?.len;
}
Ok(used_bytes)
}
pub(crate) fn full_compaction_demand_bytes(version: &Version) -> u64 {
version
.iter_levels()
.map(crate::version::Level::size)
.max()
.unwrap_or(0)
}
pub(crate) fn compute_storage_stats(
version: &Version,
is_compacting: bool,
value_bytes_are_user_values: bool,
) -> crate::Result<StorageStats> {
let mut used_bytes = 0u64;
let mut item_count = 0u64;
let mut table_count = 0u64;
let mut reclaimable_entries = 0u64;
let mut sum_key = 0u64;
let mut sum_value = 0u64;
let mut all_have_shape = true;
for table in version.iter_tables() {
let m = &table.metadata;
let on_disk = table.fs.metadata(&table.path)?.len;
used_bytes += on_disk;
item_count += m.item_count;
table_count += 1;
reclaimable_entries += m.weak_tombstone_reclaimable;
match (m.sum_user_key_bytes, m.sum_value_bytes) {
(Some(k), Some(v)) => {
sum_key += k;
sum_value += v;
}
_ => all_have_shape = false,
}
}
for blob in version.blob_files.iter() {
used_bytes += blob.0.fs.metadata(&blob.0.path)?.len;
}
let avg_entry_on_disk_bytes = if item_count == 0 {
0
} else {
used_bytes / item_count
};
let have_shape = all_have_shape && item_count > 0;
let avg_key_bytes = have_shape.then(|| sum_key / item_count);
let avg_value_bytes =
(have_shape && value_bytes_are_user_values).then(|| sum_value / item_count);
let reclaimable_bytes_estimate = reclaimable_entries * avg_entry_on_disk_bytes;
let full_compaction_bytes = full_compaction_demand_bytes(version);
let tight_compaction_bytes = crate::tree::MIN_RESERVED_HEADROOM;
let status = if is_compacting {
StorageStatus::CompactionInProgress
} else {
StorageStatus::Healthy
};
Ok(StorageStats {
used_bytes,
capacity_bytes: None,
available_bytes: None,
compaction_possible: true,
full_compaction_bytes,
tight_compaction_bytes,
item_count,
table_count,
avg_entry_on_disk_bytes,
avg_key_bytes,
avg_value_bytes,
reclaimable_bytes_estimate,
status,
})
}
pub(crate) fn compute_level_segment_stats(version: &Version) -> crate::Result<Vec<LevelStats>> {
use core::sync::atomic::Ordering::Relaxed;
let mut levels = Vec::with_capacity(version.level_count());
for (level, run_group) in version.iter_levels().enumerate() {
let mut segments = Vec::new();
let mut used_bytes = 0u64;
let mut item_count = 0u64;
let mut reads = 0u64;
let mut last_access_secs = 0u64;
for run in run_group.iter() {
for table in run.iter() {
let on_disk = table.fs.metadata(&table.path)?.len;
let items = table.metadata.item_count;
let seg_reads = table.read_count.load(Relaxed);
let seg_access = table.last_access_secs.load(Relaxed);
used_bytes += on_disk;
item_count += items;
reads = reads.saturating_add(seg_reads);
last_access_secs = last_access_secs.max(seg_access);
segments.push(SegmentStats {
table_id: table.metadata.id,
level,
used_bytes: on_disk,
item_count: items,
reads: seg_reads,
last_access_secs: seg_access,
});
}
}
levels.push(LevelStats {
level,
segment_count: segments.len(),
used_bytes,
item_count,
reads,
last_access_secs,
segments,
});
}
Ok(levels)
}
#[cfg(test)]
mod tests;