#[cfg(feature = "metrics")]
use crate::metrics::Metrics;
use crate::{
Cache, GlobalTableId, TreeId, UserValue,
config::BlobCacheFill,
version::BlobFileList,
vlog::{ValueHandle, blob_file::reader::Reader},
};
#[cfg(not(feature = "std"))]
use alloc::{string::ToString, vec::Vec};
#[derive(Default)]
pub struct Prefetched {
values: crate::HashMap<(crate::vlog::BlobFileId, u64), (crate::UserKey, u32, UserValue)>,
}
impl Prefetched {
pub(crate) fn take(&mut self, key: &[u8], vhandle: &ValueHandle) -> Option<UserValue> {
let place = (vhandle.blob_file_id, vhandle.offset);
match self.values.get(&place) {
Some((read_key, size, _)) if &**read_key == key && *size == vhandle.on_disk_size => {
self.values.remove(&place).map(|(_, _, value)| value)
}
_ => None,
}
}
pub(crate) fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub(crate) fn clear(&mut self) {
self.values.clear();
}
#[cfg(test)]
pub(crate) fn hold(&mut self, key: &[u8], vhandle: &ValueHandle, value: UserValue) {
self.values.insert(
(vhandle.blob_file_id, vhandle.offset),
(crate::UserKey::from(key), vhandle.on_disk_size, value),
);
}
}
pub enum PrefetchInto<'p> {
Cache,
Local(&'p mut Prefetched),
}
pub struct Accessor<'a> {
blob_files: &'a BlobFileList,
fill: BlobCacheFill,
#[cfg(feature = "metrics")]
metrics: Option<&'a Metrics>,
}
impl<'a> Accessor<'a> {
pub fn new(
blob_files: &'a BlobFileList,
fill: BlobCacheFill,
#[cfg(feature = "metrics")] metrics: Option<&'a Metrics>,
) -> Self {
Self {
blob_files,
fill,
#[cfg(feature = "metrics")]
metrics,
}
}
#[cfg_attr(
not(feature = "metrics"),
expect(
unused_variables,
clippy::unused_self,
reason = "the argument and the accessor's counters are both the feature's payload"
)
)]
#[inline]
pub(crate) fn count_request(&self, on_disk: usize) {
#[cfg(feature = "metrics")]
if let Some(metrics) = self.metrics {
use core::sync::atomic::Ordering::Relaxed;
metrics.blob_read_io.fetch_add(1, Relaxed);
metrics
.blob_bytes_io_requested
.fetch_add(on_disk as u64, Relaxed);
}
}
#[cfg_attr(
not(feature = "metrics"),
expect(
unused_variables,
clippy::unused_self,
reason = "the argument and the accessor's counters are both the feature's payload"
)
)]
#[inline]
pub(crate) fn count_decoded(&self, decoded: usize) {
#[cfg(feature = "metrics")]
if let Some(metrics) = self.metrics {
use core::sync::atomic::Ordering::Relaxed;
metrics
.blob_bytes_decoded
.fetch_add(decoded as u64, Relaxed);
}
}
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 lead = match cache.claim_blob(tree_id, vhandle, key) {
crate::cache::BlobClaim::Shared(value) => {
#[cfg(feature = "metrics")]
if let Some(metrics) = self.metrics {
metrics
.blob_load_shared
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
if self.fill.fills() && !cache.contains_blob(tree_id, vhandle) {
let detached = cache.insert_blob(tree_id, vhandle, key, value.clone());
self.count_gather(detached);
}
return Ok(Some(value));
}
crate::cache::BlobClaim::Lead(lead) => {
if let Some(value) = cache.get_blob(tree_id, vhandle, key) {
lead.publish(cache, tree_id, vhandle, key, value.clone(), false);
return Ok(Some(value));
}
Some(lead)
}
crate::cache::BlobClaim::Alone => None,
};
let timer = crate::cache::Cache::time_load();
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 = Reader::new(blob_file, file.as_ref());
let len = crate::vlog::blob_file::reader::record_len(key.len(), vhandle)?;
self.count_request(len);
let record = reader.read_record(vhandle, len)?;
let mut decoded = 0;
let parsed = reader.parse_record(key, vhandle, &record, &mut decoded);
self.count_decoded(decoded);
let value = parsed?;
cache.record_blob_load(tree_id, vhandle, timer);
match lead {
Some(lead) => {
let detached = lead.publish(
cache,
tree_id,
vhandle,
key,
value.clone(),
self.fill.fills(),
);
self.count_gather(detached);
}
None if self.fill.fills() => {
let detached = cache.insert_blob(tree_id, vhandle, key, value.clone());
self.count_gather(detached);
}
None => {}
}
Ok(Some(value))
}
#[cfg_attr(
not(feature = "metrics"),
expect(
unused_variables,
clippy::unused_self,
reason = "the argument and the accessor's counters are both the feature's payload"
)
)]
#[inline]
pub(crate) fn count_gather(&self, bytes: usize) {
#[cfg(feature = "metrics")]
if let Some(metrics) = self.metrics {
metrics.record_gather(bytes);
}
}
pub fn prefetch(
&self,
tree_id: TreeId,
items: &mut Vec<(&[u8], ValueHandle, usize)>,
cache: &Cache,
max_gap: u64,
max_read: usize,
mut into: PrefetchInto<'_>,
) {
let half = match into {
PrefetchInto::Cache => cache.capacity() / 2,
PrefetchInto::Local(_) => max_read as u64,
};
if half == 0 {
return;
}
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 spans: Vec<Span> = Vec::new();
let mut start = 0;
while start < items.len() && 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 {
debug_assert!(span_end - first.offset <= read_budget);
read_budget -= span_end - first.offset;
spans.push(Span {
records: start..end,
start: first.offset,
end: span_end,
});
}
start = end;
}
let mut admit_budget = half;
let mut start = 0;
while start < spans.len() && admit_budget > 0 {
let mut end = start;
let mut extent = 0u64;
while let Some(span) = spans.get(end) {
let width = span.end - span.start;
if end > start && extent + width > max_read as u64 {
break;
}
extent += width;
end += 1;
}
if let Some(batch) = spans.get(start..end) {
self.warm_spans(tree_id, items, batch, cache, &mut into, &mut admit_budget);
}
start = end;
}
}
fn warm_spans(
&self,
tree_id: TreeId,
items: &[(&[u8], ValueHandle, usize)],
spans: &[Span],
cache: &Cache,
into: &mut PrefetchInto<'_>,
admit_budget: &mut u64,
) {
struct SpanRead<'b> {
span: usize,
blob_file: &'b crate::vlog::BlobFile,
file: alloc::sync::Arc<dyn crate::fs::FsFile>,
buf: Vec<u8>,
}
let mut reads: Vec<SpanRead<'_>> = Vec::with_capacity(spans.len());
for (at, span) in spans.iter().enumerate() {
let Some((_, first, _)) = items.get(span.records.start) else {
continue;
};
let Some(blob_file) = self.blob_files.get(first.blob_file_id) else {
continue;
};
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 {
continue;
};
debug_assert!(span.end >= span.start);
let Ok(len) = usize::try_from(span.end - span.start) else {
continue;
};
self.count_request(len);
#[cfg(feature = "metrics")]
if let Some(metrics) = self.metrics {
metrics
.blob_bytes_prefetched
.fetch_add(len as u64, core::sync::atomic::Ordering::Relaxed);
}
reads.push(SpanRead {
span: at,
blob_file,
file,
buf: alloc::vec![0u8; len],
});
}
let mut issued = alloc::vec![false; reads.len()];
while let Some(lead) = issued.iter().position(|issued| !issued) {
let Some(fs) = reads
.get(lead)
.map(|read| alloc::sync::Arc::clone(&read.blob_file.0.fs))
else {
break;
};
let on_fs = |read: &SpanRead<'_>| {
core::ptr::addr_eq(
alloc::sync::Arc::as_ptr(&read.blob_file.0.fs),
alloc::sync::Arc::as_ptr(&fs),
)
};
let group: Vec<usize> = reads
.iter()
.enumerate()
.skip(lead)
.filter(|(at, read)| issued.get(*at) == Some(&false) && on_fs(read))
.map(|(at, _)| at)
.collect();
for &at in &group {
if let Some(slot) = issued.get_mut(at) {
*slot = true;
}
}
let mut got = alloc::vec![false; group.len()];
{
let mut reqs: Vec<crate::fs::BlockRead<'_>> = reads
.iter_mut()
.enumerate()
.filter(|(at, _)| group.binary_search(at).is_ok())
.map(|(_, read)| crate::fs::BlockRead {
file: read.file.as_ref(),
offset: spans.get(read.span).map_or(0, |span| span.start),
buf: crate::fs::BlockBuf::new(&mut read.buf),
})
.collect();
let _ = fs.read_blocks_batched_each(&mut reqs, &mut |index, _| {
if let Some(slot) = got.get_mut(index) {
*slot = true;
}
});
}
for (&at, got) in group.iter().zip(got) {
if !got && let Some(read) = reads.get_mut(at) {
read.buf.clear();
}
}
}
for read in reads {
if *admit_budget == 0 {
return;
}
let (Some(span), false) = (spans.get(read.span), read.buf.is_empty()) else {
continue;
};
let Some(records) = items.get(span.records.clone()) else {
continue;
};
self.admit_span(
tree_id,
records,
span.start,
&crate::Slice::from(read.buf),
read.blob_file,
read.file.as_ref(),
cache,
into,
admit_budget,
);
}
}
#[expect(
clippy::too_many_arguments,
reason = "one span: its records and bytes, its file, and where its values go under which budget"
)]
fn admit_span(
&self,
tree_id: TreeId,
records: &[(&[u8], ValueHandle, usize)],
span_start: u64,
span: &crate::Slice,
blob_file: &crate::vlog::BlobFile,
file: &dyn crate::fs::FsFile,
cache: &Cache,
into: &mut PrefetchInto<'_>,
admit_budget: &mut u64,
) {
let reader = Reader::new(blob_file, file);
let aliases_span = 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;
};
if record_end > span.len() {
continue;
}
let record = span.slice(rel..record_end);
let mut decoded = 0;
let parsed = reader.parse_record(key, &vhandle, &record, &mut decoded);
self.count_decoded(decoded);
if let Ok(value) = parsed {
let weight = (key.len() + value.len()) as u64;
if matches!(into, PrefetchInto::Cache) && weight > cache.max_entry_weight() {
continue;
}
if weight > *admit_budget {
*admit_budget = 0;
return;
}
*admit_budget -= weight;
let value = if aliases_span {
self.count_gather(value.len());
UserValue::new(&value)
} else {
value
};
match into {
PrefetchInto::Cache => {
let detached = cache.insert_blob(tree_id, &vhandle, key, value);
self.count_gather(detached);
}
PrefetchInto::Local(store) => {
self.count_gather(key.len());
store.values.insert(
(vhandle.blob_file_id, vhandle.offset),
(crate::UserKey::from(key), vhandle.on_disk_size, value),
);
}
}
}
}
}
}
struct Span {
records: core::ops::Range<usize>,
start: u64,
end: u64,
}
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;