use std::sync::atomic::{AtomicUsize, Ordering};
use anyhow::Result;
use tracing::warn;
use crate::idx::IndexKeyBase;
use crate::idx::trees::hnsw::VectorId;
use crate::idx::trees::vector::SerializedVector;
use crate::kvs::ValsBatch;
use crate::val::RecordIdKey;
pub(crate) const PENDING_MAX_BATCH_KEYS: usize = 1024;
pub(crate) const PENDING_MAX_BYTES: usize = 4 * 1024 * 1024;
pub(crate) const PENDING_MAX_PAGE_BYTES: usize = PENDING_MAX_BYTES / 4;
pub(crate) const PENDING_PROBE_ROWS: u32 = 4;
pub(crate) const PENDING_MAX_ROWS: u32 = 16;
pub(crate) fn take_pending_record_id(
exact_id: &mut Option<RecordIdKey>,
decode_fallback: impl FnOnce() -> Result<RecordIdKey>,
) -> Result<RecordIdKey> {
match exact_id.take() {
Some(id) => Ok(id),
None => decode_fallback(),
}
}
const PENDING_REPORT_ARMED: usize = 1;
#[derive(Default)]
pub(crate) struct PendingBacklogReport {
state: AtomicUsize,
}
impl PendingBacklogReport {
fn snapshot(&self) -> usize {
self.state.load(Ordering::Relaxed)
}
fn arm(&self) -> bool {
let mut current = self.state.load(Ordering::Relaxed);
loop {
let next = current.wrapping_add(2) | PENDING_REPORT_ARMED;
match self.state.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return current & PENDING_REPORT_ARMED == 0,
Err(observed) => current = observed,
}
}
}
fn clear_if_unchanged(&self, snapshot: usize) {
if snapshot & PENDING_REPORT_ARMED == 0 {
return;
}
let _ = self.state.compare_exchange(
snapshot,
snapshot & !PENDING_REPORT_ARMED,
Ordering::Relaxed,
Ordering::Relaxed,
);
}
#[cfg(test)]
pub(crate) fn is_reported(&self) -> bool {
self.state.load(Ordering::Relaxed) & PENDING_REPORT_ARMED != 0
}
}
#[derive(Default)]
pub(crate) struct PendingScoreBatch {
pub(crate) ids: Vec<VectorId>,
pub(crate) vectors: Vec<Vec<SerializedVector>>,
bytes: usize,
page_bytes: usize,
}
impl PendingScoreBatch {
pub(crate) fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub(crate) fn retain_non_empty_vectors(&mut self) {
debug_assert_eq!(self.ids.len(), self.vectors.len());
let Self {
ids,
vectors,
..
} = self;
let mut position = 0;
ids.retain(|_| {
let keep = !vectors[position].is_empty();
position += 1;
keep
});
vectors.retain(|vectors| !vectors.is_empty());
debug_assert_eq!(ids.len(), vectors.len());
}
#[cfg(diskann)]
pub(crate) fn bytes(&self) -> usize {
self.bytes
}
#[cfg(diskann)]
pub(crate) fn page_bytes(&self) -> usize {
self.page_bytes
}
fn would_exceed(&self, bytes: usize) -> bool {
!self.is_empty()
&& (self.ids.len() >= PENDING_MAX_BATCH_KEYS
|| self.bytes + self.page_bytes + bytes > PENDING_MAX_BYTES)
}
}
pub(crate) struct PendingScan<'a> {
engine: &'static str,
ikb: &'a IndexKeyBase,
backlog_report: &'a PendingBacklogReport,
report_snapshot: usize,
stats: &'a PendingScanStats,
rows: u32,
entries: usize,
armed: bool,
pub(crate) batch: PendingScoreBatch,
}
impl<'a> PendingScan<'a> {
pub(crate) fn new(
engine: &'static str,
ikb: &'a IndexKeyBase,
backlog_report: &'a PendingBacklogReport,
stats: &'a PendingScanStats,
) -> Self {
let report_snapshot = backlog_report.snapshot();
Self {
engine,
ikb,
backlog_report,
report_snapshot,
stats,
rows: PENDING_PROBE_ROWS,
entries: 0,
armed: false,
batch: PendingScoreBatch::default(),
}
}
pub(crate) fn rows(&self) -> u32 {
self.rows
}
#[cfg(diskann)]
pub(crate) fn entries(&self) -> usize {
self.entries
}
pub(crate) fn observe_page(&mut self, read: &ValsBatch<'_>) {
let bytes = (read.key_bytes + read.value_bytes) as usize;
self.stats.record_page(read.len(), bytes);
self.batch.page_bytes = bytes;
if read.is_empty() {
return;
}
let avg = bytes.div_ceil(read.len()).max(1);
self.rows = (PENDING_MAX_PAGE_BYTES / avg).clamp(1, PENDING_MAX_ROWS as usize) as u32;
}
pub(crate) fn release_page(&mut self) {
self.batch.page_bytes = 0;
}
pub(crate) fn charge_entry(&mut self, entry_bytes: usize) {
self.entries += 1;
self.stats.record_entry();
if self.entries > PENDING_MAX_BATCH_KEYS || self.batch.would_exceed(entry_bytes) {
self.report();
}
}
pub(crate) fn rollover_required(&mut self, bytes: usize) -> bool {
if !self.batch.would_exceed(bytes) {
return false;
}
self.report();
true
}
pub(crate) fn push(&mut self, id: VectorId, vectors: Vec<SerializedVector>, bytes: usize) {
self.batch.ids.push(id);
self.batch.vectors.push(vectors);
self.batch.bytes += bytes;
}
pub(crate) fn begin_scoring(&mut self) {
self.stats.record_batch(self.batch.ids.len(), self.batch.bytes, self.batch.page_bytes);
self.batch.bytes = 0;
}
#[cfg(diskann)]
pub(crate) fn record_side_read(&self, keys: usize, resident_bytes: usize) {
self.stats.record_side_read(keys, resident_bytes);
}
pub(crate) fn finish(&self) {
if !self.armed {
self.backlog_report.clear_if_unchanged(self.report_snapshot);
}
}
fn report(&mut self) {
if self.armed {
return;
}
self.armed = true;
if self.backlog_report.arm() {
warn!(
index = %self.ikb,
engine = self.engine,
pending_entries_read = self.entries,
"kNN search read more pending updates than one materialisation batch holds; index compaction is not keeping up with writes"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::{DatabaseId, IndexId, NamespaceId};
#[test]
fn scoring_batch_drops_cleared_candidates_in_place() {
let mut batch = PendingScoreBatch {
ids: vec![VectorId::DocId(1), VectorId::DocId(2), VectorId::DocId(3)],
vectors: vec![vec![], vec![SerializedVector::F32(vec![1.0, 2.0])], vec![]],
bytes: 42,
page_bytes: 7,
};
batch.retain_non_empty_vectors();
assert_eq!(batch.ids, vec![VectorId::DocId(2)]);
assert_eq!(batch.vectors, vec![vec![SerializedVector::F32(vec![1.0, 2.0])]]);
assert_eq!(batch.bytes, 42, "residency accounting covers the original batch");
assert_eq!(batch.page_bytes, 7);
}
#[test]
fn stale_scan_cannot_clear_a_newer_backlog_observation() {
let ikb = IndexKeyBase::new(NamespaceId(1), DatabaseId(2), "tb".into(), IndexId(3));
let report = PendingBacklogReport::default();
let stats = PendingScanStats::default();
let older_clean = PendingScan::new("HNSW", &ikb, &report, &stats);
let mut newer_crossing = PendingScan::new("HNSW", &ikb, &report, &stats);
newer_crossing.report();
assert!(report.is_reported());
older_clean.finish();
assert!(report.is_reported());
let older_armed = PendingScan::new("HNSW", &ikb, &report, &stats);
let mut latest_crossing = PendingScan::new("HNSW", &ikb, &report, &stats);
latest_crossing.report();
older_armed.finish();
assert!(report.is_reported());
PendingScan::new("HNSW", &ikb, &report, &stats).finish();
assert!(!report.is_reported());
}
}
#[derive(Default)]
pub(crate) struct PendingScanStats {
#[cfg(test)]
peak_batch_entries: AtomicUsize,
#[cfg(test)]
peak_batch_bytes: AtomicUsize,
#[cfg(test)]
batches: AtomicUsize,
#[cfg(test)]
peak_page_entries: AtomicUsize,
#[cfg(test)]
peak_page_bytes: AtomicUsize,
#[cfg(test)]
pages: AtomicUsize,
#[cfg(test)]
peak_resident_bytes: AtomicUsize,
#[cfg(test)]
entries_read: AtomicUsize,
#[cfg(test)]
side_read_keys: AtomicUsize,
#[cfg(test)]
side_reads: AtomicUsize,
#[cfg(test)]
interrupt: std::sync::OnceLock<(usize, crate::ctx::Canceller)>,
}
impl PendingScanStats {
fn record_batch(&self, entries: usize, bytes: usize, page_bytes: usize) {
#[cfg(test)]
{
self.peak_batch_entries.fetch_max(entries, Ordering::Relaxed);
self.peak_batch_bytes.fetch_max(bytes, Ordering::Relaxed);
self.peak_resident_bytes.fetch_max(bytes + page_bytes, Ordering::Relaxed);
self.batches.fetch_add(1, Ordering::Relaxed);
}
#[cfg(not(test))]
let _ = (entries, bytes, page_bytes);
}
fn record_page(&self, entries: usize, bytes: usize) {
#[cfg(test)]
{
self.peak_page_entries.fetch_max(entries, Ordering::Relaxed);
self.peak_page_bytes.fetch_max(bytes, Ordering::Relaxed);
self.pages.fetch_add(1, Ordering::Relaxed);
}
#[cfg(not(test))]
let _ = (entries, bytes);
}
fn record_entry(&self) {
#[cfg(test)]
{
let read = self.entries_read.fetch_add(1, Ordering::Relaxed) + 1;
if let Some((at, canceller)) = self.interrupt.get()
&& read >= *at
{
canceller.cancel();
}
}
}
#[cfg(diskann)]
fn record_side_read(&self, keys: usize, resident_bytes: usize) {
#[cfg(test)]
{
self.side_read_keys.fetch_add(keys, Ordering::Relaxed);
self.side_reads.fetch_add(1, Ordering::Relaxed);
self.peak_resident_bytes.fetch_max(resident_bytes, Ordering::Relaxed);
}
#[cfg(not(test))]
let _ = (keys, resident_bytes);
}
#[cfg(test)]
pub(crate) fn interrupt_at(&self, at: usize, canceller: crate::ctx::Canceller) {
let _ = self.interrupt.set((at, canceller));
}
#[cfg(test)]
pub(crate) fn peak_batch_entries(&self) -> usize {
self.peak_batch_entries.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn peak_batch_bytes(&self) -> usize {
self.peak_batch_bytes.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn batches(&self) -> usize {
self.batches.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn peak_page_entries(&self) -> usize {
self.peak_page_entries.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn peak_page_bytes(&self) -> usize {
self.peak_page_bytes.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn pages(&self) -> usize {
self.pages.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn peak_resident_bytes(&self) -> usize {
self.peak_resident_bytes.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn entries_read(&self) -> usize {
self.entries_read.load(Ordering::Relaxed)
}
#[cfg(all(test, diskann))]
pub(crate) fn side_read_keys(&self) -> usize {
self.side_read_keys.load(Ordering::Relaxed)
}
#[cfg(all(test, diskann))]
pub(crate) fn side_reads(&self) -> usize {
self.side_reads.load(Ordering::Relaxed)
}
}