diff --git a/src/metrics.rs b/src/metrics.rs index fac3baa3e..49a1f6ab9 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -34,6 +34,12 @@ pub struct Metrics { /// Number of blocks that were read from block cache pub(crate) data_block_load_cached: AtomicUsize, + /// Number of range tombstone blocks that were actually read from disk + pub(crate) range_tombstone_block_load_io: AtomicUsize, + + /// Number of range tombstone blocks that were read from block cache + pub(crate) range_tombstone_block_load_cached: AtomicUsize, + /// Number of filter queries that were performed pub(crate) filter_queries: AtomicUsize, @@ -48,6 +54,9 @@ pub struct Metrics { /// Number of filter block bytes that were requested from OS or disk pub(crate) filter_block_io_requested: AtomicU64, + + /// Number of range tombstone block bytes that were requested from OS or disk + pub(crate) range_tombstone_block_io_requested: AtomicU64, } #[expect( @@ -82,11 +91,17 @@ impl Metrics { self.filter_block_io_requested.load(Relaxed) } + /// Number of I/O range tombstone block bytes transferred from disk or OS page cache. + pub fn range_tombstone_block_io(&self) -> u64 { + self.range_tombstone_block_io_requested.load(Relaxed) + } + /// Number of I/O block bytes transferred from disk or OS page cache. pub fn block_io(&self) -> u64 { self.data_block_io_requested.load(Relaxed) + self.index_block_io_requested.load(Relaxed) + self.filter_block_io_requested.load(Relaxed) + + self.range_tombstone_block_io_requested.load(Relaxed) } /// Number of data blocks that were accessed. @@ -104,33 +119,46 @@ impl Metrics { self.filter_block_load_cached.load(Relaxed) + self.filter_block_load_io.load(Relaxed) } + /// Number of range tombstone blocks that were accessed. + pub fn range_tombstone_block_load_count(&self) -> usize { + self.range_tombstone_block_load_cached.load(Relaxed) + + self.range_tombstone_block_load_io.load(Relaxed) + } + /// Number of blocks that were loaded from disk or OS page cache. pub fn block_load_io_count(&self) -> usize { self.data_block_load_io.load(Relaxed) + self.index_block_load_io.load(Relaxed) + self.filter_block_load_io.load(Relaxed) + + self.range_tombstone_block_load_io.load(Relaxed) } - /// Number of data blocks that were loaded from disk or OS page cache. + /// Number of data blocks that were served from block cache. pub fn data_block_load_cached_count(&self) -> usize { self.data_block_load_cached.load(Relaxed) } - /// Number of index blocks that were loaded from disk or OS page cache. + /// Number of index blocks that were served from block cache. pub fn index_block_load_cached_count(&self) -> usize { self.index_block_load_cached.load(Relaxed) } - /// Number of filter blocks that were loaded from disk or OS page cache. + /// Number of filter blocks that were served from block cache. pub fn filter_block_load_cached_count(&self) -> usize { self.filter_block_load_cached.load(Relaxed) } - /// Number of blocks that were loaded from disk or OS page cache. + /// Number of range tombstone blocks that were served from block cache. + pub fn range_tombstone_block_load_cached_count(&self) -> usize { + self.range_tombstone_block_load_cached.load(Relaxed) + } + + /// Number of blocks that were served from block cache. pub fn block_load_cached_count(&self) -> usize { self.data_block_load_cached.load(Relaxed) + self.index_block_load_cached.load(Relaxed) + self.filter_block_load_cached.load(Relaxed) + + self.range_tombstone_block_load_cached.load(Relaxed) } /// Number of blocks that were accessed. @@ -174,6 +202,18 @@ impl Metrics { } } + /// Range tombstone block cache efficiency in percent (0.0 - 1.0). + pub fn range_tombstone_block_cache_hit_rate(&self) -> f64 { + let queries = self.range_tombstone_block_load_count() as f64; + let hits = self.range_tombstone_block_load_cached_count() as f64; + + if queries == 0.0 { + 1.0 + } else { + hits / queries + } + } + /// Block cache efficiency in percent (0.0 - 1.0). pub fn block_cache_hit_rate(&self) -> f64 { let queries = self.block_loads() as f64; @@ -210,3 +250,69 @@ impl Metrics { self.io_skipped_by_filter.load(Relaxed) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::Ordering::Relaxed; + + #[test] + fn range_tombstone_counters_default_zero() { + let m = Metrics::default(); + assert_eq!(0, m.range_tombstone_block_load_count()); + assert_eq!(0, m.range_tombstone_block_load_cached_count()); + assert_eq!(0, m.range_tombstone_block_io()); + } + + #[test] + fn range_tombstone_block_load_count_sums_cached_and_io() { + let m = Metrics::default(); + m.range_tombstone_block_load_cached.store(3, Relaxed); + m.range_tombstone_block_load_io.store(7, Relaxed); + assert_eq!(10, m.range_tombstone_block_load_count()); + } + + #[test] + fn range_tombstone_cache_hit_rate_no_loads_returns_one() { + let m = Metrics::default(); + assert!((m.range_tombstone_block_cache_hit_rate() - 1.0).abs() < f64::EPSILON); + } + + #[test] + fn range_tombstone_cache_hit_rate_mixed_loads() { + let m = Metrics::default(); + m.range_tombstone_block_load_cached.store(3, Relaxed); + m.range_tombstone_block_load_io.store(1, Relaxed); + assert!((m.range_tombstone_block_cache_hit_rate() - 0.75).abs() < f64::EPSILON); + } + + #[test] + fn block_io_includes_range_tombstone() { + let m = Metrics::default(); + m.data_block_io_requested.store(10, Relaxed); + m.index_block_io_requested.store(20, Relaxed); + m.filter_block_io_requested.store(30, Relaxed); + m.range_tombstone_block_io_requested.store(40, Relaxed); + assert_eq!(100, m.block_io()); + } + + #[test] + fn block_load_io_count_includes_range_tombstone() { + let m = Metrics::default(); + m.data_block_load_io.store(1, Relaxed); + m.index_block_load_io.store(2, Relaxed); + m.filter_block_load_io.store(3, Relaxed); + m.range_tombstone_block_load_io.store(4, Relaxed); + assert_eq!(10, m.block_load_io_count()); + } + + #[test] + fn block_load_cached_count_includes_range_tombstone() { + let m = Metrics::default(); + m.data_block_load_cached.store(5, Relaxed); + m.index_block_load_cached.store(6, Relaxed); + m.filter_block_load_cached.store(7, Relaxed); + m.range_tombstone_block_load_cached.store(8, Relaxed); + assert_eq!(26, m.block_load_cached_count()); + } +} diff --git a/src/table/tests.rs b/src/table/tests.rs index 6df174d0f..91f82921b 100644 --- a/src/table/tests.rs +++ b/src/table/tests.rs @@ -1429,3 +1429,111 @@ fn table_global_seqno() -> crate::Result<()> { Ok(()) } + +/// Exercises the `load_block` cache-miss and cache-hit paths for +/// `BlockType::RangeTombstone`, verifying that the dedicated RT metrics +/// counters are incremented instead of the data-block counters. +#[test] +#[cfg(feature = "metrics")] +fn load_block_range_tombstone_metrics() -> crate::Result<()> { + use crate::{ + cache::Cache, + descriptor_table::DescriptorTable, + range_tombstone::RangeTombstone, + table::{block::BlockType, util::load_block}, + CompressionType, + }; + use std::sync::atomic::Ordering::Relaxed; + + let dir = tempdir()?; + let file = dir.path().join("table"); + + // Build a table that contains a range tombstone block. + let mut writer = Writer::new(file.clone(), 0, 0)?; + writer.write(InternalValue::from_components( + b"a", + b"v1", + 1, + crate::ValueType::Value, + ))?; + writer.write(InternalValue::from_components( + b"z", + b"v2", + 2, + crate::ValueType::Value, + ))?; + writer.write_range_tombstone(RangeTombstone::new(b"b".into(), b"y".into(), 3)); + #[expect( + clippy::unwrap_used, + reason = "finish() returns Some after writing data items" + )] + let (_, checksum) = writer.finish()?.unwrap(); + + let metrics = Arc::new(crate::metrics::Metrics::default()); + + let table = Table::recover( + file, + checksum, + 0, + 0, + // Recovery bypasses load_block() (reads via Block::from_file() directly), + // so it intentionally does NOT increment block-load metrics — consistent + // with how filter and index recovery reads are handled. + Arc::new(Cache::with_capacity_bytes(10_000_000)), + Some(Arc::new(DescriptorTable::new(10))), + false, + false, + #[cfg(feature = "metrics")] + metrics.clone(), + )?; + + let rt_handle = table + .regions + .range_tombstones + .expect("table should have range tombstone block"); + + let table_id = table.global_id(); + + // Recovery does NOT increment block-load counters (bypasses load_block). + assert_eq!(0, metrics.range_tombstone_block_load_io.load(Relaxed)); + + // Use a fresh cache so the first load_block() call is a cache miss. + let fresh_cache = Arc::new(Cache::with_capacity_bytes(10_000_000)); + + // load_block cache miss → IO path + let _block = load_block( + table_id, + &table.path, + &table.file_accessor, + &fresh_cache, + &rt_handle, + BlockType::RangeTombstone, + CompressionType::None, + #[cfg(feature = "metrics")] + &metrics, + )?; + + assert_eq!(1, metrics.range_tombstone_block_load_io.load(Relaxed)); + assert_eq!(0, metrics.range_tombstone_block_load_cached.load(Relaxed)); + assert!(metrics.range_tombstone_block_io_requested.load(Relaxed) > 0); + assert_eq!(0, metrics.data_block_load_io.load(Relaxed)); + + // load_block cache hit (block was inserted into fresh_cache by previous call) + let _block = load_block( + table_id, + &table.path, + &table.file_accessor, + &fresh_cache, + &rt_handle, + BlockType::RangeTombstone, + CompressionType::None, + #[cfg(feature = "metrics")] + &metrics, + )?; + + assert_eq!(1, metrics.range_tombstone_block_load_io.load(Relaxed)); + assert_eq!(1, metrics.range_tombstone_block_load_cached.load(Relaxed)); + assert_eq!(0, metrics.data_block_load_cached.load(Relaxed)); + + Ok(()) +} diff --git a/src/table/util.rs b/src/table/util.rs index c4aa913aa..a558e9c82 100644 --- a/src/table/util.rs +++ b/src/table/util.rs @@ -60,9 +60,12 @@ pub fn load_block( BlockType::Index => { metrics.index_block_load_cached.fetch_add(1, Relaxed); } - // TODO(#34): RangeTombstone counted under data_block metrics — add - // dedicated range_tombstone_block_load_cached/miss counters - BlockType::Data | BlockType::Meta | BlockType::RangeTombstone => { + BlockType::RangeTombstone => { + metrics + .range_tombstone_block_load_cached + .fetch_add(1, Relaxed); + } + BlockType::Data | BlockType::Meta => { metrics.data_block_load_cached.fetch_add(1, Relaxed); } } @@ -109,8 +112,14 @@ pub fn load_block( .index_block_io_requested .fetch_add(handle.size().into(), Relaxed); } - // TODO(#34): same as above — RangeTombstone uses data_block IO counters - BlockType::Data | BlockType::Meta | BlockType::RangeTombstone => { + BlockType::RangeTombstone => { + metrics.range_tombstone_block_load_io.fetch_add(1, Relaxed); + + metrics + .range_tombstone_block_io_requested + .fetch_add(handle.size().into(), Relaxed); + } + BlockType::Data | BlockType::Meta => { metrics.data_block_load_io.fetch_add(1, Relaxed); metrics