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
50 changes: 40 additions & 10 deletions book/src/tuning.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
# Tuning Salsa

## LRU Cache
## Cache Eviction (LRU)

You can specify an LRU cache size for any non-input query:
Salsa supports Least Recently Used (LRU) cache eviction for tracked functions.
By default, memoized values are never evicted (unbounded cache). You can enable
LRU eviction by specifying a capacity at compile time:

```rs
let lru_capacity: usize = 128;
base_db::ParseQuery.in_db_mut(self).set_lru_capacity(lru_capacity);
```rust
#[salsa::tracked(lru = 128)]
fn parse(db: &dyn Db, input: SourceFile) -> Ast {
// ...
}
```

The default is `0`, which disables LRU-caching entirely.
With `lru = 128`, Salsa will keep at most 128 memoized values for this function.
When the cache exceeds this capacity, the least recently used values are evicted
at the start of each new revision.

Note that there is no garbage collection for keys and
results of old queries, so LRU caches are currently the
only knob available for avoiding unbounded memory usage
for long-running apps built on Salsa.
### Zero-Cost When Disabled

When no `lru` capacity is specified (the default), Salsa uses a no-op eviction
policy that is completely optimized away by the compiler. This means there is
zero runtime overhead for functions that don't need cache eviction.

### Runtime Capacity Adjustment

For functions with LRU enabled, you can adjust the capacity at runtime:

```rust
#[salsa::tracked(lru = 128)]
fn my_query(db: &dyn Db, input: MyInput) -> Output {
// ...
}

// Later, adjust the capacity:
my_query::set_lru_capacity(db, 256);
```

**Note:** The `set_lru_capacity` method is only generated for functions that have
an `lru` attribute. Functions without LRU enabled do not have this method.

### Memory Management

There is no garbage collection for keys and results of old queries, so LRU caches
are currently the primary mechanism for avoiding unbounded memory usage in
long-running applications built on Salsa.

## Intern Queries

Expand Down
26 changes: 14 additions & 12 deletions components/salsa-macro-rules/src/setup_tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ macro_rules! setup_tracked_fn {
// The function used to implement `C::heap_size`.
heap_size_fn: $($heap_size_fn:path)?,

// The eviction policy type for this function
eviction: $Eviction:ty,

// LRU capacity (a literal, maybe 0)
lru: $lru:tt,

Expand Down Expand Up @@ -284,6 +287,8 @@ macro_rules! setup_tracked_fn {

type Output<$db_lt> = $output_ty;

type Eviction = $Eviction;

const CYCLE_STRATEGY: $zalsa::CycleRecoveryStrategy = $zalsa::CycleRecoveryStrategy::$cycle_recovery_strategy;

$($values_equal)+
Expand Down Expand Up @@ -452,18 +457,15 @@ macro_rules! setup_tracked_fn {
}
}

$zalsa::macro_if! { if0 $lru { } else {
/// Sets the lru capacity
///
/// **WARNING:** Just like an ordinary write, this method triggers
/// cancellation. If you invoke it while a snapshot exists, it
/// will block until that snapshot is dropped -- if that snapshot
/// is owned by the current thread, this could trigger deadlock.
#[allow(dead_code)]
fn set_lru_capacity(db: &mut dyn $Db, value: usize) {
$Configuration::fn_ingredient_mut(db).set_capacity(value);
}
} }
/// Sets the lru capacity
///
/// **WARNING:** Just like an ordinary write, this method triggers
/// cancellation. If you invoke it while a snapshot exists, it
/// will block until that snapshot is dropped -- if that snapshot
/// is owned by the current thread, this could trigger deadlock.
fn set_lru_capacity(db: &mut dyn $Db, value: usize) where for<'trivial_bounds> $Eviction: $zalsa::function::HasCapacity {
$Configuration::fn_ingredient_mut(db).set_capacity(value);
}
}

$zalsa::attach($db, || {
Expand Down
8 changes: 8 additions & 0 deletions components/salsa-macros/src/tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ impl Macro {

let lru = Literal::usize_unsuffixed(self.args.lru.unwrap_or(0));

// Determine the eviction policy type based on whether LRU capacity is specified
let eviction_type = if self.args.lru.is_some() {
quote!(::salsa::plumbing::function::Lru)
} else {
quote!(::salsa::plumbing::function::NoopEviction)
};

let return_mode = self
.args
.returns
Expand Down Expand Up @@ -222,6 +229,7 @@ impl Macro {
values_equal: {#eq},
needs_interner: #needs_interner,
heap_size_fn: #(#heap_size_fn)*,
eviction: #eviction_type,
lru: #lru,
return_mode: #return_mode,
persist: #persist,
Expand Down
24 changes: 17 additions & 7 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ mod accumulated;
mod backdate;
mod delete;
mod diff_outputs;
mod eviction;
mod execute;
mod fetch;
mod inputs;
mod lru;
mod maybe_changed_after;
mod memo;
mod specify;
mod sync;

pub use eviction::{EvictionPolicy, HasCapacity, Lru, NoopEviction};

pub type Memo<C> = memo::Memo<'static, C>;

pub trait Configuration: Any {
Expand All @@ -58,6 +60,9 @@ pub trait Configuration: Any {
/// The value computed by the function.
type Output<'db>: Send + Sync;

/// The eviction policy for this function's memoized values.
type Eviction: EvictionPolicy;

/// Determines whether this function can recover from being a participant in a cycle
/// (and, if so, how).
const CYCLE_STRATEGY: CycleRecoveryStrategy;
Expand Down Expand Up @@ -168,8 +173,9 @@ pub struct IngredientImpl<C: Configuration> {
/// tracked function's struct is a plain salsa struct or an enum `#[derive(Supertype)]`.
memo_ingredient_indices: <C::SalsaStruct<'static> as SalsaStructInDb>::MemoIngredientMap,

