use crate::version::Version;
#[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,
}
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,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn stats_with_avg(avg_entry_on_disk_bytes: u64) -> StorageStats {
StorageStats {
used_bytes: 0,
capacity_bytes: None,
available_bytes: None,
compaction_possible: true,
full_compaction_bytes: 0,
tight_compaction_bytes: 0,
item_count: 0,
table_count: 0,
avg_entry_on_disk_bytes,
avg_key_bytes: None,
avg_value_bytes: None,
reclaimable_bytes_estimate: 0,
status: StorageStatus::Healthy,
}
}
#[test]
fn estimated_remaining_entries_divides_budget_by_average() {
let stats = stats_with_avg(50);
assert_eq!(stats.estimated_remaining_entries(1000), 20);
assert_eq!(stats.estimated_remaining_entries(1049), 20);
assert_eq!(stats.estimated_remaining_entries(0), 0);
}
#[test]
fn estimated_remaining_entries_is_zero_when_average_is_unknown() {
let stats = stats_with_avg(0);
assert_eq!(stats.estimated_remaining_entries(1_000_000), 0);
}
#[test]
fn compute_on_empty_version_maps_compaction_flag_to_status() {
use crate::TreeType;
use crate::version::Version;
let version = Version::new(0, TreeType::Standard);
#[expect(
clippy::unwrap_used,
reason = "compute_storage_stats cannot fail on an empty in-memory version (no file to stat)"
)]
let busy = compute_storage_stats(&version, true, true).unwrap();
assert_eq!(busy.status, StorageStatus::CompactionInProgress);
assert_eq!(busy.used_bytes, 0);
assert_eq!(busy.item_count, 0);
assert_eq!(busy.table_count, 0);
assert_eq!(busy.avg_key_bytes, None);
assert_eq!(busy.estimated_remaining_entries(1_000_000), 0);
#[expect(
clippy::unwrap_used,
reason = "compute_storage_stats cannot fail on an empty in-memory version (no file to stat)"
)]
let idle = compute_storage_stats(&version, false, true).unwrap();
assert_eq!(idle.status, StorageStatus::Healthy);
}
}