Skip to content
Draft
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
91 changes: 63 additions & 28 deletions cachebox/_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ class Cache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -429,10 +433,11 @@ class Cache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -592,6 +597,10 @@ class FIFOCache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -611,10 +620,11 @@ class FIFOCache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -807,6 +817,10 @@ class RRCache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -826,10 +840,11 @@ class RRCache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -1015,6 +1030,10 @@ class LRUCache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -1034,10 +1053,11 @@ class LRUCache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -1264,6 +1284,10 @@ class LFUCache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -1283,10 +1307,11 @@ class LFUCache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -1472,6 +1497,10 @@ class TTLCache(BaseCacheImpl[KT, VT]):
Note:
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.
"""
...

Expand All @@ -1491,10 +1520,11 @@ class TTLCache(BaseCacheImpl[KT, VT]):
factory: The factory to call and get default value from if ``key`` is not in the cache.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down Expand Up @@ -1734,6 +1764,10 @@ class VTTLCache(BaseCacheImpl[KT, VT]):
Use `setdefault_with`, if computing the value is expensive or has side
effectes.

`getsizeof` runs with the internal lock released; if another thread
inserts the key meanwhile, that value wins, and if the losing
`getsizeof` raises, its exception still propagates to this caller.

Args:
key: The key to look up or insert.
default: The value to insert if ``key`` is not in the cache.
Expand All @@ -1760,10 +1794,11 @@ class VTTLCache(BaseCacheImpl[KT, VT]):
ttl: An optional time-to-live duration for item.

Warning:
if two threads miss the same key at once, `factory` can run
more than once; the value inserted first wins and is returned to
both. If `factory` raises, nothing is inserted and the exception
propagates.
if two threads miss the same key at once, `factory` (and
`getsizeof`) can run more than once; the value inserted first wins
and is returned to both callers that succeed. If the losing call's
`factory` or `getsizeof` raises, nothing more is inserted and the
exception still propagates to that caller.
"""
...

Expand Down
12 changes: 12 additions & 0 deletions src/internal/lazyheap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,18 @@ impl<T> LazyHeap<T> {
self.is_sorted = true;
}

/// Moves every element out into `out`, without sorting.
///
/// The heap is empty and considered sorted after this call.
#[inline]
pub fn drain_into(&mut self, out: &mut Vec<T>) {
out.reserve(self.data.len());
while let Some(element) = self.unlink_back() {
out.push(element);
}
self.is_sorted = true;
}

/// Shrinks the backing buffer's capacity as close to its current length
/// as possible.
#[inline]
Expand Down
21 changes: 19 additions & 2 deletions src/policies/fifopolicy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl traits::VacantExt for Vacant<'_> {

#[inline]
fn evict(&mut self) -> pyo3::PyResult<()> {
self.policy.evict(self.shared)?;
let handle = self.policy.evict(self.shared)?;
self.policy.pending_drops.push(handle);
Ok(())
}

Expand Down Expand Up @@ -120,6 +121,10 @@ pub struct FIFOPolicy {
/// Running total of all stored handles' sizes, maintained incrementally.
currsize: usize,

/// Handles parked for destruction after the lock is released;
/// see [`super::traits::PolicyExt::pending_drops`].
pending_drops: Vec<Handle>,

Comment on lines +124 to +127

@chirizxc chirizxc Aug 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should be:

pending_drops: smallvec::SmallVec<[Handle; 1]>  // or [Handle; 2]

Changing it to smallvec::SmallVec<[Handle; 1]> (or [Handle; 2]) would eliminate the allocation for single evictions / insert-replaces the most common scenario (insert, setdefault, remove) and would degrade to Vec-like behavior only during clear() or drain(n) with a large n

What do you think about this? @awolverp

/// Number of handles ever popped from the front of [`FIFOPolicy::entries`].
///
/// Because [`VecDeque`] indices shift on front-removal, naively keeping
Expand All @@ -146,6 +151,7 @@ impl FIFOPolicy {
table: hashbrown::raw::RawTable::with_capacity(capacity),
entries: VecDeque::with_capacity(capacity),
currsize: 0,
pending_drops: Vec::new(),
front_offset: 0,
}
}
Expand Down Expand Up @@ -314,6 +320,16 @@ impl PolicyExt for FIFOPolicy {
Ok(front)
}

#[inline(always)]
fn pending_drops(&mut self) -> &mut Vec<Handle> {
&mut self.pending_drops
}

#[inline(always)]
fn len(&self) -> usize {
self.entries.len()
}

