Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Version 0.7.0

- **Breaking change**: The type of the `max_capacity` has been changed from `usize`
to `u64`. This was necessary to have the weight-based cache management consistent
across different CPU architectures.

### Added

- Add support for weight-based (size aware) cache management.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,13 @@ $ RUSTFLAGS='--cfg skeptic --cfg trybuild' cargo test \
- [x] `async` optimized caches. (`v0.2.0`)
- [x] Bounding a cache with weighted size of entry.
(`v0.7.0` via [#24](https://github.com/moka-rs/moka/pull/24))
- [ ] API stabilization. (Smaller core API, shorter names for frequently used
methods)
- [ ] API stabilization. (Smaller core cache API, shorter names for frequently
used methods)
- e.g.
- `get(&Q)` → `get_if_present(&Q)`
- `get_or_insert_with(K, F)` → `get(K, F)`
- `get_or_try_insert_with(K, F)` → `try_get(K, F)`
- `blocking_insert(K, V)` → `blocking().insert(K, V)`.
- `get(&Q)` → `get_if_present(&Q)`
- `blocking_insert(K, V)` → `blocking().insert(K, V)`
- `time_to_live()` → `config().time_to_live()`
- [ ] Cache statistics. (Hit rate, etc.)
- [ ] Notifications on eviction, etc.
Expand Down
7 changes: 7 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::convert::TryInto;

pub(crate) mod builder_utils;
pub(crate) mod deque;
pub(crate) mod error;
Expand All @@ -14,3 +16,8 @@ pub(crate) mod unsafe_weak_pointer;
pub(crate) mod atomic_time;

pub(crate) mod time;

// Ensures the value fits in a range of `128u32..=u32::MAX`.
pub(crate) fn sketch_capacity(max_capacity: u64) -> u32 {
max_capacity.try_into().unwrap_or(u32::MAX).max(128)
}
51 changes: 34 additions & 17 deletions src/common/frequency_sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
/// A probabilistic multi-set for estimating the popularity of an element within
/// a time window. The maximum frequency of an element is limited to 15 (4-bits)
/// and an aging process periodically halves the popularity of all elements.
#[derive(Default)]
pub(crate) struct FrequencySketch {
sample_size: u32,
table_mask: u64,
Expand Down Expand Up @@ -67,8 +68,11 @@ static ONE_MASK: u64 = 0x1111_1111_1111_1111;
// -------------------------------------------------------------------------------

impl FrequencySketch {
/// Creates a frequency sketch with the capacity.
pub(crate) fn with_capacity(cap: u32) -> Self {
/// Initializes and increases the capacity of this `FrequencySketch` instance,
/// if necessary, to ensure that it can accurately estimate the popularity of
/// elements given the maximum size of the cache. This operation forgets all
/// previous counts when resizing.
pub(crate) fn ensure_capacity(&mut self, cap: u32) {
// The max byte size of the table, Box<[u64; table_size]>
//
// | Pointer width | Max size |
Expand All @@ -91,24 +95,27 @@ impl FrequencySketch {
} else {
maximum.next_power_of_two()
};
let table = vec![0; table_size as usize].into_boxed_slice();
let table_mask = 0.max(table_size - 1) as u64;
let sample_size = if cap == 0 {

if self.table.len() as u32 >= table_size {
return;
}

self.table = vec![0; table_size as usize].into_boxed_slice();
self.table_mask = 0.max(table_size - 1) as u64;
self.sample_size = if cap == 0 {
10
} else {
maximum.saturating_mul(10).min(i32::MAX as u32)
};
Self {
sample_size,
table_mask,
table,
size: 0,
}
}

/// Takes the hash value of an element, and returns the estimated number of
/// occurrences of the element, up to the maximum (15).
pub(crate) fn frequency(&self, hash: u64) -> u8 {
if self.table.is_empty() {
return 0;
}

let start = ((hash & 3) << 2) as u8;
let mut frequency = std::u8::MAX;
for i in 0..4 {
Expand All @@ -125,6 +132,10 @@ impl FrequencySketch {
/// exceeds a threshold. This process provides a frequency aging to allow
/// expired long term entries to fade away.
pub(crate) fn increment(&mut self, hash: u64) {
if self.table.is_empty() {
return;
}

let start = ((hash & 3) << 2) as u8;
let mut added = false;
for i in 0..4 {
Expand Down Expand Up @@ -201,7 +212,8 @@ mod tests {
// This test was ported from Caffeine.
#[test]
fn increment_once() {
let mut sketch = FrequencySketch::with_capacity(512);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(512);
let hasher = hasher();
let item_hash = hasher(*ITEM);
sketch.increment(item_hash);
Expand All @@ -211,7 +223,8 @@ mod tests {
// This test was ported from Caffeine.
#[test]
fn increment_max() {
let mut sketch = FrequencySketch::with_capacity(512);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(512);
let hasher = hasher();
let item_hash = hasher(*ITEM);
for _ in 0..20 {
Expand All @@ -223,7 +236,8 @@ mod tests {
// This test was ported from Caffeine.
#[test]
fn increment_distinct() {
let mut sketch = FrequencySketch::with_capacity(512);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(512);
let hasher = hasher();
sketch.increment(hasher(*ITEM));
sketch.increment(hasher(ITEM.wrapping_add(1)));
Expand All @@ -235,7 +249,8 @@ mod tests {
// This test was ported from Caffeine.
#[test]
fn index_of_around_zero() {
let sketch = FrequencySketch::with_capacity(512);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(512);
let mut indexes = std::collections::HashSet::new();
let hashes = vec![std::u64::MAX, 0, 1];
for hash in hashes.iter() {
Expand All @@ -250,7 +265,8 @@ mod tests {
#[test]
fn reset() {
let mut reset = false;
let mut sketch = FrequencySketch::with_capacity(64);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(64);
let hasher = hasher();

for i in 1..(20 * sketch.table.len() as u32) {
Expand All @@ -268,7 +284,8 @@ mod tests {
// This test was ported from Caffeine.
#[test]
fn heavy_hitters() {
let mut sketch = FrequencySketch::with_capacity(65_536);
let mut sketch = FrequencySketch::default();
sketch.ensure_capacity(65_536);
let hasher = hasher();

for i in 100..100_000 {
Expand Down
6 changes: 3 additions & 3 deletions src/future/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use std::{
/// ```
///
pub struct CacheBuilder<K, V, C> {
max_capacity: Option<usize>,
max_capacity: Option<u64>,
initial_capacity: Option<usize>,
weigher: Option<Weigher<K, V>>,
time_to_live: Option<Duration>,
Expand Down Expand Up @@ -84,7 +84,7 @@ where
{
/// Construct a new `CacheBuilder` that will be used to build a `Cache` holding
/// up to `max_capacity` entries.
pub fn new(max_capacity: usize) -> Self {
pub fn new(max_capacity: u64) -> Self {
Self {
max_capacity: Some(max_capacity),
..Default::default()
Expand Down Expand Up @@ -138,7 +138,7 @@ where

impl<K, V, C> CacheBuilder<K, V, C> {
/// Sets the max capacity of the cache.
pub fn max_capacity(self, max_capacity: usize) -> Self {
pub fn max_capacity(self, max_capacity: u64) -> Self {
Self {
max_capacity: Some(max_capacity),
..self
Expand Down
50 changes: 34 additions & 16 deletions src/future/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ where
/// `time_to_live`, use the [`CacheBuilder`][builder-struct].
///
/// [builder-struct]: ./struct.CacheBuilder.html
pub fn new(max_capacity: usize) -> Self {
pub fn new(max_capacity: u64) -> Self {
let build_hasher = RandomState::default();
Self::with_everything(
Some(max_capacity),
Expand Down Expand Up @@ -344,7 +344,7 @@ where
S: BuildHasher + Clone + Send + Sync + 'static,
{
pub(crate) fn with_everything(
max_capacity: Option<usize>,
max_capacity: Option<u64>,
initial_capacity: Option<usize>,
build_hasher: S,
weigher: Option<Weigher<K, V>>,
Expand Down Expand Up @@ -699,6 +699,16 @@ where
pub fn num_segments(&self) -> usize {
1
}

#[cfg(test)]
fn estimated_entry_count(&self) -> u64 {
self.base.estimated_entry_count()
}

#[cfg(test)]
fn weighted_size(&self) -> u64 {
self.base.weighted_size()
}
}

impl<K, V, S> ConcurrentCacheExt<K, V> for Cache<K, V, S>
Expand Down Expand Up @@ -839,15 +849,11 @@ where
impl<K, V, S> Cache<K, V, S>
where
K: Hash + Eq + Send + Sync + 'static,
V: Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
S: BuildHasher + Clone + Send + Sync + 'static,
{
fn is_table_empty(&self) -> bool {
self.table_size() == 0
}

fn table_size(&self) -> usize {
self.base.table_size()
self.estimated_entry_count() == 0
}

fn invalidation_predicate_count(&self) -> usize {
Expand Down Expand Up @@ -1027,11 +1033,23 @@ mod tests {
assert_eq!(cache.get(&"c"), None);
assert_eq!(cache.get(&"d"), Some(dennis));

// Update "b" with "bill" (w: 20). This should evict "d" (w: 15).
// Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15).
cache.insert("b", bill).await;
cache.sync();
assert_eq!(cache.get(&"b"), Some(bill));
assert_eq!(cache.get(&"d"), None);

// Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15).
cache.insert("a", alice).await;
cache.insert("b", bob).await;
cache.sync();
assert_eq!(cache.get(&"a"), Some(alice));
assert_eq!(cache.get(&"b"), Some(bob));
assert_eq!(cache.get(&"d"), None);

// Verify the sizes.
assert_eq!(cache.estimated_entry_count(), 2);
assert_eq!(cache.weighted_size(), 25);
}

#[tokio::test]
Expand Down Expand Up @@ -1140,7 +1158,7 @@ mod tests {
assert_eq!(cache.get(&1), Some("bob"));
// This should survive as it was inserted after calling invalidate_entries_if.
assert_eq!(cache.get(&3), Some("alice"));
assert_eq!(cache.table_size(), 2);
assert_eq!(cache.estimated_entry_count(), 2);
assert_eq!(cache.invalidation_predicate_count(), 0);

mock.increment(Duration::from_secs(5)); // 15 secs from the start.
Expand All @@ -1157,7 +1175,7 @@ mod tests {

assert!(cache.get(&1).is_none());
assert!(cache.get(&3).is_none());
assert_eq!(cache.table_size(), 0);
assert_eq!(cache.estimated_entry_count(), 0);
assert_eq!(cache.invalidation_predicate_count(), 0);

Ok(())
Expand Down Expand Up @@ -1194,13 +1212,13 @@ mod tests {
cache.insert("b", "bob").await;
cache.sync();

assert_eq!(cache.table_size(), 1);
assert_eq!(cache.estimated_entry_count(), 1);

mock.increment(Duration::from_secs(5)); // 15 secs.
cache.sync();

assert_eq!(cache.get(&"b"), Some("bob"));
assert_eq!(cache.table_size(), 1);
assert_eq!(cache.estimated_entry_count(), 1);

cache.insert("b", "bill").await;
cache.sync();
Expand All @@ -1209,7 +1227,7 @@ mod tests {
cache.sync();

assert_eq!(cache.get(&"b"), Some("bill"));
assert_eq!(cache.table_size(), 1);
assert_eq!(cache.estimated_entry_count(), 1);

mock.increment(Duration::from_secs(5)); // 25 secs
cache.sync();
Expand Down Expand Up @@ -1247,14 +1265,14 @@ mod tests {
cache.insert("b", "bob").await;
cache.sync();

assert_eq!(cache.table_size(), 2);
assert_eq!(cache.estimated_entry_count(), 2);

mock.increment(Duration::from_secs(5)); // 15 secs.
cache.sync();

assert_eq!(cache.get(&"a"), None);
assert_eq!(cache.get(&"b"), Some("bob"));
assert_eq!(cache.table_size(), 1);
assert_eq!(cache.estimated_entry_count(), 1);

mock.increment(Duration::from_secs(10)); // 25 secs
cache.sync();
Expand Down
Loading