moka-rs/moka

Statistics for metrics

yanns opened this issue · 3 comments

yanns commented

I'm wondering if we can expose metrics about a Cache, like caffeine does.

Currently, it's difficult to observe the impact of tuning the cache size and the eviction policies.

Hi. Currently Cache does not provide built-in statistics. I tried to implement it a while ago but did not have time to finish it. I will try again in the future.

For now, please collect the statistics by yourself. Here is an example:

use std::sync::atomic::{AtomicU64, Ordering};

use moka::sync::Cache;
use once_cell::sync::Lazy;

fn main() {
    // To record the number of evictions, create an eviction listener.
    let listener = |_k, _v, cause| {
        use moka::notification::RemovalCause;

        // RemovalCause::Size means that the cache reached its maximum capacity
        // and had to evict an entry.
        //
        // For other causes, please see:
        // https://docs.rs/moka/*/moka/notification/enum.RemovalCause.html
        if cause == RemovalCause::Size {
            CACHE_STATS.record_eviction();
        }
    };

    // Create a cache with the eviction listener.
    let cache = Cache::builder()
        .max_capacity(100)
        .eviction_listener(listener)
        .build();

    // Insert 1000 entries into the cache with the max capacity of 100.
    for i in 0..1000 {
        get_or_insert(&cache, i % 250);
    }

    // For the sake of this example, we will call sync() to ensure that all
    // pending tasks (e.g. evictions) are processed. In a real application, you
    // do not have to call it as the Cache will periodically call it for you.
    use moka::sync::ConcurrentCacheExt;
    cache.sync();

    println!("{:?}", *CACHE_STATS);
    // => CacheStats { hits: 600, misses: 400, evictions: 300 }
}

fn get_or_insert(cache: &Cache<i32, i32>, key: i32) -> i32 {
    let entry = cache.entry(key).or_insert_with(|| key);

    if entry.is_fresh() {
        // The entry was just inserted into the cache.
        CACHE_STATS.record_miss();
    } else {
        // The entry was already in the cache.
        CACHE_STATS.record_hit();
    }

    entry.into_value()
}

static CACHE_STATS: Lazy<CacheStats> = Lazy::new(CacheStats::default);

#[derive(Debug, Default)]
pub struct CacheStats {
    hits: AtomicU64,
    misses: AtomicU64,
    evictions: AtomicU64,
}

impl CacheStats {
    pub fn record_hit(&self) {
        self.hits.fetch_add(1, Ordering::AcqRel);
    }

    pub fn record_miss(&self) {
        self.misses.fetch_add(1, Ordering::AcqRel);
    }

    pub fn record_eviction(&self) {
        self.evictions.fetch_add(1, Ordering::AcqRel);
    }
}
yanns commented

thanks for the detailed info @tatsuya6502
This is very useful! ❤️

I tried to implement it a while ago but did not have time to finish it. I will try again in the future.

I started to work on it via #262.