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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Allow input, interned, and tracked structs to select 128-, 256-, 512-, or 1024-slot table pages
with the `page_size` macro option. The default remains 1024 slots.

## [0.27.2](https://github.com/salsa-rs/salsa/compare/salsa-v0.27.1...salsa-v0.27.2) - 2026-06-25

### Fixed
Expand Down
18 changes: 18 additions & 0 deletions book/src/tuning.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Tuning Salsa

## Table page sizes

Input, interned, and tracked structs can select their table page capacity in slots:

```rust
#[salsa::input(page_size = 128)]
struct Project {
name: String,
}
```

The supported capacities are `128`, `256`, `512`, and `1024`. Omitting the option is equivalent
to `page_size = 1024`. The option applies to Salsa structs, not tracked functions.

Smaller pages waste less capacity for sparse ingredients; larger pages use fewer page objects for
dense ingredients. Page size is part of the persisted ID encoding, so serialized databases cannot
be reused after changing it.

## Cache Eviction (LRU)

Salsa supports Least Recently Used (LRU) cache eviction for tracked functions.
Expand Down
3 changes: 3 additions & 0 deletions components/salsa-macro-rules/src/setup_input_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ macro_rules! setup_input_struct {
// The function used to implement `C::heap_size`.
heap_size_fn: $($heap_size_fn:path)?,

page_size: $page_size:ident,

// If `true`, `serialize_fn` and `deserialize_fn` have been provided.
persist: $persist:tt,

Expand Down Expand Up @@ -105,6 +107,7 @@ macro_rules! setup_input_struct {

const PERSIST: bool = $persist;

type PageSize = $zalsa::$page_size;
type Singleton = $zalsa::macro_if! {if $is_singleton {$zalsa::input::Singleton} else {$zalsa::input::NotSingleton}};

type Struct = $Struct;
Expand Down
3 changes: 3 additions & 0 deletions components/salsa-macro-rules/src/setup_interned_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ macro_rules! setup_interned_struct {
// The function used to implement `C::heap_size`.
heap_size_fn: $($heap_size_fn:path)?,

page_size: $page_size:ident,

// If `true`, `serialize_fn` and `deserialize_fn` have been provided.
persist: $persist:tt,

Expand Down Expand Up @@ -157,6 +159,7 @@ macro_rules! setup_interned_struct {
const DEBUG_NAME: &'static str = stringify!($Struct);
const PERSIST: bool = $persist;

type PageSize = $zalsa::$page_size;
$(
const REVISIONS: ::core::num::NonZeroUsize = ::core::num::NonZeroUsize::new($revisions).unwrap();
)?
Expand Down
2 changes: 2 additions & 0 deletions components/salsa-macro-rules/src/setup_tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ macro_rules! setup_tracked_fn {
const DEBUG_NAME: &'static str = concat!($(stringify!($self_ty), "::",)? stringify!($fn_name), "::interned_arguments");
const PERSIST: bool = $persist;

type PageSize = $zalsa::PageSize1024;

type Fields<$db_lt> = ($($interned_input_ty),*);

type Struct<$db_lt> = $InternedData<$db_lt>;
Expand Down
3 changes: 3 additions & 0 deletions components/salsa-macro-rules/src/setup_tracked_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ macro_rules! setup_tracked_struct {
// The function used to implement `C::heap_size`.
heap_size_fn: $($heap_size_fn:path)?,

page_size: $page_size:ident,

// If `true`, `serialize_fn` and `deserialize_fn` have been provided.
persist: $persist:tt,

Expand Down Expand Up @@ -147,6 +149,7 @@ macro_rules! setup_tracked_struct {

const PERSIST: bool = $persist;

type PageSize = $zalsa::$page_size;
type Fields<$db_lt> = ($($field_ty,)*);

type Revisions = [$zalsa::AtomicRevision; $N];
Expand Down
4 changes: 4 additions & 0 deletions components/salsa-macros/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `page_size = 128 | 256 | 512 | 1024` to input, interned, and tracked struct macros.

## [0.27.1](https://github.com/salsa-rs/salsa/compare/salsa-macros-v0.27.0...salsa-macros-v0.27.1) - 2026-06-24

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions components/salsa-macros/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ impl AllowedOptions for InputStruct {

const HEAP_SIZE: bool = true;

const PAGE_SIZE: bool = true;

const SELF_TY: bool = false;

const PERSIST: AllowedPersistOptions = AllowedPersistOptions::AllowedValue;
Expand Down Expand Up @@ -115,6 +117,7 @@ impl Macro {
let is_singleton = self.args.singleton.is_some();
let generate_debug_impl = salsa_struct.generate_debug_impl();
let heap_size_fn = self.args.heap_size_fn.iter();
let page_size = format_ident!("PageSize{}", self.args.page_size.unwrap_or(1024));

let persist = self.args.persist();
let serialize_fn = salsa_struct.serialize_fn();
Expand Down Expand Up @@ -148,6 +151,7 @@ impl Macro {
is_singleton: #is_singleton,
generate_debug_impl: #generate_debug_impl,
heap_size_fn: #(#heap_size_fn)*,
page_size: #page_size,
persist: #persist,
serialize_fn: #(#serialize_fn)*,
deserialize_fn: #(#deserialize_fn)*,
Expand Down
4 changes: 4 additions & 0 deletions components/salsa-macros/src/interned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ impl AllowedOptions for InternedStruct {

const HEAP_SIZE: bool = true;

const PAGE_SIZE: bool = true;

const SELF_TY: bool = false;

const PERSIST: AllowedPersistOptions = AllowedPersistOptions::AllowedValue;
Expand Down Expand Up @@ -138,6 +140,7 @@ impl Macro {
let deserialize_fn = salsa_struct.deserialize_fn();

let heap_size_fn = self.args.heap_size_fn.iter();
let page_size = format_ident!("PageSize{}", self.args.page_size.unwrap_or(1024));

let zalsa = self.hygiene.ident("zalsa");
let zalsa_struct = self.hygiene.ident("zalsa_struct");
Expand Down Expand Up @@ -176,6 +179,7 @@ impl Macro {
num_fields: #num_fields,
generate_debug_impl: #generate_debug_impl,
heap_size_fn: #(#heap_size_fn)*,
page_size: #page_size,
persist: #persist,
serialize_fn: #(#serialize_fn)*,
deserialize_fn: #(#deserialize_fn)*,
Expand Down
41 changes: 41 additions & 0 deletions components/salsa-macros/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ pub(crate) struct Options<A: AllowedOptions> {
/// If this is `Some`, the value is the provided `heap_size` function.
pub heap_size_fn: Option<syn::Path>,

/// The physical table page capacity in slots.
pub page_size: Option<usize>,

/// The `self_ty = <Ty>` option is used to set the the self type of the tracked impl for tracked
/// functions. This is merely used to refine the query name.
pub self_ty: Option<syn::Type>,
Expand Down Expand Up @@ -157,6 +160,7 @@ impl<A: AllowedOptions> Default for Options<A> {
id: Default::default(),
revisions: Default::default(),
heap_size_fn: Default::default(),
page_size: Default::default(),
self_ty: Default::default(),
persist: Default::default(),
}
Expand All @@ -182,6 +186,7 @@ pub(crate) trait AllowedOptions {
const ID: bool;
const REVISIONS: bool;
const HEAP_SIZE: bool;
const PAGE_SIZE: bool = false;
const SELF_TY: bool;
const PERSIST: AllowedPersistOptions;
}
Expand Down Expand Up @@ -519,6 +524,38 @@ impl<A: AllowedOptions> syn::parse::Parse for Options<A> {
"`heap_size` option not allowed here",
));
}
} else if ident == "page_size" {
if !A::PAGE_SIZE {
return Err(syn::Error::new(
ident.span(),
"`page_size` option not allowed here",
));
}
let _eq = Equals::parse(input)?;
let expr = syn::Expr::parse(input)?;
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = expr
else {
return Err(syn::Error::new(
expr.span(),
"`page_size` must be an integer literal",
));
};
let value = lit.base10_parse::<usize>()?;
if !matches!(value, 128 | 256 | 512 | 1024) {
return Err(syn::Error::new(
lit.span(),
"expected 128, 256, 512, or 1024 slots",
));
}
if options.page_size.replace(value).is_some() {
return Err(syn::Error::new(
ident.span(),
"option `page_size` provided twice",
));
}
} else if ident == "self_ty" {
if A::SELF_TY {
let _eq = Equals::parse(input)?;
Expand Down Expand Up @@ -572,6 +609,7 @@ impl<A: AllowedOptions> quote::ToTokens for Options<A> {
id,
revisions,
heap_size_fn,
page_size,
self_ty,
persist,
phantom: _,
Expand Down Expand Up @@ -627,6 +665,9 @@ impl<A: AllowedOptions> quote::ToTokens for Options<A> {
if let Some(heap_size_fn) = heap_size_fn {
tokens.extend(quote::quote! { heap_size = #heap_size_fn, });
}
if let Some(page_size) = page_size {
tokens.extend(quote::quote! { page_size = #page_size, });
}
if let Some(self_ty) = self_ty {
tokens.extend(quote::quote! { self_ty = #self_ty, });
}
Expand Down
4 changes: 4 additions & 0 deletions components/salsa-macros/src/tracked_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ impl AllowedOptions for TrackedStruct {

const HEAP_SIZE: bool = true;

const PAGE_SIZE: bool = true;

const SELF_TY: bool = false;

const PERSIST: AllowedPersistOptions = AllowedPersistOptions::AllowedValue;
Expand Down Expand Up @@ -149,6 +151,7 @@ impl Macro {
let deserialize_fn = salsa_struct.deserialize_fn();

let heap_size_fn = self.args.heap_size_fn.iter();
let page_size = format_ident!("PageSize{}", self.args.page_size.unwrap_or(1024));

let num_tracked_fields = salsa_struct.num_tracked_fields();
let generate_debug_impl = salsa_struct.generate_debug_impl();
Expand Down Expand Up @@ -199,6 +202,7 @@ impl Macro {
generate_debug_impl: #generate_debug_impl,

heap_size_fn: #(#heap_size_fn)*,
page_size: #page_size,

persist: #persist,
serialize_fn: #(#serialize_fn)*,
Expand Down
4 changes: 3 additions & 1 deletion src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,12 +415,14 @@ mod memory_usage {
let mut queries = HashMap::new();
let mut structs = Vec::new();
let mut page_infos = self.zalsa().table().page_infos();
let page_capacity = self.zalsa().table().page_capacity();

for input_ingredient in self.zalsa().ingredients() {
let Some(input_info) = input_ingredient.memory_usage(self) else {
continue;
};
let page_capacity = input_ingredient
.page_capacity()
.expect("struct ingredients must provide a table page capacity");

let mut size_of_fields = 0;
let mut size_of_metadata = 0;
Expand Down
5 changes: 3 additions & 2 deletions src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use crate::zalsa::Zalsa;
/// The `Id` of a salsa struct in the database [`Table`](`crate::table::Table`).
///
/// The high-order bits of an `Id` store a 32-bit generation counter, while
/// the low-order bits pack a [`PageIndex`](`crate::table::PageIndex`) and
/// [`SlotIndex`](`crate::table::SlotIndex`) within the page.
/// the low-order bits pack a page-size class, a [`PageIndex`](`crate::table::PageIndex`), and
/// [`SlotIndex`](`crate::table::SlotIndex`) within the page. The page-size class is selected by
/// the Salsa struct's `page_size` option.
///
/// The low-order bits of `Id` are a `u32` ranging from `0..Id::MAX_U32`.
/// The maximum range is smaller than a standard `u32` to leave
Expand Down
6 changes: 6 additions & 0 deletions src/ingredient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ pub trait Ingredient: Any + fmt::Debug + Send + Sync {
None
}

/// Returns the table page capacity for struct ingredients.
#[cfg(feature = "salsa_unstable")]
fn page_capacity(&self) -> Option<usize> {
None
}

/// Whether this ingredient will be persisted with the database.
fn is_persistable(&self) -> bool {
false
Expand Down
14 changes: 13 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub trait Configuration: Any {
/// Whether this struct should be persisted with the database.
const PERSIST: bool;

/// Physical table page-size policy.
#[doc(hidden)]
type PageSize: crate::table::PageSize;

/// The singleton state for this input if any.
type Singleton: SingletonChoice + Send + Sync;

Expand Down Expand Up @@ -366,6 +370,11 @@ impl<C: Configuration> Ingredient for IngredientImpl<C> {
Some(memory_usage)
}

#[cfg(feature = "salsa_unstable")]
fn page_capacity(&self) -> Option<usize> {
Some(crate::table::page_capacity::<C::PageSize>())
}

fn is_persistable(&self) -> bool {
C::PERSIST
}
Expand Down Expand Up @@ -473,6 +482,8 @@ unsafe impl<C> Slot for Value<C>
where
C: Configuration,
{
type PageSize = C::PageSize;

#[inline(always)]
unsafe fn memos(
this: *const Self,
Expand Down Expand Up @@ -610,7 +621,8 @@ mod persistence {

while let Some((id, value)) = access.next_entry::<u64, DeserializeValue<C>>()? {
let id = Id::from_bits(id);
let (page_idx, _) = crate::table::split_id(id);
let (page_idx, _) = crate::table::try_split_typed_id::<C::PageSize>(id)
.ok_or_else(|| de::Error::custom("serialized ID has the wrong page size"))?;

let value = Value::<C> {
fields: value.fields.0,
Expand Down
15 changes: 14 additions & 1 deletion src/interned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub trait Configuration: Sized + 'static {
/// Whether this struct should be persisted with the database.
const PERSIST: bool;

/// Physical table page-size policy.
#[doc(hidden)]
type PageSize: crate::table::PageSize;

// The minimum number of revisions that must pass before a stale value is garbage collected.
const REVISIONS: NonZeroUsize = NonZeroUsize::new(DEFAULT_REVISIONS).unwrap();

Expand Down Expand Up @@ -1045,6 +1049,11 @@ where
Some(memory_usage)
}

#[cfg(feature = "salsa_unstable")]
fn page_capacity(&self) -> Option<usize> {
Some(crate::table::page_capacity::<C::PageSize>())
}

fn is_persistable(&self) -> bool {
C::PERSIST
}
Expand Down Expand Up @@ -1096,6 +1105,8 @@ unsafe impl<C> Slot for Value<C>
where
C: Configuration,
{
type PageSize = C::PageSize;

#[inline(always)]
unsafe fn memos(
this: *const Self,
Expand Down Expand Up @@ -1690,7 +1701,8 @@ mod persistence {

while let Some((id, value)) = access.next_entry::<u64, DeserializeValue<C>>()? {
let id = Id::from_bits(id);
let (page_idx, _) = crate::table::split_id(id);
let (page_idx, _) = crate::table::try_split_typed_id::<C::PageSize>(id)
.ok_or_else(|| de::Error::custom("serialized ID has the wrong page size"))?;

// Determine the value shard.
let hash = ingredient.hasher.hash_one(&value.fields.0);
Expand Down Expand Up @@ -1788,6 +1800,7 @@ mod tests {
const PERSIST: bool = false;
const REVISIONS: NonZeroUsize = NonZeroUsize::new(2).unwrap();

type PageSize = crate::table::PageSize1024;
type Fields<'db> = ();
type Struct<'db> = Id;

Expand Down
Loading