Skip to content

Commit b8cdd30

Browse files
authored
add an option to tune interned garbage collection (#911)
1 parent 6ced42b commit b8cdd30

21 files changed

+247
-62
lines changed

components/salsa-macro-rules/src/setup_interned_struct.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ macro_rules! setup_interned_struct {
2828
// the salsa ID
2929
id: $Id:path,
3030

31+
// The minimum number of revisions to keep the value interned.
32+
revisions: $($revisions:expr)?,
33+
3134
// the lifetime used in the desugared interned struct.
3235
// if the `db_lt_arg`, is present, this is `db_lt_arg`, but otherwise,
3336
// it is `'static`.
@@ -129,6 +132,9 @@ macro_rules! setup_interned_struct {
129132
line: line!(),
130133
};
131134
const DEBUG_NAME: &'static str = stringify!($Struct);
135+
$(
136+
const REVISIONS: ::core::num::NonZeroUsize = ::core::num::NonZeroUsize::new($revisions).unwrap();
137+
)?
132138
type Fields<'a> = $StructDataIdent<'a>;
133139
type Struct<'db> = $Struct< $($db_lt_arg)? >;
134140
}

components/salsa-macros/src/accumulator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ impl AllowedOptions for Accumulator {
4444
const LRU: bool = false;
4545
const CONSTRUCTOR_NAME: bool = false;
4646
const ID: bool = false;
47+
const REVISIONS: bool = false;
4748
}
4849

4950
struct StructMacro {

components/salsa-macros/src/input.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ impl crate::options::AllowedOptions for InputStruct {
6262
const CONSTRUCTOR_NAME: bool = true;
6363

6464
const ID: bool = false;
65+
66+
const REVISIONS: bool = false;
6567
}
6668

6769
impl SalsaStructAllowedOptions for InputStruct {

components/salsa-macros/src/interned.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ impl crate::options::AllowedOptions for InternedStruct {
6262
const CONSTRUCTOR_NAME: bool = true;
6363

6464
const ID: bool = true;
65+
66+
const REVISIONS: bool = true;
6567
}
6668

6769
impl SalsaStructAllowedOptions for InternedStruct {
@@ -105,6 +107,7 @@ impl Macro {
105107
let generate_debug_impl = salsa_struct.generate_debug_impl();
106108
let has_lifetime = salsa_struct.generate_lifetime();
107109
let id = salsa_struct.id();
110+
let revisions = salsa_struct.revisions();
108111

109112
let (db_lt_arg, cfg, interior_lt) = if has_lifetime {
110113
(
@@ -140,6 +143,7 @@ impl Macro {
140143
db_lt: #db_lt,
141144
db_lt_arg: #db_lt_arg,
142145
id: #id,
146+
revisions: #(#revisions)*,
143147
interior_lt: #interior_lt,
144148
new_fn: #new_fn,
145149
field_options: [#(#field_options),*],

components/salsa-macros/src/options.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ pub(crate) struct Options<A: AllowedOptions> {
9393
/// If this is `Some`, the value is the `<ident>`.
9494
pub id: Option<syn::Path>,
9595

96+
/// The `revisions = <usize>` option is used to set the minimum number of revisions
97+
/// to keep a value interned.
98+
///
99+
/// This is stored as a `syn::Expr` to support `usize::MAX`.
100+
pub revisions: Option<syn::Expr>,
101+
96102
/// Remember the `A` parameter, which plays no role after parsing.
97103
phantom: PhantomData<A>,
98104
}
@@ -116,6 +122,7 @@ impl<A: AllowedOptions> Default for Options<A> {
116122
lru: Default::default(),
117123
singleton: Default::default(),
118124
id: Default::default(),
125+
revisions: Default::default(),
119126
}
120127
}
121128
}
@@ -137,6 +144,7 @@ pub(crate) trait AllowedOptions {
137144
const LRU: bool;
138145
const CONSTRUCTOR_NAME: bool;
139146
const ID: bool;
147+
const REVISIONS: bool;
140148
}
141149

142150
type Equals = syn::Token![=];
@@ -368,6 +376,22 @@ impl<A: AllowedOptions> syn::parse::Parse for Options<A> {
368376
"`id` option not allowed here",
369377
));
370378
}
379+
} else if ident == "revisions" {
380+
if A::REVISIONS {
381+
let _eq = Equals::parse(input)?;
382+
let expr = syn::Expr::parse(input)?;
383+
if let Some(old) = options.revisions.replace(expr) {
384+
return Err(syn::Error::new(
385+
old.span(),
386+
"option `revisions` provided twice",
387+
));
388+
}
389+
} else {
390+
return Err(syn::Error::new(
391+
ident.span(),
392+
"`revisions` option not allowed here",
393+
));
394+
}
371395
} else {
372396
return Err(syn::Error::new(
373397
ident.span(),

components/salsa-macros/src/salsa_struct.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ where
133133
}
134134
}
135135

136+
/// Returns the `revisions` in `Options` as an optional iterator.
137+
pub(crate) fn revisions(&self) -> impl Iterator<Item = &syn::Expr> + '_ {
138+
self.args.revisions.iter()
139+
}
140+
136141
/// Disallow `#[tracked]` attributes on the fields of this struct.
137142
///
138143
/// If an `#[tracked]` field is found, return an error.

components/salsa-macros/src/tracked_fn.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ impl crate::options::AllowedOptions for TrackedFn {
5555
const CONSTRUCTOR_NAME: bool = false;
5656

5757
const ID: bool = false;
58+
59+
const REVISIONS: bool = false;
5860
}
5961

6062
struct Macro {

components/salsa-macros/src/tracked_struct.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ impl crate::options::AllowedOptions for TrackedStruct {
5757
const CONSTRUCTOR_NAME: bool = true;
5858

5959
const ID: bool = false;
60+
61+
const REVISIONS: bool = false;
6062
}
6163

6264
impl SalsaStructAllowedOptions for TrackedStruct {

src/interned.rs

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::cell::{Cell, UnsafeCell};
33
use std::fmt;
44
use std::hash::{BuildHasher, Hash, Hasher};
55
use std::marker::PhantomData;
6+
use std::num::NonZeroUsize;
67
use std::path::{Path, PathBuf};
78

89
use crossbeam_utils::CachePadded;
@@ -31,6 +32,13 @@ pub trait Configuration: Sized + 'static {
3132

3233
const DEBUG_NAME: &'static str;
3334

35+
// The minimum number of revisions that must pass before a stale value is garbage collected.
36+
#[cfg(test)]
37+
const REVISIONS: NonZeroUsize = NonZeroUsize::new(3).unwrap();
38+
39+
#[cfg(not(test))] // More aggressive garbage collection by default when testing.
40+
const REVISIONS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
41+
3442
/// The fields of the struct being interned.
3543
type Fields<'db>: InternedData;
3644

@@ -63,7 +71,7 @@ pub struct IngredientImpl<C: Configuration> {
6371
shards: Box<[CachePadded<Mutex<IngredientShard<C>>>]>,
6472

6573
/// A queue of recent revisions in which values were interned.
66-
revision_queue: RevisionQueue,
74+
revision_queue: RevisionQueue<C>,
6775

6876
memo_table_types: Arc<MemoTableTypes>,
6977

@@ -158,7 +166,12 @@ struct ValueShared {
158166

159167
impl ValueShared {
160168
/// Returns `true` if this value slot can be reused when interning, and should be added to the LRU.
161-
fn is_reusable(&self) -> bool {
169+
fn is_reusable<C: Configuration>(&self) -> bool {
170+
// Garbage collection is disabled.
171+
if C::REVISIONS == IMMORTAL {
172+
return false;
173+
}
174+
162175
// Collecting higher durability values requires invalidating the revision for their
163176
// durability (see `Database::synthetic_write`, which requires a mutable reference to
164177
// the database) to avoid short-circuiting calls to `maybe_changed_after`. This is
@@ -337,7 +350,7 @@ where
337350
})
338351
});
339352

340-
if value_shared.is_reusable() {
353+
if value_shared.is_reusable::<C>() {
341354
// Move the value to the front of the LRU list.
342355
//
343356
// SAFETY: We hold the lock for the shard containing the value, and `value` is
@@ -351,14 +364,14 @@ where
351364
}
352365

353366
if let Some((_, stamp)) = zalsa_local.active_query() {
354-
let was_reusable = value_shared.is_reusable();
367+
let was_reusable = value_shared.is_reusable::<C>();
355368

356369
// Record the maximum durability across all queries that intern this value.
357370
value_shared.durability = std::cmp::max(value_shared.durability, stamp.durability);
358371

359372
// If the value is no longer reusable, i.e. the durability increased, remove it
360373
// from the LRU.
361-
if was_reusable && !value_shared.is_reusable() {
374+
if was_reusable && !value_shared.is_reusable::<C>() {
362375
// SAFETY: We hold the lock for the shard containing the value, and `value`
363376
// was previously reusable, so is in the list.
364377
unsafe { shard.lru.cursor_mut_from_ptr(value).remove() };
@@ -507,7 +520,7 @@ where
507520
// correct type.
508521
unsafe { self.clear_memos(zalsa, &mut memo_table, new_id) };
509522

510-
if value_shared.is_reusable() {
523+
if value_shared.is_reusable::<C>() {
511524
// Move the value to the front of the LRU list.
512525
//
513526
// SAFETY: The value pointer is valid for the lifetime of the database.
@@ -580,7 +593,7 @@ where
580593
// SAFETY: We hold the lock for the shard containing the value.
581594
let value_shared = unsafe { &mut *value.shared.get() };
582595

583-
if value_shared.is_reusable() {
596+
if value_shared.is_reusable::<C>() {
584597
// Add the value to the front of the LRU list.
585598
//
586599
// SAFETY: The value pointer is valid for the lifetime of the database
@@ -855,35 +868,48 @@ impl<Fields: Send + Sync + 'static> Slot for Value<Fields> {
855868
}
856869
}
857870

858-
#[cfg(not(test))]
859-
const REVS: usize = 3;
860-
861-
#[cfg(test)] // Aggressively reuse slots in tests.
862-
const REVS: usize = 1;
863-
864871
/// Keep track of revisions in which interned values were read, to determine staleness.
865872
///
866873
/// An interned value is considered stale if it has not been read in the past `REVS`
867874
/// revisions. However, we only consider revisions in which interned values were actually
868875
/// read, as revisions may be created in bursts.
869-
struct RevisionQueue {
876+
struct RevisionQueue<C> {
870877
lock: Mutex<()>,
871-
revisions: [AtomicRevision; REVS],
878+
// Once `feature(generic_const_exprs)` is stable this can just be an array.
879+
revisions: Box<[AtomicRevision]>,
880+
_configuration: PhantomData<fn() -> C>,
872881
}
873882

874-
impl Default for RevisionQueue {
875-
fn default() -> RevisionQueue {
883+
// `#[salsa::interned(revisions = usize::MAX)]` disables garbage collection.
884+
const IMMORTAL: NonZeroUsize = NonZeroUsize::MAX;
885+
886+
impl<C: Configuration> Default for RevisionQueue<C> {
887+
fn default() -> RevisionQueue<C> {
888+
let revisions = if C::REVISIONS == IMMORTAL {
889+
Box::default()
890+
} else {
891+
(0..C::REVISIONS.get())
892+
.map(|_| AtomicRevision::start())
893+
.collect()
894+
};
895+
876896
RevisionQueue {
877897
lock: Mutex::new(()),
878-
revisions: [const { AtomicRevision::start() }; REVS],
898+
revisions,
899+
_configuration: PhantomData,
879900
}
880901
}
881902
}
882903

883-
impl RevisionQueue {
904+
impl<C: Configuration> RevisionQueue<C> {
884905
/// Record the given revision as active.
885906
#[inline]
886907
fn record(&self, revision: Revision) {
908+
// Garbage collection is disabled.
909+
if C::REVISIONS == IMMORTAL {
910+
return;
911+
}
912+
887913
// Fast-path: We already recorded this revision.
888914
if self.revisions[0].load() >= revision {
889915
return;
@@ -899,7 +925,7 @@ impl RevisionQueue {
899925
// Otherwise, update the queue, maintaining sorted order.
900926
//
901927
// Note that this should only happen once per revision.
902-
for i in (1..REVS).rev() {
928+
for i in (1..C::REVISIONS.get()).rev() {
903929
self.revisions[i].store(self.revisions[i - 1].load());
904930
}
905931

@@ -909,7 +935,12 @@ impl RevisionQueue {
909935
/// Returns `true` if the given revision is old enough to be considered stale.
910936
#[inline]
911937
fn is_stale(&self, revision: Revision) -> bool {
912-
let oldest = self.revisions[REVS - 1].load();
938+
// Garbage collection is disabled.
939+
if C::REVISIONS == IMMORTAL {
940+
return false;
941+
}
942+
943+
let oldest = self.revisions[C::REVISIONS.get() - 1].load();
913944

914945
// If we have not recorded `REVS` revisions yet, nothing can be stale.
915946
if oldest == Revision::start() {
@@ -919,10 +950,16 @@ impl RevisionQueue {
919950
revision < oldest
920951
}
921952

922-
/// Returns `true` if `REVS` revisions have been recorded as active.
953+
/// Returns `true` if `C::REVISIONS` revisions have been recorded as active,
954+
/// i.e. enough data has been recorded to start garbage collection.
923955
#[inline]
924956
fn is_primed(&self) -> bool {
925-
self.revisions[REVS - 1].load() > Revision::start()
957+
// Garbage collection is disabled.
958+
if C::REVISIONS == IMMORTAL {
959+
return false;
960+
}
961+
962+
self.revisions[C::REVISIONS.get() - 1].load() > Revision::start()
926963
}
927964
}
928965

tests/compile-fail/accumulator_incompatibles.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ struct AccWithRecover(u32);
1919
#[salsa::accumulator(lru = 12)]
2020
struct AccWithLru(u32);
2121

22+
#[salsa::accumulator(revisions = 12)]
23+
struct AccWithRevisions(u32);
24+
2225
#[salsa::accumulator(constructor = Constructor)]
2326
struct AccWithConstructor(u32);
2427

0 commit comments

Comments
 (0)