keyvaluedb 0.1.8

Key-Value Database Wrapper
Documentation
//! Generic statistics for key-value databases

use std::collections::HashMap;

/// Statistic kind to query.
pub enum Kind {
    /// Overall statistics since start.
    Overall,
    /// Statistics since previous query.
    SincePrevious,
}

/// Statistic for the `span` period
#[derive(Debug, Clone)]
pub struct IoStats {
    /// Number of transaction.
    pub transactions: u64,
    /// Number of read operations.
    pub reads: u64,
    /// Number of reads resulted in a read from cache.
    pub cache_reads: u64,
    /// Number of write operations.
    pub writes: u64,
    /// Number of bytes read
    pub bytes_read: u64,
    /// Number of bytes read from cache
    pub cache_read_bytes: u64,
    /// Number of bytes write
    pub bytes_written: u64,
    /// Number of delete operations.
    pub deletes: u64,
    /// Number of prefix (batch) delete operations.
    pub prefix_deletes: u64,
    /// Write size buckets. Keys are write sizes, slightly rounded upwards.
    /// Values are the number of times a value of a particular size was written.
    pub write_size_buckets: HashMap<usize, u64>,
    /// Similar to `write_size_buckets` but tracks total sizes of transactions
    /// and the average duration for a transaction of that size. The duration is
    /// in **microseconds**.
    pub tx_write_size_buckets: HashMap<usize, (u64, f64)>,
    /// Start of the statistic period in **microseconds** since epoch.
    pub started: u64,
    /// Total duration of the statistic period.
    pub span: std::time::Duration,
}

impl IoStats {
    /// Empty statistic report.
    pub fn empty() -> Self {
        Self {
            transactions: 0,
            reads: 0,
            cache_reads: 0,
            writes: 0,
            bytes_read: 0,
            cache_read_bytes: 0,
            bytes_written: 0,
            deletes: 0,
            prefix_deletes: 0,
            write_size_buckets: HashMap::new(),
            tx_write_size_buckets: HashMap::new(),
            started: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map_or(Default::default(), |d| d.as_micros() as u64),
            span: std::time::Duration::default(),
        }
    }

    /// Record a write operation with a given size.
    pub fn record_write(&mut self, size: usize) {
        self.writes += 1;
        self.bytes_written += size as u64;

        let size_bucket = estimate_bucket_size(size);
        self.write_size_buckets
            .entry(size_bucket)
            .and_modify(|x| *x += 1)
            .or_insert(1);
    }

    /// Record a transaction operation with a given size.
    /// This tracks sizes of all values within transactions rather than individual writes.
    pub fn record_tx_write(&mut self, size: usize, duration_micros: f64) {
        let size_bucket = estimate_bucket_size(size);
        self.tx_write_size_buckets
            .entry(size_bucket)
            .and_modify(|(count, avg_duration)| {
                *avg_duration =
                    (*avg_duration * (*count as f64) + duration_micros) / ((*count as f64) + 1.);
                *count += 1;
            })
            .or_insert((1, duration_micros));
    }

    /// Average batch (transaction) size (writes per transaction)
    pub fn avg_batch_size(&self) -> f64 {
        if self.writes == 0 {
            return 0.0;
        }
        self.transactions as f64 / self.writes as f64
    }

    /// Read operations per second.
    pub fn reads_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        self.reads as f64 / self.span.as_secs_f64()
    }

    pub fn byte_reads_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        self.bytes_read as f64 / self.span.as_secs_f64()
    }

    /// Write operations per second.
    pub fn writes_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        self.writes as f64 / self.span.as_secs_f64()
    }

    pub fn byte_writes_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        self.bytes_written as f64 / self.span.as_secs_f64()
    }

    /// Total number of operations per second.
    pub fn ops_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        (self.writes as f64 + self.reads as f64) / self.span.as_secs_f64()
    }

    /// Transactions per second.
    pub fn transactions_per_sec(&self) -> f64 {
        if self.span.as_secs_f64() == 0.0 {
            return 0.0;
        }

        (self.transactions as f64) / self.span.as_secs_f64()
    }

    pub fn avg_transaction_size(&self) -> f64 {
        if self.transactions == 0 {
            return 0.0;
        }

        self.bytes_written as f64 / self.transactions as f64
    }

    pub fn cache_hit_ratio(&self) -> f64 {
        if self.reads == 0 {
            return 0.0;
        }

        self.cache_reads as f64 / self.reads as f64
    }
}

fn estimate_bucket_size(size: usize) -> usize {
    if size == 0 {
        return 0;
    }

    // Bucket size granularity for particular `size`
    let step = (size.next_power_of_two() / 16).max(128);

    // Round up to the nearest step
    size.div_ceil(step) * step
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_record_write() {
        assert_eq!(estimate_bucket_size(100), 128);
        assert_eq!(estimate_bucket_size(200), 256);
        assert_eq!(estimate_bucket_size(1000), 1024);
        assert_eq!(estimate_bucket_size(2500), 2560);
        assert_eq!(estimate_bucket_size(5100), 5120);
        assert_eq!(estimate_bucket_size(9000), 9216);
        assert_eq!(estimate_bucket_size(9900), 10240);
    }
}