use crate::{
Cache, GlobalTableId, TreeId, UserValue,
version::BlobFileList,
vlog::{ValueHandle, blob_file::reader::Reader},
};
#[cfg(not(feature = "std"))]
use alloc::{string::ToString, vec::Vec};
pub struct Accessor<'a> {
blob_files: &'a BlobFileList,
#[cfg(zstd_any)]
zstd_dictionary: Option<&'a crate::compression::ZstdDictionary>,
}
impl<'a> Accessor<'a> {
pub fn new(blob_files: &'a BlobFileList) -> Self {
Self {
blob_files,
#[cfg(zstd_any)]
zstd_dictionary: None,
}
}
#[cfg(zstd_any)]
#[must_use]
pub fn with_dict(mut self, dict: Option<&'a crate::compression::ZstdDictionary>) -> Self {
self.zstd_dictionary = dict;
self
}
pub fn get(
&self,
tree_id: TreeId,
key: &[u8],
vhandle: &ValueHandle,
cache: &Cache,
) -> crate::Result<Option<UserValue>> {
if let Some(value) = cache.get_blob(tree_id, vhandle, key) {
return Ok(Some(value));
}
let Some(blob_file) = self.blob_files.get(vhandle.blob_file_id) else {
return Ok(None);
};
let bf_id = GlobalTableId::from((tree_id, blob_file.id()));
let (file, _) = blob_file
.file_accessor()
.get_or_open_blob_file(&bf_id, &blob_file.0.path)?;
let reader = {
let r = Reader::new(blob_file, file.as_ref());
#[cfg(zstd_any)]
let r = r.with_dict(self.zstd_dictionary);
r
};
let value = reader.get(key, vhandle)?;
cache.insert_blob(tree_id, vhandle, key, value.clone());
Ok(Some(value))
}
pub fn prefetch(
&self,
tree_id: TreeId,
items: &mut Vec<(&[u8], ValueHandle, usize)>,
cache: &Cache,
max_gap: u64,
max_read: usize,
) {
let capacity = cache.capacity();
if capacity == 0 {
return;
}
let half = capacity / 2;
let mut record_bytes: u64 = 0;
let mut full = false;
items.retain_mut(|(key, vhandle, len)| {
if full || cache.contains_blob(tree_id, vhandle) {
return false;
}
let Ok(record) = crate::vlog::blob_file::reader::record_len(key.len(), vhandle) else {
return false;
};
if record_bytes + record as u64 > half {
full = true;
return false;
}
record_bytes += record as u64;
*len = record;
true
});
if items.len() < 2 {
return;
}
let key =
|(_, vhandle, _): &(&[u8], ValueHandle, usize)| (vhandle.blob_file_id, vhandle.offset);
if !items.is_sorted_by_key(key) {
items.sort_unstable_by_key(key);
}
let mut read_budget = half;
let mut admit_budget = half;
let mut start = 0;
while start < items.len() && admit_budget > 0 && read_budget > 0 {
#[expect(clippy::indexing_slicing, reason = "start < items.len() by the loop")]
let (_, first, _) = items[start];
let Some((end, span_end)) =
span_extent(items, start, max_gap, max_read as u64, read_budget)
else {
start += 1;
continue;
};
if end - start >= 2
&& let Some(span) = items.get(start..end)
{
debug_assert!(span_end - first.offset <= read_budget);
read_budget -= span_end - first.offset;
self.warm_span(
tree_id,
span,
first.offset,
span_end,
cache,
&mut admit_budget,
);
}
start = end;
}
}
fn warm_span(
&self,
tree_id: TreeId,
records: &[(&[u8], ValueHandle, usize)],
span_start: u64,
span_end: u64,
cache: &Cache,
admit_budget: &mut u64,
) {
let Some((_, first, _)) = records.first() else {
return;
};
let Some(blob_file) = self.blob_files.get(first.blob_file_id) else {
return;
};
let bf_id = GlobalTableId::from((tree_id, blob_file.id()));
let Ok((file, _)) = blob_file
.file_accessor()
.get_or_open_blob_file(&bf_id, &blob_file.0.path)
else {
return;
};
debug_assert!(span_end >= span_start);
let Ok(span_len) = usize::try_from(span_end - span_start) else {
return;
};
let Ok(span) = crate::file::read_exact(file.as_ref(), span_start, span_len) else {
return;
};
let reader = {
let r = Reader::new(blob_file, file.as_ref());
#[cfg(zstd_any)]
let r = r.with_dict(self.zstd_dictionary);
r
};
let aliases_input = matches!(blob_file.0.meta.compression, crate::CompressionType::None);
for &(key, vhandle, len) in records {
if *admit_budget == 0 {
return;
}
debug_assert!(vhandle.offset >= span_start);
let Ok(rel) = usize::try_from(vhandle.offset - span_start) else {
continue;
};
let Some(record_end) = rel.checked_add(len) else {
continue;
};
let Some(bytes) = span.get(rel..record_end) else {
continue;
};
let record = if aliases_input {
crate::Slice::from(bytes)
} else {
span.slice(rel..record_end)
};
if let Ok(value) = reader.parse_record(key, &vhandle, &record) {
let weight = (key.len() + value.len()) as u64;
if weight > cache.max_entry_weight() {
continue;
}
if weight > *admit_budget {
*admit_budget = 0;
return;
}
*admit_budget -= weight;
cache.insert_blob(tree_id, &vhandle, key, value);
}
}
}
}
fn span_extent(
items: &[(&[u8], ValueHandle, usize)],
start: usize,
max_gap: u64,
max_read: u64,
read_budget: u64,
) -> Option<(usize, u64)> {
let reach = max_read.min(read_budget);
let (_, first, first_len) = *items.get(start)?;
let file_id = first.blob_file_id;
let mut end = start + 1;
let mut span_end = first.offset.checked_add(first_len as u64)?;
if span_end - first.offset > reach {
return Some((end, span_end));
}
while let Some(&(_, next, next_len)) = items.get(end) {
let within_gap = span_end
.checked_add(max_gap)
.is_some_and(|reach| next.offset <= reach);
if next.blob_file_id != file_id || !within_gap {
break;
}
let Some(next_end) = next.offset.checked_add(next_len as u64) else {
break;
};
debug_assert!(next_end >= first.offset);
if next_end - first.offset > reach {
break;
}
span_end = span_end.max(next_end);
end += 1;
}
Some((end, span_end))
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests;