use crate::portability::{AtomicU64, Ordering};
use crate::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum Ticker {
BytesWritten = 0,
BytesRead = 1,
KeysWritten = 2,
KeysRead = 3,
KeysDeleted = 4,
RangeDeletesWritten = 5,
MergesWritten = 6,
BlockCacheHit = 7,
BlockCacheMiss = 8,
BlockCacheAdd = 9,
BloomFilterUseful = 10,
BloomFilterFullPositive = 11,
CompactionBytesRead = 12,
CompactionBytesWritten = 13,
CompactionCount = 14,
FlushBytesWritten = 15,
FlushCount = 16,
WalBytesWritten = 17,
WalSyncCount = 18,
IterSeekCount = 19,
IterNextCount = 20,
WriteStallMicros = 21,
SnapshotsRegistered = 22,
SnapshotsReleased = 23,
WalTailDiscarded = 24,
}
const NUM_TICKERS: usize = 25;
const ALL_TICKERS: &[Ticker] = &[
Ticker::BytesWritten,
Ticker::BytesRead,
Ticker::KeysWritten,
Ticker::KeysRead,
Ticker::KeysDeleted,
Ticker::RangeDeletesWritten,
Ticker::MergesWritten,
Ticker::BlockCacheHit,
Ticker::BlockCacheMiss,
Ticker::BlockCacheAdd,
Ticker::BloomFilterUseful,
Ticker::BloomFilterFullPositive,
Ticker::CompactionBytesRead,
Ticker::CompactionBytesWritten,
Ticker::CompactionCount,
Ticker::FlushBytesWritten,
Ticker::FlushCount,
Ticker::WalBytesWritten,
Ticker::WalSyncCount,
Ticker::IterSeekCount,
Ticker::IterNextCount,
Ticker::WriteStallMicros,
Ticker::SnapshotsRegistered,
Ticker::SnapshotsReleased,
Ticker::WalTailDiscarded,
];
impl Ticker {
pub fn name(&self) -> &'static str {
match self {
Ticker::BytesWritten => "regolith.bytes_written",
Ticker::BytesRead => "regolith.bytes_read",
Ticker::KeysWritten => "regolith.keys_written",
Ticker::KeysRead => "regolith.keys_read",
Ticker::KeysDeleted => "regolith.keys_deleted",
Ticker::RangeDeletesWritten => "regolith.range_deletes_written",
Ticker::MergesWritten => "regolith.merges_written",
Ticker::BlockCacheHit => "regolith.block_cache_hit",
Ticker::BlockCacheMiss => "regolith.block_cache_miss",
Ticker::BlockCacheAdd => "regolith.block_cache_add",
Ticker::BloomFilterUseful => "regolith.bloom_filter_useful",
Ticker::BloomFilterFullPositive => "regolith.bloom_filter_full_positive",
Ticker::CompactionBytesRead => "regolith.compaction_bytes_read",
Ticker::CompactionBytesWritten => "regolith.compaction_bytes_written",
Ticker::CompactionCount => "regolith.compaction_count",
Ticker::FlushBytesWritten => "regolith.flush_bytes_written",
Ticker::FlushCount => "regolith.flush_count",
Ticker::WalBytesWritten => "regolith.wal_bytes_written",
Ticker::WalSyncCount => "regolith.wal_sync_count",
Ticker::IterSeekCount => "regolith.iter_seek_count",
Ticker::IterNextCount => "regolith.iter_next_count",
Ticker::WriteStallMicros => "regolith.write_stall_micros",
Ticker::SnapshotsRegistered => "regolith.snapshots_registered",
Ticker::SnapshotsReleased => "regolith.snapshots_released",
Ticker::WalTailDiscarded => "regolith.wal_tail_discarded",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum Histogram {
DbGet = 0,
DbWrite = 1,
DbIterSeek = 2,
DbIterNext = 3,
CompactionTime = 4,
FlushTime = 5,
BlockReadTime = 6,
BytesPerRead = 7,
BytesPerWrite = 8,
WalWriteTime = 9,
}
const NUM_HISTOGRAMS: usize = 10;
const ALL_HISTOGRAMS: &[Histogram] = &[
Histogram::DbGet,
Histogram::DbWrite,
Histogram::DbIterSeek,
Histogram::DbIterNext,
Histogram::CompactionTime,
Histogram::FlushTime,
Histogram::BlockReadTime,
Histogram::BytesPerRead,
Histogram::BytesPerWrite,
Histogram::WalWriteTime,
];
impl Histogram {
pub fn name(&self) -> &'static str {
match self {
Histogram::DbGet => "regolith.db_get",
Histogram::DbWrite => "regolith.db_write",
Histogram::DbIterSeek => "regolith.db_iter_seek",
Histogram::DbIterNext => "regolith.db_iter_next",
Histogram::CompactionTime => "regolith.compaction_time",
Histogram::FlushTime => "regolith.flush_time",
Histogram::BlockReadTime => "regolith.block_read_time",
Histogram::BytesPerRead => "regolith.bytes_per_read",
Histogram::BytesPerWrite => "regolith.bytes_per_write",
Histogram::WalWriteTime => "regolith.wal_write_time",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HistogramSnapshot {
pub count: u64,
pub sum: u64,
pub min: u64,
pub max: u64,
}
impl HistogramSnapshot {
pub fn average(&self) -> u64 {
self.sum.checked_div(self.count).unwrap_or(0)
}
}
#[derive(Debug, Default)]
struct HistogramData {
count: u64,
sum: u64,
min: u64,
max: u64,
}
impl HistogramData {
fn record(&mut self, value: u64) {
if self.count == 0 {
self.min = value;
self.max = value;
} else {
if value < self.min {
self.min = value;
}
if value > self.max {
self.max = value;
}
}
self.count += 1;
self.sum = self.sum.saturating_add(value);
}
fn snapshot(&self) -> HistogramSnapshot {
HistogramSnapshot {
count: self.count,
sum: self.sum,
min: self.min,
max: self.max,
}
}
fn clear(&mut self) {
*self = HistogramData::default();
}
}
pub struct Statistics {
tickers: [AtomicU64; NUM_TICKERS],
histograms: [Mutex<HistogramData>; NUM_HISTOGRAMS],
}
impl std::fmt::Debug for Statistics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Statistics").finish_non_exhaustive()
}
}
impl Default for Statistics {
fn default() -> Self {
Self::new()
}
}
impl Statistics {
pub fn new() -> Self {
let tickers = std::array::from_fn(|_| AtomicU64::new(0));
let histograms = std::array::from_fn(|_| Mutex::new(HistogramData::default()));
Self {
tickers,
histograms,
}
}
pub fn get_ticker(&self, ticker: Ticker) -> u64 {
self.tickers[ticker as usize].load(Ordering::Relaxed)
}
pub fn get_histogram_snapshot(&self, hist: Histogram) -> HistogramSnapshot {
self.histograms[hist as usize].lock().snapshot()
}
pub fn reset(&self) {
for t in &self.tickers {
t.store(0, Ordering::Relaxed);
}
for h in &self.histograms {
h.lock().clear();
}
}
pub fn dump(&self) -> String {
let mut out = String::new();
out.push_str("-- tickers --\n");
for ticker in ALL_TICKERS {
out.push_str(&format!(
"{:40} {}\n",
ticker.name(),
self.tickers[*ticker as usize].load(Ordering::Relaxed)
));
}
out.push_str("-- histograms --\n");
for hist in ALL_HISTOGRAMS {
let snap = self.histograms[*hist as usize].lock().snapshot();
out.push_str(&format!(
"{:40} count={} sum={} min={} max={} avg={}\n",
hist.name(),
snap.count,
snap.sum,
snap.min,
snap.max,
snap.average(),
));
}
out
}
pub(crate) fn add(&self, ticker: Ticker, amount: u64) {
self.tickers[ticker as usize].fetch_add(amount, Ordering::Relaxed);
}
pub(crate) fn record(&self, hist: Histogram, value: u64) {
self.histograms[hist as usize].lock().record(value);
}
}
pub(crate) struct TimeScope<'a> {
start: Option<u64>,
stats: Option<&'a Statistics>,
hist: Histogram,
}
impl<'a> TimeScope<'a> {
pub(crate) fn new(stats: Option<&'a Statistics>, hist: Histogram) -> Self {
Self {
start: stats.and_then(|_| crate::env::platform_micros()),
stats,
hist,
}
}
}
impl Drop for TimeScope<'_> {
fn drop(&mut self) {
if let (Some(start), Some(stats), Some(now)) =
(self.start, self.stats, crate::env::platform_micros())
{
stats.record(self.hist, now.saturating_sub(start));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ticker_add_and_read() {
let s = Statistics::new();
assert_eq!(s.get_ticker(Ticker::BytesWritten), 0);
s.add(Ticker::BytesWritten, 100);
s.add(Ticker::BytesWritten, 50);
assert_eq!(s.get_ticker(Ticker::BytesWritten), 150);
}
#[test]
fn histogram_records_min_max_sum_count() {
let s = Statistics::new();
s.record(Histogram::DbGet, 10);
s.record(Histogram::DbGet, 20);
s.record(Histogram::DbGet, 5);
let snap = s.get_histogram_snapshot(Histogram::DbGet);
assert_eq!(snap.count, 3);
assert_eq!(snap.sum, 35);
assert_eq!(snap.min, 5);
assert_eq!(snap.max, 20);
assert_eq!(snap.average(), 11);
}
#[test]
fn reset_zeroes_everything() {
let s = Statistics::new();
s.add(Ticker::BytesRead, 42);
s.record(Histogram::FlushTime, 99);
s.reset();
assert_eq!(s.get_ticker(Ticker::BytesRead), 0);
let snap = s.get_histogram_snapshot(Histogram::FlushTime);
assert_eq!(snap, HistogramSnapshot::default());
}
#[test]
fn dump_contains_every_ticker_and_histogram_name() {
let s = Statistics::new();
let out = s.dump();
assert!(out.contains("regolith.bytes_written"));
assert!(out.contains("regolith.block_cache_hit"));
assert!(out.contains("regolith.db_get"));
assert!(out.contains("regolith.flush_time"));
}
#[test]
fn histogram_empty_snapshot_is_default() {
let s = Statistics::new();
assert_eq!(
s.get_histogram_snapshot(Histogram::DbGet),
HistogramSnapshot::default()
);
}
#[test]
fn time_scope_records_on_drop() {
let s = Statistics::new();
{
let _t = TimeScope::new(Some(&s), Histogram::DbGet);
std::thread::sleep(std::time::Duration::from_millis(1));
}
let snap = s.get_histogram_snapshot(Histogram::DbGet);
assert_eq!(snap.count, 1);
assert!(snap.sum > 0, "scope should have recorded non-zero micros");
}
#[test]
fn time_scope_disabled_is_noop() {
let _t = TimeScope::new(None, Histogram::DbGet);
}
}