#[inline]
fn shrink_to_fit(&mut self, shared: &Self::Shared) {
shared.generation_version().increment();
Expand All @@ -332,7 +348,7 @@ impl PolicyExt for FIFOPolicy {

shared.generation_version().increment();
self.table.clear();
self.entries.clear();
self.pending_drops.extend(self.entries.drain(..));
self.currsize = 0;
self.front_offset = 0;
}
Expand Down Expand Up @@ -392,6 +408,7 @@ impl PolicyExt for FIFOPolicy {
table: self.table.clone(),
entries,
currsize: self.currsize,
pending_drops: Vec::new(),
front_offset: self.front_offset,
}
}
Expand Down
21 changes: 19 additions & 2 deletions src/policies/lfupolicy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ impl traits::VacantExt for Vacant<'_> {

#[inline]
fn evict(&mut self) -> pyo3::PyResult<()> {
self.policy.evict(self.shared)?;
let handle = self.policy.evict(self.shared)?;
self.policy.pending_drops.push(handle);
Ok(())
}

Expand All @@ -226,6 +227,10 @@ pub struct LFUPolicy {

/// Running total of all stored handles' sizes, maintained incrementally.
currsize: usize,

/// Handles parked for destruction after the lock is released;
/// see [`super::traits::PolicyExt::pending_drops`].
pending_drops: Vec<FrequencyHandle>,
}

impl LFUPolicy {
Expand All @@ -238,6 +243,7 @@ impl LFUPolicy {
table: hashbrown::raw::RawTable::with_capacity(capacity),
heap: lazyheap::LazyHeap::new(),
currsize: 0,
pending_drops: Vec::new(),
}
}

Expand Down Expand Up @@ -388,14 +394,24 @@ impl PolicyExt for LFUPolicy {
Ok(handle)
}

#[inline(always)]
fn pending_drops(&mut self) -> &mut Vec<FrequencyHandle> {
&mut self.pending_drops
}

#[inline(always)]
fn len(&self) -> usize {
self.heap.len()
}

fn clear(&mut self, shared: &Self::Shared) {
if self.heap.is_empty() {
return;
}

shared.generation_version().increment();
self.table.clear_no_drop();
self.heap.clear();
self.heap.drain_into(&mut self.pending_drops);
self.currsize = 0;
}

Expand Down Expand Up @@ -472,6 +488,7 @@ impl PolicyExt for LFUPolicy {
table,
heap,
currsize: self.currsize,
pending_drops: Vec::new(),
}
}

Expand Down
24 changes: 22 additions & 2 deletions src/policies/lrupolicy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ impl traits::VacantExt for Vacant<'_> {

#[inline]
fn evict(&mut self) -> pyo3::PyResult<()> {
self.policy.evict(self.shared)?;
let handle = self.policy.evict(self.shared)?;
self.policy.pending_drops.push(handle);
Ok(())
}

Expand All @@ -102,6 +103,10 @@ pub struct LRUPolicy {

/// Running total of all stored handles' sizes, maintained incrementally.
currsize: usize,

/// Handles parked for destruction after the lock is released;
/// see [`super::traits::PolicyExt::pending_drops`].
pending_drops: Vec<Handle>,
}

impl LRUPolicy {
Expand All @@ -114,6 +119,7 @@ impl LRUPolicy {
table: hashbrown::raw::RawTable::with_capacity(capacity),
list: linked_list::LinkedList::new(),
currsize: 0,
pending_drops: Vec::new(),
}
}

Expand Down Expand Up @@ -240,6 +246,16 @@ impl PolicyExt for LRUPolicy {
Ok(handle)
}

#[inline(always)]
fn pending_drops(&mut self) -> &mut Vec<Handle> {
&mut self.pending_drops
}

#[inline(always)]
fn len(&self) -> usize {
self.list.len()
}

#[inline]
fn shrink_to_fit(&mut self, _shared: &Self::Shared) {
self.table
Expand All @@ -254,7 +270,10 @@ impl PolicyExt for LRUPolicy {

shared.generation_version().increment();
self.table.clear_no_drop();
self.list.clear();
self.pending_drops.reserve(self.list.len());
while let Some(handle) = self.list.pop_front() {
self.pending_drops.push(handle);
}
self.currsize = 0;
}

Expand Down Expand Up @@ -319,6 +338,7 @@ impl PolicyExt for LRUPolicy {
table,
list: entries,
currsize: self.currsize,
pending_drops: Vec::new(),
}
}

Expand Down
Loading