/// Eviction policy - type determined by Configuration.
/// Used to find memos to throw out when we have too many memoized values.
lru: lru::Lru,
eviction: C::Eviction,

/// An downcaster to `C::DbView`.
///
Expand Down Expand Up @@ -203,12 +209,12 @@ where
pub fn new(
index: IngredientIndex,
memo_ingredient_indices: <C::SalsaStruct<'static> as SalsaStructInDb>::MemoIngredientMap,
lru: usize,
eviction_capacity: usize,
) -> Self {
Self {
index,
memo_ingredient_indices,
lru: lru::Lru::new(lru),
eviction: C::Eviction::new(eviction_capacity),
deleted_entries: Default::default(),
view_caster: OnceLock::new(),
sync_table: SyncTable::new(index),
Expand All @@ -233,8 +239,12 @@ where
DatabaseKeyIndex::new(self.index, key)
}

pub fn set_capacity(&mut self, capacity: usize) {
self.lru.set_capacity(capacity);
/// Set eviction capacity. Only available when eviction policy supports it.
pub fn set_capacity(&mut self, capacity: usize)
where
C::Eviction: HasCapacity,
{
self.eviction.set_capacity(capacity);
}

/// Returns a reference to the memo value that lives as long as self.
Expand Down Expand Up @@ -475,7 +485,7 @@ where
}

fn reset_for_new_revision(&mut self, table: &mut Table) {
self.lru.for_each_evicted(|evict| {
self.eviction.for_each_evicted(|evict| {
let ingredient_index = table.ingredient_index(evict);
Self::evict_value_from_memo_for(
table.memos_mut(evict),
Expand Down
40 changes: 40 additions & 0 deletions src/function/eviction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Pluggable cache eviction strategies for memoized function values.
//!
//! This module provides the [`EvictionPolicy`] trait that allows different
//! eviction strategies to be used for salsa tracked functions.

mod lru;
mod noop;

pub use lru::Lru;
pub use noop::NoopEviction;

use crate::Id;

/// Trait for cache eviction strategies.
///
/// Implementations control when memoized values are evicted from the cache.
/// The eviction policy is selected at compile time via the `Configuration` trait.
pub trait EvictionPolicy: Send + Sync {
/// Create a new eviction policy with the given capacity.
fn new(capacity: usize) -> Self;

/// Record that an item was accessed.
fn record_use(&self, id: Id);

/// Set the maximum capacity.
fn set_capacity(&mut self, capacity: usize);

/// Iterate over items that should be evicted.
///
/// Called once per revision during `reset_for_new_revision`.
/// The callback `cb` should be invoked for each item to evict.
fn for_each_evicted(&mut self, cb: impl FnMut(Id));
}

/// Marker trait for eviction policies that have a configurable capacity.
///
/// This trait is used to conditionally generate the `set_lru_capacity` method
/// on tracked functions. Only policies that implement this trait will expose
/// runtime capacity configuration.
pub trait HasCapacity: EvictionPolicy {}
66 changes: 66 additions & 0 deletions src/function/eviction/lru.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Least Recently Used (LRU) eviction policy.
//!
//! This policy tracks the most recently accessed items and evicts
//! the least recently used ones when the cache exceeds its capacity.

use std::num::NonZeroUsize;

use crate::hash::FxLinkedHashSet;
use crate::sync::Mutex;
use crate::Id;

use super::{EvictionPolicy, HasCapacity};

/// Least Recently Used eviction policy.
///
/// When the number of memoized values exceeds the configured capacity,
/// the least recently accessed values are evicted at the start of each
/// new revision.
pub struct Lru {
capacity: Option<NonZeroUsize>,
set: Mutex<FxLinkedHashSet<Id>>,
}

impl Lru {
#[inline(never)]
fn insert(&self, id: Id) {
self.set.lock().insert(id);
}
}

impl EvictionPolicy for Lru {
fn new(cap: usize) -> Self {
Self {
capacity: NonZeroUsize::new(cap),
set: Mutex::default(),
}
}

#[inline(always)]
fn record_use(&self, id: Id) {
if self.capacity.is_some() {
self.insert(id);
}
}

fn set_capacity(&mut self, capacity: usize) {
self.capacity = NonZeroUsize::new(capacity);
if self.capacity.is_none() {
self.set.get_mut().clear();
}
}

fn for_each_evicted(&mut self, mut cb: impl FnMut(Id)) {
let Some(cap) = self.capacity else {
return;
};
let set = self.set.get_mut();
while set.len() > cap.get() {
if let Some(id) = set.pop_front() {
cb(id);
}
}
}
}

impl HasCapacity for Lru {}
23 changes: 23 additions & 0 deletions src/function/eviction/noop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! No-op eviction policy - cache grows unbounded.
//!
//! This is the default eviction policy when no LRU capacity is specified.

use crate::{function::EvictionPolicy, Id};

/// No eviction - cache grows unbounded.
pub struct NoopEviction;

impl EvictionPolicy for NoopEviction {
fn new(_cap: usize) -> Self {
Self
}

#[inline(always)]
fn record_use(&self, _id: Id) {}

#[inline(always)]
fn set_capacity(&mut self, _capacity: usize) {}

#[inline(always)]
fn for_each_evicted(&mut self, _cb: impl FnMut(Id)) {}
}
3 changes: 2 additions & 1 deletion src/function/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use rustc_hash::FxHashMap;

use crate::cycle::{CycleHeads, CycleRecoveryStrategy, IterationCount};
use crate::function::eviction::EvictionPolicy;
use crate::function::maybe_changed_after::VerifyCycleHeads;
use crate::function::memo::Memo;
use crate::function::sync::ClaimResult;
Expand Down Expand Up @@ -33,7 +34,7 @@ where
// SAFETY: We just refreshed the memo so it is guaranteed to contain a value now.
let memo_value = unsafe { memo.value.as_ref().unwrap_unchecked() };

self.lru.record_use(id);
self.eviction.record_use(id);

zalsa_local.report_tracked_read(
database_key_index,
Expand Down
Loading
Loading