use super::{
build::DiskANNTemporaryBudget,
changes::{classify, Change, DiskANNChangeRead, DiskANNJournalPruner},
format::{DiskANNChangeIdentity, DiskANNGeneration},
pages::DiskANNOriginReader,
DiskANNIndexOptions,
};
use crate::{read_control::StorageReadControl, StorageBackendResult};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DiskANNChangeStatistics {
pub documents: u64,
pub vectors: u64,
pub vector_bytes: u64,
}
impl DiskANNChangeStatistics {
pub fn checked_add(self, other: Self) -> StorageBackendResult<Self> {
let add = |left: u64, right: u64| {
left.checked_add(right)
.ok_or_else(|| invalid("outstanding change statistics overflow"))
};
Ok(Self {
documents: add(self.documents, other.documents)?,
vectors: add(self.vectors, other.vectors)?,
vector_bytes: add(self.vector_bytes, other.vector_bytes)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiskANNStatisticsCursor {
generation: DiskANNGeneration,
after: DiskANNChangeIdentity,
}
#[derive(Debug, Clone, Copy)]
pub struct DiskANNStatisticsRequest {
pub after: Option<DiskANNStatisticsCursor>,
pub max_records: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiskANNStatisticsPage {
pub generation: DiskANNGeneration,
pub examined: usize,
pub outstanding: DiskANNChangeStatistics,
pub next: Option<DiskANNStatisticsCursor>,
}
pub trait DiskANNMaintenanceSource: DiskANNJournalPruner {
fn statistics(
&self,
request: DiskANNStatisticsRequest,
control: &StorageReadControl,
) -> StorageBackendResult<DiskANNStatisticsPage>;
fn rebuild(
self: Box<Self>,
options: DiskANNIndexOptions,
temporary: &DiskANNTemporaryBudget,
control: &StorageReadControl,
) -> StorageBackendResult<()>;
}
pub(crate) fn measure(
origins: &DiskANNOriginReader,
journal: &dyn DiskANNChangeRead,
request: DiskANNStatisticsRequest,
control: &StorageReadControl,
) -> StorageBackendResult<DiskANNStatisticsPage> {
control.check()?;
let generation = origins.manifest().input().generation;
if request.max_records == 0
|| request
.after
.is_some_and(|cursor| cursor.generation != generation)
{
return Err(invalid("invalid statistics limit or generation cursor"));
}
let mut after = request.after.map(|cursor| cursor.after);
let mut page = DiskANNStatisticsPage {
generation,
examined: 0,
outstanding: DiskANNChangeStatistics::default(),
next: None,
};
for _ in 0..request.max_records.min(64) {
control.check()?;
let Some(identity) = journal.next_after(after, control)? else {
page.next = None;
control.check()?;
return Ok(page);
};
if after.is_some_and(|last| last.encode() >= identity.encode()) {
return Err(invalid("journal statistics cursor did not advance"));
}
match classify(origins, journal, identity, control)? {
Change::Missing => return Err(invalid("captured journal key has no captured value")),
Change::Reclaimable => {}
Change::Outstanding(origin) => {
let vector_bytes = origin
.count()
.checked_mul(u64::from(origin.dimensions()))
.and_then(|bytes| bytes.checked_mul(4))
.ok_or_else(|| invalid("outstanding vector byte count overflow"))?;
page.outstanding = page.outstanding.checked_add(DiskANNChangeStatistics {
documents: 1,
vectors: origin.count(),
vector_bytes,
})?;
}
}
page.examined += 1;
after = Some(identity);
page.next = Some(DiskANNStatisticsCursor {
generation,
after: identity,
});
}
control.check()?;
Ok(page)
}
fn invalid(message: &'static str) -> crate::StorageBackendError {
crate::mvcc::VersionError::InvalidEncoding(message).into_storage_error()
}