Auto merge of #17639 - Veykril:salsa-perf, r=Veykril

Some more small salsa memory improvements

This does limit our lru limits to 2^16 but if you want to set them higher than that you might as well not set them at all. Also makes `LRU` opt-in per query now, allowing us to drop all the unnecessary LRU stuff for most queries
This commit is contained in:
bors 2024-07-19 18:45:16 +00:00
commit b333f85a9d
18 changed files with 1240 additions and 402 deletions

View file

@ -42,9 +42,9 @@ pub trait Upcast<T: ?Sized> {
fn upcast(&self) -> &T;
}
pub const DEFAULT_FILE_TEXT_LRU_CAP: usize = 16;
pub const DEFAULT_PARSE_LRU_CAP: usize = 128;
pub const DEFAULT_BORROWCK_LRU_CAP: usize = 2024;
pub const DEFAULT_FILE_TEXT_LRU_CAP: u16 = 16;
pub const DEFAULT_PARSE_LRU_CAP: u16 = 128;
pub const DEFAULT_BORROWCK_LRU_CAP: u16 = 2024;
pub trait FileLoader {
/// Text of the file.
@ -59,6 +59,7 @@ pub trait FileLoader {
#[salsa::query_group(SourceDatabaseStorage)]
pub trait SourceDatabase: FileLoader + std::fmt::Debug {
/// Parses the file into the syntax tree.
#[salsa::lru]
fn parse(&self, file_id: EditionedFileId) -> Parse<ast::SourceFile>;
/// Returns the set of errors obtained from parsing the file including validation errors.
@ -105,6 +106,7 @@ pub trait SourceDatabaseExt: SourceDatabase {
#[salsa::input]
fn compressed_file_text(&self, file_id: FileId) -> Arc<[u8]>;
#[salsa::lru]
fn file_text(&self, file_id: FileId) -> Arc<str>;
/// Path to a file, relative to the root of its source root.

View file

@ -65,7 +65,7 @@ pub trait ExpandDatabase: SourceDatabase {
#[salsa::transparent]
fn parse_or_expand_with_err(&self, file_id: HirFileId) -> ExpandResult<Parse<SyntaxNode>>;
/// Implementation for the macro case.
// This query is LRU cached
#[salsa::lru]
fn parse_macro_expansion(
&self,
macro_file: MacroFileId,

View file

@ -61,6 +61,7 @@ pub trait HirDatabase: DefDatabase + Upcast<dyn DefDatabase> {
) -> Result<Arc<MirBody>, MirLowerError>;
#[salsa::invoke(crate::mir::borrowck_query)]
#[salsa::lru]
fn borrowck(&self, def: DefWithBodyId) -> Result<Arc<[BorrowckResult]>, MirLowerError>;
#[salsa::invoke(crate::consteval::const_eval_query)]

View file

@ -145,7 +145,7 @@ impl Default for RootDatabase {
}
impl RootDatabase {
pub fn new(lru_capacity: Option<usize>) -> RootDatabase {
pub fn new(lru_capacity: Option<u16>) -> RootDatabase {
let mut db = RootDatabase { storage: ManuallyDrop::new(salsa::Storage::default()) };
db.set_crate_graph_with_durability(Default::default(), Durability::HIGH);
db.set_proc_macros_with_durability(Default::default(), Durability::HIGH);
@ -161,7 +161,7 @@ impl RootDatabase {
self.set_expand_proc_attr_macros_with_durability(true, Durability::HIGH);
}
pub fn update_base_query_lru_capacities(&mut self, lru_capacity: Option<usize>) {
pub fn update_base_query_lru_capacities(&mut self, lru_capacity: Option<u16>) {
let lru_capacity = lru_capacity.unwrap_or(base_db::DEFAULT_PARSE_LRU_CAP);
base_db::FileTextQuery.in_db_mut(self).set_lru_capacity(DEFAULT_FILE_TEXT_LRU_CAP);
base_db::ParseQuery.in_db_mut(self).set_lru_capacity(lru_capacity);
@ -170,7 +170,7 @@ impl RootDatabase {
hir::db::BorrowckQuery.in_db_mut(self).set_lru_capacity(base_db::DEFAULT_BORROWCK_LRU_CAP);
}
pub fn update_lru_capacities(&mut self, lru_capacities: &FxHashMap<Box<str>, usize>) {
pub fn update_lru_capacities(&mut self, lru_capacities: &FxHashMap<Box<str>, u16>) {
use hir::db as hir_db;
base_db::FileTextQuery.in_db_mut(self).set_lru_capacity(DEFAULT_FILE_TEXT_LRU_CAP);
@ -192,135 +192,6 @@ impl RootDatabase {
.copied()
.unwrap_or(base_db::DEFAULT_BORROWCK_LRU_CAP),
);
macro_rules! update_lru_capacity_per_query {
($( $module:ident :: $query:ident )*) => {$(
if let Some(&cap) = lru_capacities.get(stringify!($query)) {
$module::$query.in_db_mut(self).set_lru_capacity(cap);
}
)*}
}
update_lru_capacity_per_query![
// SourceDatabase
// base_db::ParseQuery
// base_db::CrateGraphQuery
// base_db::ProcMacrosQuery
// SourceDatabaseExt
base_db::FileTextQuery
// base_db::FileSourceRootQuery
// base_db::SourceRootQuery
base_db::SourceRootCratesQuery
// ExpandDatabase
hir_db::AstIdMapQuery
// hir_db::ParseMacroExpansionQuery
// hir_db::InternMacroCallQuery
hir_db::MacroArgQuery
hir_db::DeclMacroExpanderQuery
// hir_db::MacroExpandQuery
hir_db::ExpandProcMacroQuery
hir_db::ParseMacroExpansionErrorQuery
// DefDatabase
hir_db::FileItemTreeQuery
hir_db::BlockDefMapQuery
hir_db::StructDataWithDiagnosticsQuery
hir_db::UnionDataWithDiagnosticsQuery
hir_db::EnumDataQuery
hir_db::EnumVariantDataWithDiagnosticsQuery
hir_db::ImplDataWithDiagnosticsQuery
hir_db::TraitDataWithDiagnosticsQuery
hir_db::TraitAliasDataQuery
hir_db::TypeAliasDataQuery
hir_db::FunctionDataQuery
hir_db::ConstDataQuery
hir_db::StaticDataQuery
hir_db::Macro2DataQuery
hir_db::MacroRulesDataQuery
hir_db::ProcMacroDataQuery
hir_db::BodyWithSourceMapQuery
hir_db::BodyQuery
hir_db::ExprScopesQuery
hir_db::GenericParamsQuery
hir_db::FieldsAttrsQuery
hir_db::FieldsAttrsSourceMapQuery
hir_db::AttrsQuery
hir_db::CrateLangItemsQuery
hir_db::LangItemQuery
hir_db::ImportMapQuery
hir_db::FieldVisibilitiesQuery
hir_db::FunctionVisibilityQuery
hir_db::ConstVisibilityQuery
hir_db::CrateSupportsNoStdQuery
// HirDatabase
hir_db::MirBodyQuery
hir_db::BorrowckQuery
hir_db::TyQuery
hir_db::ValueTyQuery
hir_db::ImplSelfTyQuery
hir_db::ConstParamTyQuery
hir_db::ConstEvalQuery
hir_db::ConstEvalDiscriminantQuery
hir_db::ImplTraitQuery
hir_db::FieldTypesQuery
hir_db::LayoutOfAdtQuery
hir_db::TargetDataLayoutQuery
hir_db::CallableItemSignatureQuery
hir_db::ReturnTypeImplTraitsQuery
hir_db::GenericPredicatesForParamQuery
hir_db::GenericPredicatesQuery
hir_db::TraitEnvironmentQuery
hir_db::GenericDefaultsQuery
hir_db::InherentImplsInCrateQuery
hir_db::InherentImplsInBlockQuery
hir_db::IncoherentInherentImplCratesQuery
hir_db::TraitImplsInCrateQuery
hir_db::TraitImplsInBlockQuery
hir_db::TraitImplsInDepsQuery
// hir_db::InternCallableDefQuery
// hir_db::InternLifetimeParamIdQuery
// hir_db::InternImplTraitIdQuery
// hir_db::InternTypeOrConstParamIdQuery
// hir_db::InternClosureQuery
// hir_db::InternCoroutineQuery
hir_db::AssociatedTyDataQuery
hir_db::TraitDatumQuery
hir_db::AdtDatumQuery
hir_db::ImplDatumQuery
hir_db::FnDefDatumQuery
hir_db::FnDefVarianceQuery
hir_db::AdtVarianceQuery
hir_db::AssociatedTyValueQuery
hir_db::ProgramClausesForChalkEnvQuery
// SymbolsDatabase
symbol_index::ModuleSymbolsQuery
symbol_index::LibrarySymbolsQuery
// symbol_index::LocalRootsQuery
// symbol_index::LibraryRootsQuery
// LineIndexDatabase
crate::LineIndexQuery
// InternDatabase
// hir_db::InternFunctionQuery
// hir_db::InternStructQuery
// hir_db::InternUnionQuery
// hir_db::InternEnumQuery
// hir_db::InternConstQuery
// hir_db::InternStaticQuery
// hir_db::InternTraitQuery
// hir_db::InternTraitAliasQuery
// hir_db::InternTypeAliasQuery
// hir_db::InternImplQuery
// hir_db::InternExternBlockQuery
// hir_db::InternBlockQuery
// hir_db::InternMacro2Query
// hir_db::InternProcMacroQuery
// hir_db::InternMacroRulesQuery
];
}
}

View file

@ -161,7 +161,7 @@ pub struct AnalysisHost {
}
impl AnalysisHost {
pub fn new(lru_capacity: Option<usize>) -> AnalysisHost {
pub fn new(lru_capacity: Option<u16>) -> AnalysisHost {
AnalysisHost { db: RootDatabase::new(lru_capacity) }
}
@ -169,11 +169,11 @@ impl AnalysisHost {
AnalysisHost { db }
}
pub fn update_lru_capacity(&mut self, lru_capacity: Option<usize>) {
pub fn update_lru_capacity(&mut self, lru_capacity: Option<u16>) {
self.db.update_base_query_lru_capacities(lru_capacity);
}
pub fn update_lru_capacities(&mut self, lru_capacities: &FxHashMap<Box<str>, usize>) {
pub fn update_lru_capacities(&mut self, lru_capacities: &FxHashMap<Box<str>, u16>) {
self.db.update_lru_capacities(lru_capacities);
}

View file

@ -377,7 +377,7 @@ fn load_crate_graph(
) -> RootDatabase {
let ProjectWorkspace { toolchain, target_layout, .. } = ws;
let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<usize>().ok());
let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<u16>().ok());
let mut db = RootDatabase::new(lru_cap);
let mut analysis_change = ChangeWithProcMacros::new();

View file

@ -283,9 +283,9 @@ config_data! {
linkedProjects: Vec<ManifestOrProjectJson> = vec![],
/// Number of syntax trees rust-analyzer keeps in memory. Defaults to 128.
lru_capacity: Option<usize> = None,
lru_capacity: Option<u16> = None,
/// Sets the LRU capacity of the specified queries.
lru_query_capacities: FxHashMap<Box<str>, usize> = FxHashMap::default(),
lru_query_capacities: FxHashMap<Box<str>, u16> = FxHashMap::default(),
/// These proc-macros will be ignored when trying to expand them.
///
@ -1743,11 +1743,11 @@ impl Config {
extra_env
}
pub fn lru_parse_query_capacity(&self) -> Option<usize> {
pub fn lru_parse_query_capacity(&self) -> Option<u16> {
self.lru_capacity().to_owned()
}
pub fn lru_query_capacities_config(&self) -> Option<&FxHashMap<Box<str>, usize>> {
pub fn lru_query_capacities_config(&self) -> Option<&FxHashMap<Box<str>, u16>> {
self.lru_query_capacities().is_empty().not().then(|| self.lru_query_capacities())
}
@ -2926,7 +2926,7 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
"FxHashMap<String, String>" => set! {
"type": "object",
},
"FxHashMap<Box<str>, usize>" => set! {
"FxHashMap<Box<str>, u16>" => set! {
"type": "object",
},
"FxHashMap<String, Option<String>>" => set! {
@ -2936,6 +2936,11 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
"type": ["null", "integer"],
"minimum": 0,
},
"Option<u16>" => set! {
"type": ["null", "integer"],
"minimum": 0,
"maximum": 65535,
},
"Option<String>" => set! {
"type": ["null", "string"],
},

View file

@ -53,7 +53,11 @@ pub(crate) fn query_group(args: TokenStream, input: TokenStream) -> TokenStream
num_storages += 1;
}
"dependencies" => {
storage = QueryStorage::Dependencies;
storage = QueryStorage::LruDependencies;
num_storages += 1;
}
"lru" => {
storage = QueryStorage::LruMemoized;
num_storages += 1;
}
"input" => {
@ -235,7 +239,7 @@ pub(crate) fn query_group(args: TokenStream, input: TokenStream) -> TokenStream
queries_with_storage.push(fn_name);
let tracing = if let QueryStorage::Memoized = query.storage {
let tracing = if let QueryStorage::Memoized | QueryStorage::LruMemoized = query.storage {
let s = format!("{trait_name}::{fn_name}");
Some(quote! {
let _p = tracing::debug_span!(#s, #(#key_names = tracing::field::debug(&#key_names)),*).entered();
@ -376,8 +380,9 @@ pub(crate) fn query_group(args: TokenStream, input: TokenStream) -> TokenStream
let storage = match &query.storage {
QueryStorage::Memoized => quote!(salsa::plumbing::MemoizedStorage<Self>),
QueryStorage::Dependencies => {
quote!(salsa::plumbing::DependencyStorage<Self>)
QueryStorage::LruMemoized => quote!(salsa::plumbing::LruMemoizedStorage<Self>),
QueryStorage::LruDependencies => {
quote!(salsa::plumbing::LruDependencyStorage<Self>)
}
QueryStorage::Input if query.keys.is_empty() => {
quote!(salsa::plumbing::UnitInputStorage<Self>)
@ -724,7 +729,8 @@ impl Query {
#[derive(Debug, Clone, PartialEq, Eq)]
enum QueryStorage {
Memoized,
Dependencies,
LruDependencies,
LruMemoized,
Input,
Interned,
InternedLookup { intern_query_type: Ident },
@ -739,7 +745,9 @@ impl QueryStorage {
| QueryStorage::Interned
| QueryStorage::InternedLookup { .. }
| QueryStorage::Transparent => false,
QueryStorage::Memoized | QueryStorage::Dependencies => true,
QueryStorage::Memoized | QueryStorage::LruMemoized | QueryStorage::LruDependencies => {
true
}
}
}
}

View file

@ -1,9 +1,7 @@
use crate::debug::TableEntry;
use crate::durability::Durability;
use crate::hash::FxIndexMap;
use crate::lru::Lru;
use crate::plumbing::DerivedQueryStorageOps;
use crate::plumbing::LruQueryStorageOps;
use crate::plumbing::QueryFunction;
use crate::plumbing::QueryStorageMassOps;
use crate::plumbing::QueryStorageOps;
@ -13,7 +11,6 @@ use crate::{Database, DatabaseKeyIndex, QueryDb, Revision};
use parking_lot::RwLock;
use std::borrow::Borrow;
use std::hash::Hash;
use std::marker::PhantomData;
use triomphe::Arc;
mod slot;
@ -22,79 +19,33 @@ use slot::Slot;
/// Memoized queries store the result plus a list of the other queries
/// that they invoked. This means we can avoid recomputing them when
/// none of those inputs have changed.
pub type MemoizedStorage<Q> = DerivedStorage<Q, AlwaysMemoizeValue>;
/// "Dependency" queries just track their dependencies and not the
/// actual value (which they produce on demand). This lessens the
/// storage requirements.
pub type DependencyStorage<Q> = DerivedStorage<Q, NeverMemoizeValue>;
pub type MemoizedStorage<Q> = DerivedStorage<Q>;
/// Handles storage where the value is 'derived' by executing a
/// function (in contrast to "inputs").
pub struct DerivedStorage<Q, MP>
pub struct DerivedStorage<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
group_index: u16,
lru_list: Lru<Slot<Q, MP>>,
slot_map: RwLock<FxIndexMap<Q::Key, Arc<Slot<Q, MP>>>>,
policy: PhantomData<MP>,
slot_map: RwLock<FxIndexMap<Q::Key, Arc<Slot<Q>>>>,
}
impl<Q, MP> std::panic::RefUnwindSafe for DerivedStorage<Q, MP>
impl<Q> std::panic::RefUnwindSafe for DerivedStorage<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Key: std::panic::RefUnwindSafe,
Q::Value: std::panic::RefUnwindSafe,
{
}
pub trait MemoizationPolicy<Q>: Send + Sync
where
Q: QueryFunction,
{
fn should_memoize_value(key: &Q::Key) -> bool;
fn memoized_value_eq(old_value: &Q::Value, new_value: &Q::Value) -> bool;
}
pub enum AlwaysMemoizeValue {}
impl<Q> MemoizationPolicy<Q> for AlwaysMemoizeValue
impl<Q> DerivedStorage<Q>
where
Q: QueryFunction,
Q::Value: Eq,
{
fn should_memoize_value(_key: &Q::Key) -> bool {
true
}
fn memoized_value_eq(old_value: &Q::Value, new_value: &Q::Value) -> bool {
old_value == new_value
}
}
pub enum NeverMemoizeValue {}
impl<Q> MemoizationPolicy<Q> for NeverMemoizeValue
where
Q: QueryFunction,
{
fn should_memoize_value(_key: &Q::Key) -> bool {
false
}
fn memoized_value_eq(_old_value: &Q::Value, _new_value: &Q::Value) -> bool {
panic!("cannot reach since we never memoize")
}
}
impl<Q, MP> DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn slot(&self, key: &Q::Key) -> Arc<Slot<Q, MP>> {
fn slot(&self, key: &Q::Key) -> Arc<Slot<Q>> {
if let Some(v) = self.slot_map.read().get(key) {
return v.clone();
}
@ -111,20 +62,15 @@ where
}
}
impl<Q, MP> QueryStorageOps<Q> for DerivedStorage<Q, MP>
impl<Q> QueryStorageOps<Q> for DerivedStorage<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
const CYCLE_STRATEGY: crate::plumbing::CycleRecoveryStrategy = Q::CYCLE_STRATEGY;
fn new(group_index: u16) -> Self {
DerivedStorage {
group_index,
slot_map: RwLock::new(FxIndexMap::default()),
lru_list: Default::default(),
policy: PhantomData,
}
DerivedStorage { group_index, slot_map: RwLock::new(FxIndexMap::default()) }
}
fn fmt_index(
@ -161,10 +107,6 @@ where
let slot = self.slot(key);
let StampedValue { value, durability, changed_at } = slot.read(db, key);
if let Some(evicted) = self.lru_list.record_use(&slot) {
evicted.evict();
}
db.salsa_runtime().report_query_read_and_unwind_if_cycle_resulted(
slot.database_key_index(),
durability,
@ -175,7 +117,7 @@ where
}
fn durability(&self, db: &<Q as QueryDb<'_>>::DynDb, key: &Q::Key) -> Durability {
self.slot(key).durability(db)
self.slot_map.read().get(key).map_or(Durability::LOW, |slot| slot.durability(db))
}
fn entries<C>(&self, _db: &<Q as QueryDb<'_>>::DynDb) -> C
@ -187,31 +129,19 @@ where
}
}
impl<Q, MP> QueryStorageMassOps for DerivedStorage<Q, MP>
impl<Q> QueryStorageMassOps for DerivedStorage<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn purge(&self) {
self.lru_list.purge();
*self.slot_map.write() = Default::default();
}
}
impl<Q, MP> LruQueryStorageOps for DerivedStorage<Q, MP>
impl<Q> DerivedQueryStorageOps<Q> for DerivedStorage<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn set_lru_capacity(&self, new_capacity: usize) {
self.lru_list.set_lru_capacity(new_capacity);
}
}
impl<Q, MP> DerivedQueryStorageOps<Q> for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
fn invalidate<S>(&self, runtime: &mut Runtime, key: &S)
where

View file

@ -1,8 +1,5 @@
use crate::debug::TableEntry;
use crate::derived::MemoizationPolicy;
use crate::durability::Durability;
use crate::lru::LruIndex;
use crate::lru::LruNode;
use crate::plumbing::{DatabaseOps, QueryFunction};
use crate::revision::Revision;
use crate::runtime::local_state::ActiveQueryGuard;
@ -14,21 +11,18 @@ use crate::runtime::WaitResult;
use crate::Cycle;
use crate::{Database, DatabaseKeyIndex, Event, EventKind, QueryDb};
use parking_lot::{RawRwLock, RwLock};
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::{debug, info};
pub(super) struct Slot<Q, MP>
pub(super) struct Slot<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
key_index: u32,
// FIXME: Yeet this
group_index: u16,
state: RwLock<QueryState<Q>>,
lru_index: LruIndex,
policy: PhantomData<MP>,
}
/// Defines the "current state" of query's memoized results.
@ -54,7 +48,7 @@ where
struct Memo<V> {
/// The result of the query, if we decide to memoize it.
value: Option<V>,
value: V,
/// Last revision when this memo was verified; this begins
/// as the current revision.
@ -77,12 +71,6 @@ enum ProbeState<V, G> {
/// verified in this revision.
Stale(G),
/// There is an entry, and it has been verified
/// in this revision, but it has no cached
/// value. The `Revision` is the revision where the
/// value last changed (if we were to recompute it).
NoValue(G, Revision),
/// There is an entry which has been verified,
/// and it has the following value-- or, we blocked
/// on another thread, and that resulted in a cycle.
@ -103,18 +91,16 @@ enum MaybeChangedSinceProbeState<G> {
Stale(G),
}
impl<Q, MP> Slot<Q, MP>
impl<Q> Slot<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
pub(super) fn new(database_key_index: DatabaseKeyIndex) -> Self {
Self {
key_index: database_key_index.key_index,
group_index: database_key_index.group_index,
state: RwLock::new(QueryState::NotComputed),
lru_index: LruIndex::default(),
policy: PhantomData,
}
}
@ -146,9 +132,7 @@ where
loop {
match self.probe(db, self.state.read(), runtime, revision_now) {
ProbeState::UpToDate(v) => return v,
ProbeState::Stale(..) | ProbeState::NoValue(..) | ProbeState::NotComputed(..) => {
break
}
ProbeState::Stale(..) | ProbeState::NotComputed(..) => break,
ProbeState::Retry => continue,
}
}
@ -176,9 +160,7 @@ where
let mut old_memo = loop {
match self.probe(db, self.state.upgradable_read(), runtime, revision_now) {
ProbeState::UpToDate(v) => return v,
ProbeState::Stale(state)
| ProbeState::NotComputed(state)
| ProbeState::NoValue(state, _) => {
ProbeState::Stale(state) | ProbeState::NotComputed(state) => {
type RwLockUpgradableReadGuard<'a, T> =
lock_api::RwLockUpgradableReadGuard<'a, RawRwLock, T>;
@ -226,7 +208,7 @@ where
runtime: &Runtime,
revision_now: Revision,
active_query: ActiveQueryGuard<'_>,
panic_guard: PanicGuard<'_, Q, MP>,
panic_guard: PanicGuard<'_, Q>,
old_memo: Option<Memo<Q::Value>>,
key: &Q::Key,
) -> StampedValue<Q::Value> {
@ -285,22 +267,18 @@ where
// "backdate" its `changed_at` revision to be the same as the
// old value.
if let Some(old_memo) = &old_memo {
if let Some(old_value) = &old_memo.value {
// Careful: if the value became less durable than it
// used to be, that is a "breaking change" that our
// consumers must be aware of. Becoming *more* durable
// is not. See the test `constant_to_non_constant`.
if revisions.durability >= old_memo.revisions.durability
&& MP::memoized_value_eq(old_value, &value)
{
debug!(
"read_upgrade({:?}): value is equal, back-dating to {:?}",
self, old_memo.revisions.changed_at,
);
// Careful: if the value became less durable than it
// used to be, that is a "breaking change" that our
// consumers must be aware of. Becoming *more* durable
// is not. See the test `constant_to_non_constant`.
if revisions.durability >= old_memo.revisions.durability && old_memo.value == value {
debug!(
"read_upgrade({:?}): value is equal, back-dating to {:?}",
self, old_memo.revisions.changed_at,
);
assert!(old_memo.revisions.changed_at <= revisions.changed_at);
revisions.changed_at = old_memo.revisions.changed_at;
}
assert!(old_memo.revisions.changed_at <= revisions.changed_at);
revisions.changed_at = old_memo.revisions.changed_at;
}
}
@ -310,8 +288,7 @@ where
changed_at: revisions.changed_at,
};
let memo_value =
if self.should_memoize_value(key) { Some(new_value.value.clone()) } else { None };
let memo_value = new_value.value.clone();
debug!("read_upgrade({:?}): result.revisions = {:#?}", self, revisions,);
@ -371,20 +348,16 @@ where
return ProbeState::Stale(state);
}
if let Some(value) = &memo.value {
let value = StampedValue {
durability: memo.revisions.durability,
changed_at: memo.revisions.changed_at,
value: value.clone(),
};
let value = &memo.value;
let value = StampedValue {
durability: memo.revisions.durability,
changed_at: memo.revisions.changed_at,
value: value.clone(),
};
info!("{:?}: returning memoized value changed at {:?}", self, value.changed_at);
info!("{:?}: returning memoized value changed at {:?}", self, value.changed_at);
ProbeState::UpToDate(value)
} else {
let changed_at = memo.revisions.changed_at;
ProbeState::NoValue(state, changed_at)
}
ProbeState::UpToDate(value)
}
}
}
@ -407,21 +380,9 @@ where
match &*self.state.read() {
QueryState::NotComputed => None,
QueryState::InProgress { .. } => Some(TableEntry::new(key.clone(), None)),
QueryState::Memoized(memo) => Some(TableEntry::new(key.clone(), memo.value.clone())),
}
}
pub(super) fn evict(&self) {
let mut state = self.state.write();
if let QueryState::Memoized(memo) = &mut *state {
// Evicting a value with an untracked input could
// lead to inconsistencies. Note that we can't check
// `has_untracked_input` when we add the value to the cache,
// because inputs can become untracked in the next revision.
if memo.has_untracked_input() {
return;
QueryState::Memoized(memo) => {
Some(TableEntry::new(key.clone(), Some(memo.value.clone())))
}
memo.value = None;
}
}
@ -489,8 +450,7 @@ where
// If we know when value last changed, we can return right away.
// Note that we don't need the actual value to be available.
ProbeState::NoValue(_, changed_at)
| ProbeState::UpToDate(StampedValue { value: _, durability: _, changed_at }) => {
ProbeState::UpToDate(StampedValue { value: _, durability: _, changed_at }) => {
MaybeChangedSinceProbeState::ChangedAt(changed_at)
}
@ -545,7 +505,7 @@ where
let maybe_changed = old_memo.revisions.changed_at > revision;
panic_guard.proceed(Some(old_memo));
maybe_changed
} else if old_memo.value.is_some() {
} else {
// We found that this memoized value may have changed
// but we have an old value. We can re-run the code and
// actually *check* if it has changed.
@ -559,12 +519,6 @@ where
key,
);
changed_at > revision
} else {
// We found that inputs to this memoized value may have chanced
// but we don't have an old value to compare against or re-use.
// No choice but to drop the memo and say that its value may have changed.
panic_guard.proceed(None);
true
}
}
@ -583,10 +537,6 @@ where
mutex_guard,
)
}
fn should_memoize_value(&self, key: &Q::Key) -> bool {
MP::should_memoize_value(key)
}
}
impl<Q> QueryState<Q>
@ -598,21 +548,21 @@ where
}
}
struct PanicGuard<'me, Q, MP>
struct PanicGuard<'me, Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
slot: &'me Slot<Q, MP>,
slot: &'me Slot<Q>,
runtime: &'me Runtime,
}
impl<'me, Q, MP> PanicGuard<'me, Q, MP>
impl<'me, Q> PanicGuard<'me, Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
fn new(slot: &'me Slot<Q, MP>, runtime: &'me Runtime) -> Self {
fn new(slot: &'me Slot<Q>, runtime: &'me Runtime) -> Self {
Self { slot, runtime }
}
@ -666,10 +616,10 @@ Please report this bug to https://github.com/salsa-rs/salsa/issues."
}
}
impl<'me, Q, MP> Drop for PanicGuard<'me, Q, MP>
impl<'me, Q> Drop for PanicGuard<'me, Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Value: Eq,
{
fn drop(&mut self) {
if std::thread::panicking() {
@ -702,15 +652,11 @@ where
revision_now: Revision,
active_query: &ActiveQueryGuard<'_>,
) -> Option<StampedValue<V>> {
// If we don't have a memoized value, nothing to validate.
if self.value.is_none() {
return None;
}
if self.verify_revisions(db, revision_now, active_query) {
self.value.clone().map(|value| StampedValue {
Some(StampedValue {
durability: self.revisions.durability,
changed_at: self.revisions.changed_at,
value,
value: self.value.clone(),
})
} else {
None
@ -788,58 +734,42 @@ where
self.verified_at = revision_now;
true
}
fn has_untracked_input(&self) -> bool {
self.revisions.untracked
}
}
impl<Q, MP> std::fmt::Debug for Slot<Q, MP>
impl<Q> std::fmt::Debug for Slot<Q>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{:?}", Q::default())
}
}
impl<Q, MP> LruNode for Slot<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn lru_index(&self) -> &LruIndex {
&self.lru_index
}
}
/// Check that `Slot<Q, MP>: Send + Sync` as long as
/// Check that `Slot<Q, >: Send + Sync` as long as
/// `DB::DatabaseData: Send + Sync`, which in turn implies that
/// `Q::Key: Send + Sync`, `Q::Value: Send + Sync`.
#[allow(dead_code)]
fn check_send_sync<Q, MP>()
fn check_send_sync<Q>()
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Key: Send + Sync,
Q::Value: Send + Sync,
{
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<Slot<Q, MP>>();
is_send_sync::<Slot<Q>>();
}
/// Check that `Slot<Q, MP>: 'static` as long as
/// Check that `Slot<Q, >: 'static` as long as
/// `DB::DatabaseData: 'static`, which in turn implies that
/// `Q::Key: 'static`, `Q::Value: 'static`.
#[allow(dead_code)]
fn check_static<Q, MP>()
fn check_static<Q>()
where
Q: QueryFunction + 'static,
MP: MemoizationPolicy<Q> + 'static,
Q::Key: 'static,
Q::Value: 'static,
{
fn is_static<T: 'static>() {}
is_static::<Slot<Q, MP>>();
is_static::<Slot<Q>>();
}

View file

@ -0,0 +1,233 @@
use crate::debug::TableEntry;
use crate::durability::Durability;
use crate::hash::FxIndexMap;
use crate::lru::Lru;
use crate::plumbing::DerivedQueryStorageOps;
use crate::plumbing::LruQueryStorageOps;
use crate::plumbing::QueryFunction;
use crate::plumbing::QueryStorageMassOps;
use crate::plumbing::QueryStorageOps;
use crate::runtime::StampedValue;
use crate::Runtime;
use crate::{Database, DatabaseKeyIndex, QueryDb, Revision};
use parking_lot::RwLock;
use std::borrow::Borrow;
use std::hash::Hash;
use std::marker::PhantomData;
use triomphe::Arc;
mod slot;
use slot::Slot;
/// Memoized queries store the result plus a list of the other queries
/// that they invoked. This means we can avoid recomputing them when
/// none of those inputs have changed.
pub type MemoizedStorage<Q> = DerivedStorage<Q, AlwaysMemoizeValue>;
/// "Dependency" queries just track their dependencies and not the
/// actual value (which they produce on demand). This lessens the
/// storage requirements.
pub type DependencyStorage<Q> = DerivedStorage<Q, NeverMemoizeValue>;
/// Handles storage where the value is 'derived' by executing a
/// function (in contrast to "inputs").
pub struct DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
group_index: u16,
lru_list: Lru<Slot<Q, MP>>,
slot_map: RwLock<FxIndexMap<Q::Key, Arc<Slot<Q, MP>>>>,
policy: PhantomData<MP>,
}
impl<Q, MP> std::panic::RefUnwindSafe for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Key: std::panic::RefUnwindSafe,
Q::Value: std::panic::RefUnwindSafe,
{
}
pub trait MemoizationPolicy<Q>: Send + Sync
where
Q: QueryFunction,
{
fn should_memoize_value(key: &Q::Key) -> bool;
fn memoized_value_eq(old_value: &Q::Value, new_value: &Q::Value) -> bool;
}
pub enum AlwaysMemoizeValue {}
impl<Q> MemoizationPolicy<Q> for AlwaysMemoizeValue
where
Q: QueryFunction,
Q::Value: Eq,
{
fn should_memoize_value(_key: &Q::Key) -> bool {
true
}
fn memoized_value_eq(old_value: &Q::Value, new_value: &Q::Value) -> bool {
old_value == new_value
}
}
pub enum NeverMemoizeValue {}
impl<Q> MemoizationPolicy<Q> for NeverMemoizeValue
where
Q: QueryFunction,
{
fn should_memoize_value(_key: &Q::Key) -> bool {
false
}
fn memoized_value_eq(_old_value: &Q::Value, _new_value: &Q::Value) -> bool {
panic!("cannot reach since we never memoize")
}
}
impl<Q, MP> DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn slot(&self, key: &Q::Key) -> Arc<Slot<Q, MP>> {
if let Some(v) = self.slot_map.read().get(key) {
return v.clone();
}
let mut write = self.slot_map.write();
let entry = write.entry(key.clone());
let key_index = entry.index() as u32;
let database_key_index = DatabaseKeyIndex {
group_index: self.group_index,
query_index: Q::QUERY_INDEX,
key_index,
};
entry.or_insert_with(|| Arc::new(Slot::new(database_key_index))).clone()
}
}
impl<Q, MP> QueryStorageOps<Q> for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
const CYCLE_STRATEGY: crate::plumbing::CycleRecoveryStrategy = Q::CYCLE_STRATEGY;
fn new(group_index: u16) -> Self {
DerivedStorage {
group_index,
slot_map: RwLock::new(FxIndexMap::default()),
lru_list: Default::default(),
policy: PhantomData,
}
}
fn fmt_index(
&self,
_db: &<Q as QueryDb<'_>>::DynDb,
index: u32,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let slot_map = self.slot_map.read();
let key = slot_map.get_index(index as usize).unwrap().0;
write!(fmt, "{}::{}({:?})", std::any::type_name::<Q>(), Q::QUERY_NAME, key)
}
fn maybe_changed_after(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
index: u32,
revision: Revision,
) -> bool {
debug_assert!(revision < db.salsa_runtime().current_revision());
let (key, slot) = {
let read = self.slot_map.read();
let Some((key, slot)) = read.get_index(index as usize) else {
return false;
};
(key.clone(), slot.clone())
};
slot.maybe_changed_after(db, revision, &key)
}
fn fetch(&self, db: &<Q as QueryDb<'_>>::DynDb, key: &Q::Key) -> Q::Value {
db.unwind_if_cancelled();
let slot = self.slot(key);
let StampedValue { value, durability, changed_at } = slot.read(db, key);
if let Some(evicted) = self.lru_list.record_use(&slot) {
evicted.evict();
}
db.salsa_runtime().report_query_read_and_unwind_if_cycle_resulted(
slot.database_key_index(),
durability,
changed_at,
);
value
}
fn durability(&self, db: &<Q as QueryDb<'_>>::DynDb, key: &Q::Key) -> Durability {
self.slot(key).durability(db)
}
fn entries<C>(&self, _db: &<Q as QueryDb<'_>>::DynDb) -> C
where
C: std::iter::FromIterator<TableEntry<Q::Key, Q::Value>>,
{
let slot_map = self.slot_map.read();
slot_map.iter().filter_map(|(key, slot)| slot.as_table_entry(key)).collect()
}
}
impl<Q, MP> QueryStorageMassOps for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn purge(&self) {
self.lru_list.purge();
*self.slot_map.write() = Default::default();
}
}
impl<Q, MP> LruQueryStorageOps for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn set_lru_capacity(&self, new_capacity: u16) {
self.lru_list.set_lru_capacity(new_capacity);
}
}
impl<Q, MP> DerivedQueryStorageOps<Q> for DerivedStorage<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn invalidate<S>(&self, runtime: &mut Runtime, key: &S)
where
S: Eq + Hash,
Q::Key: Borrow<S>,
{
runtime.with_incremented_revision(|new_revision| {
let map_read = self.slot_map.read();
if let Some(slot) = map_read.get(key) {
if let Some(durability) = slot.invalidate(new_revision) {
return Some(durability);
}
}
None
})
}
}

View file

@ -0,0 +1,845 @@
use crate::debug::TableEntry;
use crate::derived_lru::MemoizationPolicy;
use crate::durability::Durability;
use crate::lru::LruIndex;
use crate::lru::LruNode;
use crate::plumbing::{DatabaseOps, QueryFunction};
use crate::revision::Revision;
use crate::runtime::local_state::ActiveQueryGuard;
use crate::runtime::local_state::QueryRevisions;
use crate::runtime::Runtime;
use crate::runtime::RuntimeId;
use crate::runtime::StampedValue;
use crate::runtime::WaitResult;
use crate::Cycle;
use crate::{Database, DatabaseKeyIndex, Event, EventKind, QueryDb};
use parking_lot::{RawRwLock, RwLock};
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::{debug, info};
pub(super) struct Slot<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
key_index: u32,
group_index: u16,
state: RwLock<QueryState<Q>>,
lru_index: LruIndex,
policy: PhantomData<MP>,
}
/// Defines the "current state" of query's memoized results.
enum QueryState<Q>
where
Q: QueryFunction,
{
NotComputed,
/// The runtime with the given id is currently computing the
/// result of this query.
InProgress {
id: RuntimeId,
/// Set to true if any other queries are blocked,
/// waiting for this query to complete.
anyone_waiting: AtomicBool,
},
/// We have computed the query already, and here is the result.
Memoized(Memo<Q::Value>),
}
struct Memo<V> {
/// The result of the query, if we decide to memoize it.
value: Option<V>,
/// Last revision when this memo was verified; this begins
/// as the current revision.
pub(crate) verified_at: Revision,
/// Revision information
revisions: QueryRevisions,
}
/// Return value of `probe` helper.
enum ProbeState<V, G> {
/// Another thread was active but has completed.
/// Try again!
Retry,
/// No entry for this key at all.
NotComputed(G),
/// There is an entry, but its contents have not been
/// verified in this revision.
Stale(G),
/// There is an entry, and it has been verified
/// in this revision, but it has no cached
/// value. The `Revision` is the revision where the
/// value last changed (if we were to recompute it).
NoValue(G, Revision),
/// There is an entry which has been verified,
/// and it has the following value-- or, we blocked
/// on another thread, and that resulted in a cycle.
UpToDate(V),
}
/// Return value of `maybe_changed_after_probe` helper.
enum MaybeChangedSinceProbeState<G> {
/// Another thread was active but has completed.
/// Try again!
Retry,
/// Value may have changed in the given revision.
ChangedAt(Revision),
/// There is a stale cache entry that has not been
/// verified in this revision, so we can't say.
Stale(G),
}
impl<Q, MP> Slot<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
pub(super) fn new(database_key_index: DatabaseKeyIndex) -> Self {
Self {
key_index: database_key_index.key_index,
group_index: database_key_index.group_index,
state: RwLock::new(QueryState::NotComputed),
lru_index: LruIndex::default(),
policy: PhantomData,
}
}
pub(super) fn database_key_index(&self) -> DatabaseKeyIndex {
DatabaseKeyIndex {
group_index: self.group_index,
query_index: Q::QUERY_INDEX,
key_index: self.key_index,
}
}
pub(super) fn read(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
key: &Q::Key,
) -> StampedValue<Q::Value> {
let runtime = db.salsa_runtime();
// NB: We don't need to worry about people modifying the
// revision out from under our feet. Either `db` is a frozen
// database, in which case there is a lock, or the mutator
// thread is the current thread, and it will be prevented from
// doing any `set` invocations while the query function runs.
let revision_now = runtime.current_revision();
info!("{:?}: invoked at {:?}", self, revision_now,);
// First, do a check with a read-lock.
loop {
match self.probe(db, self.state.read(), runtime, revision_now) {
ProbeState::UpToDate(v) => return v,
ProbeState::Stale(..) | ProbeState::NoValue(..) | ProbeState::NotComputed(..) => {
break
}
ProbeState::Retry => continue,
}
}
self.read_upgrade(db, key, revision_now)
}
/// Second phase of a read operation: acquires an upgradable-read
/// and -- if needed -- validates whether inputs have changed,
/// recomputes value, etc. This is invoked after our initial probe
/// shows a potentially out of date value.
fn read_upgrade(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
key: &Q::Key,
revision_now: Revision,
) -> StampedValue<Q::Value> {
let runtime = db.salsa_runtime();
debug!("{:?}: read_upgrade(revision_now={:?})", self, revision_now,);
// Check with an upgradable read to see if there is a value
// already. (This permits other readers but prevents anyone
// else from running `read_upgrade` at the same time.)
let mut old_memo = loop {
match self.probe(db, self.state.upgradable_read(), runtime, revision_now) {
ProbeState::UpToDate(v) => return v,
ProbeState::Stale(state)
| ProbeState::NotComputed(state)
| ProbeState::NoValue(state, _) => {
type RwLockUpgradableReadGuard<'a, T> =
lock_api::RwLockUpgradableReadGuard<'a, RawRwLock, T>;
let mut state = RwLockUpgradableReadGuard::upgrade(state);
match std::mem::replace(&mut *state, QueryState::in_progress(runtime.id())) {
QueryState::Memoized(old_memo) => break Some(old_memo),
QueryState::InProgress { .. } => unreachable!(),
QueryState::NotComputed => break None,
}
}
ProbeState::Retry => continue,
}
};
let panic_guard = PanicGuard::new(self, runtime);
let active_query = runtime.push_query(self.database_key_index());
// If we have an old-value, it *may* now be stale, since there
// has been a new revision since the last time we checked. So,
// first things first, let's walk over each of our previous
// inputs and check whether they are out of date.
if let Some(memo) = &mut old_memo {
if let Some(value) = memo.verify_value(db.ops_database(), revision_now, &active_query) {
info!("{:?}: validated old memoized value", self,);
db.salsa_event(Event {
runtime_id: runtime.id(),
kind: EventKind::DidValidateMemoizedValue {
database_key: self.database_key_index(),
},
});
panic_guard.proceed(old_memo);
return value;
}
}
self.execute(db, runtime, revision_now, active_query, panic_guard, old_memo, key)
}
fn execute(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
runtime: &Runtime,
revision_now: Revision,
active_query: ActiveQueryGuard<'_>,
panic_guard: PanicGuard<'_, Q, MP>,
old_memo: Option<Memo<Q::Value>>,
key: &Q::Key,
) -> StampedValue<Q::Value> {
tracing::info!("{:?}: executing query", self.database_key_index().debug(db));
db.salsa_event(Event {
runtime_id: db.salsa_runtime().id(),
kind: EventKind::WillExecute { database_key: self.database_key_index() },
});
// Query was not previously executed, or value is potentially
// stale, or value is absent. Let's execute!
let value = match Cycle::catch(|| Q::execute(db, key.clone())) {
Ok(v) => v,
Err(cycle) => {
tracing::debug!(
"{:?}: caught cycle {:?}, have strategy {:?}",
self.database_key_index().debug(db),
cycle,
Q::CYCLE_STRATEGY,
);
match Q::CYCLE_STRATEGY {
crate::plumbing::CycleRecoveryStrategy::Panic => {
panic_guard.proceed(None);
cycle.throw()
}
crate::plumbing::CycleRecoveryStrategy::Fallback => {
if let Some(c) = active_query.take_cycle() {
assert!(c.is(&cycle));
Q::cycle_fallback(db, &cycle, key)
} else {
// we are not a participant in this cycle
debug_assert!(!cycle
.participant_keys()
.any(|k| k == self.database_key_index()));
cycle.throw()
}
}
}
}
};
let mut revisions = active_query.pop();
// We assume that query is side-effect free -- that is, does
// not mutate the "inputs" to the query system. Sanity check
// that assumption here, at least to the best of our ability.
assert_eq!(
runtime.current_revision(),
revision_now,
"revision altered during query execution",
);
// If the new value is equal to the old one, then it didn't
// really change, even if some of its inputs have. So we can
// "backdate" its `changed_at` revision to be the same as the
// old value.
if let Some(old_memo) = &old_memo {
if let Some(old_value) = &old_memo.value {
// Careful: if the value became less durable than it
// used to be, that is a "breaking change" that our
// consumers must be aware of. Becoming *more* durable
// is not. See the test `constant_to_non_constant`.
if revisions.durability >= old_memo.revisions.durability
&& MP::memoized_value_eq(old_value, &value)
{
debug!(
"read_upgrade({:?}): value is equal, back-dating to {:?}",
self, old_memo.revisions.changed_at,
);
assert!(old_memo.revisions.changed_at <= revisions.changed_at);
revisions.changed_at = old_memo.revisions.changed_at;
}
}
}
let new_value = StampedValue {
value,
durability: revisions.durability,
changed_at: revisions.changed_at,
};
let memo_value =
if self.should_memoize_value(key) { Some(new_value.value.clone()) } else { None };
debug!("read_upgrade({:?}): result.revisions = {:#?}", self, revisions,);
panic_guard.proceed(Some(Memo { value: memo_value, verified_at: revision_now, revisions }));
new_value
}
/// Helper for `read` that does a shallow check (not recursive) if we have an up-to-date value.
///
/// Invoked with the guard `state` corresponding to the `QueryState` of some `Slot` (the guard
/// can be either read or write). Returns a suitable `ProbeState`:
///
/// - `ProbeState::UpToDate(r)` if the table has an up-to-date value (or we blocked on another
/// thread that produced such a value).
/// - `ProbeState::StaleOrAbsent(g)` if either (a) there is no memo for this key, (b) the memo
/// has no value; or (c) the memo has not been verified at the current revision.
///
/// Note that in case `ProbeState::UpToDate`, the lock will have been released.
fn probe<StateGuard>(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
state: StateGuard,
runtime: &Runtime,
revision_now: Revision,
) -> ProbeState<StampedValue<Q::Value>, StateGuard>
where
StateGuard: Deref<Target = QueryState<Q>>,
{
match &*state {
QueryState::NotComputed => ProbeState::NotComputed(state),
QueryState::InProgress { id, anyone_waiting } => {
let other_id = *id;
// NB: `Ordering::Relaxed` is sufficient here,
// as there are no loads that are "gated" on this
// value. Everything that is written is also protected
// by a lock that must be acquired. The role of this
// boolean is to decide *whether* to acquire the lock,
// not to gate future atomic reads.
anyone_waiting.store(true, Ordering::Relaxed);
self.block_on_or_unwind(db, runtime, other_id, state);
// Other thread completely normally, so our value may be available now.
ProbeState::Retry
}
QueryState::Memoized(memo) => {
debug!(
"{:?}: found memoized value, verified_at={:?}, changed_at={:?}",
self, memo.verified_at, memo.revisions.changed_at,
);
if memo.verified_at < revision_now {
return ProbeState::Stale(state);
}
if let Some(value) = &memo.value {
let value = StampedValue {
durability: memo.revisions.durability,
changed_at: memo.revisions.changed_at,
value: value.clone(),
};
info!("{:?}: returning memoized value changed at {:?}", self, value.changed_at);
ProbeState::UpToDate(value)
} else {
let changed_at = memo.revisions.changed_at;
ProbeState::NoValue(state, changed_at)
}
}
}
}
pub(super) fn durability(&self, db: &<Q as QueryDb<'_>>::DynDb) -> Durability {
match &*self.state.read() {
QueryState::NotComputed => Durability::LOW,
QueryState::InProgress { .. } => panic!("query in progress"),
QueryState::Memoized(memo) => {
if memo.check_durability(db.salsa_runtime()) {
memo.revisions.durability
} else {
Durability::LOW
}
}
}
}
pub(super) fn as_table_entry(&self, key: &Q::Key) -> Option<TableEntry<Q::Key, Q::Value>> {
match &*self.state.read() {
QueryState::NotComputed => None,
QueryState::InProgress { .. } => Some(TableEntry::new(key.clone(), None)),
QueryState::Memoized(memo) => Some(TableEntry::new(key.clone(), memo.value.clone())),
}
}
pub(super) fn evict(&self) {
let mut state = self.state.write();
if let QueryState::Memoized(memo) = &mut *state {
// Evicting a value with an untracked input could
// lead to inconsistencies. Note that we can't check
// `has_untracked_input` when we add the value to the cache,
// because inputs can become untracked in the next revision.
if memo.has_untracked_input() {
return;
}
memo.value = None;
}
}
pub(super) fn invalidate(&self, new_revision: Revision) -> Option<Durability> {
tracing::debug!("Slot::invalidate(new_revision = {:?})", new_revision);
match &mut *self.state.write() {
QueryState::Memoized(memo) => {
memo.revisions.untracked = true;
memo.revisions.inputs = None;
memo.revisions.changed_at = new_revision;
Some(memo.revisions.durability)
}
QueryState::NotComputed => None,
QueryState::InProgress { .. } => unreachable!(),
}
}
pub(super) fn maybe_changed_after(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
revision: Revision,
key: &Q::Key,
) -> bool {
let runtime = db.salsa_runtime();
let revision_now = runtime.current_revision();
db.unwind_if_cancelled();
debug!(
"maybe_changed_after({:?}) called with revision={:?}, revision_now={:?}",
self, revision, revision_now,
);
// Do an initial probe with just the read-lock.
//
// If we find that a cache entry for the value is present
// but hasn't been verified in this revision, we'll have to
// do more.
loop {
match self.maybe_changed_after_probe(db, self.state.read(), runtime, revision_now) {
MaybeChangedSinceProbeState::Retry => continue,
MaybeChangedSinceProbeState::ChangedAt(changed_at) => return changed_at > revision,
MaybeChangedSinceProbeState::Stale(state) => {
drop(state);
return self.maybe_changed_after_upgrade(db, revision, key);
}
}
}
}
fn maybe_changed_after_probe<StateGuard>(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
state: StateGuard,
runtime: &Runtime,
revision_now: Revision,
) -> MaybeChangedSinceProbeState<StateGuard>
where
StateGuard: Deref<Target = QueryState<Q>>,
{
match self.probe(db, state, runtime, revision_now) {
ProbeState::Retry => MaybeChangedSinceProbeState::Retry,
ProbeState::Stale(state) => MaybeChangedSinceProbeState::Stale(state),
// If we know when value last changed, we can return right away.
// Note that we don't need the actual value to be available.
ProbeState::NoValue(_, changed_at)
| ProbeState::UpToDate(StampedValue { value: _, durability: _, changed_at }) => {
MaybeChangedSinceProbeState::ChangedAt(changed_at)
}
// If we have nothing cached, then value may have changed.
ProbeState::NotComputed(_) => MaybeChangedSinceProbeState::ChangedAt(revision_now),
}
}
fn maybe_changed_after_upgrade(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
revision: Revision,
key: &Q::Key,
) -> bool {
let runtime = db.salsa_runtime();
let revision_now = runtime.current_revision();
// Get an upgradable read lock, which permits other reads but no writers.
// Probe again. If the value is stale (needs to be verified), then upgrade
// to a write lock and swap it with InProgress while we work.
let mut old_memo = match self.maybe_changed_after_probe(
db,
self.state.upgradable_read(),
runtime,
revision_now,
) {
MaybeChangedSinceProbeState::ChangedAt(changed_at) => return changed_at > revision,
// If another thread was active, then the cache line is going to be
// either verified or cleared out. Just recurse to figure out which.
// Note that we don't need an upgradable read.
MaybeChangedSinceProbeState::Retry => {
return self.maybe_changed_after(db, revision, key)
}
MaybeChangedSinceProbeState::Stale(state) => {
type RwLockUpgradableReadGuard<'a, T> =
lock_api::RwLockUpgradableReadGuard<'a, RawRwLock, T>;
let mut state = RwLockUpgradableReadGuard::upgrade(state);
match std::mem::replace(&mut *state, QueryState::in_progress(runtime.id())) {
QueryState::Memoized(old_memo) => old_memo,
QueryState::NotComputed | QueryState::InProgress { .. } => unreachable!(),
}
}
};
let panic_guard = PanicGuard::new(self, runtime);
let active_query = runtime.push_query(self.database_key_index());
if old_memo.verify_revisions(db.ops_database(), revision_now, &active_query) {
let maybe_changed = old_memo.revisions.changed_at > revision;
panic_guard.proceed(Some(old_memo));
maybe_changed
} else if old_memo.value.is_some() {
// We found that this memoized value may have changed
// but we have an old value. We can re-run the code and
// actually *check* if it has changed.
let StampedValue { changed_at, .. } = self.execute(
db,
runtime,
revision_now,
active_query,
panic_guard,
Some(old_memo),
key,
);
changed_at > revision
} else {
// We found that inputs to this memoized value may have chanced
// but we don't have an old value to compare against or re-use.
// No choice but to drop the memo and say that its value may have changed.
panic_guard.proceed(None);
true
}
}
/// Helper: see [`Runtime::try_block_on_or_unwind`].
fn block_on_or_unwind<MutexGuard>(
&self,
db: &<Q as QueryDb<'_>>::DynDb,
runtime: &Runtime,
other_id: RuntimeId,
mutex_guard: MutexGuard,
) {
runtime.block_on_or_unwind(
db.ops_database(),
self.database_key_index(),
other_id,
mutex_guard,
)
}
fn should_memoize_value(&self, key: &Q::Key) -> bool {
MP::should_memoize_value(key)
}
}
impl<Q> QueryState<Q>
where
Q: QueryFunction,
{
fn in_progress(id: RuntimeId) -> Self {
QueryState::InProgress { id, anyone_waiting: Default::default() }
}
}
struct PanicGuard<'me, Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
slot: &'me Slot<Q, MP>,
runtime: &'me Runtime,
}
impl<'me, Q, MP> PanicGuard<'me, Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn new(slot: &'me Slot<Q, MP>, runtime: &'me Runtime) -> Self {
Self { slot, runtime }
}
/// Indicates that we have concluded normally (without panicking).
/// If `opt_memo` is some, then this memo is installed as the new
/// memoized value. If `opt_memo` is `None`, then the slot is cleared
/// and has no value.
fn proceed(mut self, opt_memo: Option<Memo<Q::Value>>) {
self.overwrite_placeholder(WaitResult::Completed, opt_memo);
std::mem::forget(self)
}
/// Overwrites the `InProgress` placeholder for `key` that we
/// inserted; if others were blocked, waiting for us to finish,
/// then notify them.
fn overwrite_placeholder(&mut self, wait_result: WaitResult, opt_memo: Option<Memo<Q::Value>>) {
let old_value = {
let mut write = self.slot.state.write();
match opt_memo {
// Replace the `InProgress` marker that we installed with the new
// memo, thus releasing our unique access to this key.
Some(memo) => std::mem::replace(&mut *write, QueryState::Memoized(memo)),
// We had installed an `InProgress` marker, but we panicked before
// it could be removed. At this point, we therefore "own" unique
// access to our slot, so we can just remove the key.
None => std::mem::replace(&mut *write, QueryState::NotComputed),
}
};
match old_value {
QueryState::InProgress { id, anyone_waiting } => {
assert_eq!(id, self.runtime.id());
// NB: As noted on the `store`, `Ordering::Relaxed` is
// sufficient here. This boolean signals us on whether to
// acquire a mutex; the mutex will guarantee that all writes
// we are interested in are visible.
if anyone_waiting.load(Ordering::Relaxed) {
self.runtime
.unblock_queries_blocked_on(self.slot.database_key_index(), wait_result);
}
}
_ => panic!(
"\
Unexpected panic during query evaluation, aborting the process.
Please report this bug to https://github.com/salsa-rs/salsa/issues."
),
}
}
}
impl<'me, Q, MP> Drop for PanicGuard<'me, Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn drop(&mut self) {
if std::thread::panicking() {
// We panicked before we could proceed and need to remove `key`.
self.overwrite_placeholder(WaitResult::Panicked, None)
} else {
// If no panic occurred, then panic guard ought to be
// "forgotten" and so this Drop code should never run.
panic!(".forget() was not called")
}
}
}
impl<V> Memo<V>
where
V: Clone,
{
/// Determines whether the value stored in this memo (if any) is still
/// valid in the current revision. If so, returns a stamped value.
///
/// If needed, this will walk each dependency and
/// recursively invoke `maybe_changed_after`, which may in turn
/// re-execute the dependency. This can cause cycles to occur,
/// so the current query must be pushed onto the
/// stack to permit cycle detection and recovery: therefore,
/// takes the `active_query` argument as evidence.
fn verify_value(
&mut self,
db: &dyn Database,
revision_now: Revision,
active_query: &ActiveQueryGuard<'_>,
) -> Option<StampedValue<V>> {
// If we don't have a memoized value, nothing to validate.
if self.value.is_none() {
return None;
}
if self.verify_revisions(db, revision_now, active_query) {
self.value.clone().map(|value| StampedValue {
durability: self.revisions.durability,
changed_at: self.revisions.changed_at,
value,
})
} else {
None
}
}
/// Determines whether the value represented by this memo is still
/// valid in the current revision; note that the value itself is
/// not needed for this check. If needed, this will walk each
/// dependency and recursively invoke `maybe_changed_after`, which
/// may in turn re-execute the dependency. This can cause cycles to occur,
/// so the current query must be pushed onto the
/// stack to permit cycle detection and recovery: therefore,
/// takes the `active_query` argument as evidence.
fn verify_revisions(
&mut self,
db: &dyn Database,
revision_now: Revision,
_active_query: &ActiveQueryGuard<'_>,
) -> bool {
assert!(self.verified_at != revision_now);
let verified_at = self.verified_at;
debug!(
"verify_revisions: verified_at={:?}, revision_now={:?}, inputs={:#?}",
verified_at, revision_now, self.revisions.inputs
);
if self.check_durability(db.salsa_runtime()) {
return self.mark_value_as_verified(revision_now);
}
match &self.revisions.inputs {
// We can't validate values that had untracked inputs; just have to
// re-execute.
None if self.revisions.untracked => return false,
None => {}
// Check whether any of our inputs changed since the
// **last point where we were verified** (not since we
// last changed). This is important: if we have
// memoized values, then an input may have changed in
// revision R2, but we found that *our* value was the
// same regardless, so our change date is still
// R1. But our *verification* date will be R2, and we
// are only interested in finding out whether the
// input changed *again*.
Some(inputs) => {
let changed_input =
inputs.slice.iter().find(|&&input| db.maybe_changed_after(input, verified_at));
if let Some(input) = changed_input {
debug!("validate_memoized_value: `{:?}` may have changed", input);
return false;
}
}
};
self.mark_value_as_verified(revision_now)
}
/// True if this memo is known not to have changed based on its durability.
fn check_durability(&self, runtime: &Runtime) -> bool {
let last_changed = runtime.last_changed_revision(self.revisions.durability);
debug!(
"check_durability(last_changed={:?} <= verified_at={:?}) = {:?}",
last_changed,
self.verified_at,
last_changed <= self.verified_at,
);
last_changed <= self.verified_at
}
fn mark_value_as_verified(&mut self, revision_now: Revision) -> bool {
self.verified_at = revision_now;
true
}
fn has_untracked_input(&self) -> bool {
self.revisions.untracked
}
}
impl<Q, MP> std::fmt::Debug for Slot<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{:?}", Q::default())
}
}
impl<Q, MP> LruNode for Slot<Q, MP>
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
{
fn lru_index(&self) -> &LruIndex {
&self.lru_index
}
}
/// Check that `Slot<Q, MP>: Send + Sync` as long as
/// `DB::DatabaseData: Send + Sync`, which in turn implies that
/// `Q::Key: Send + Sync`, `Q::Value: Send + Sync`.
#[allow(dead_code)]
fn check_send_sync<Q, MP>()
where
Q: QueryFunction,
MP: MemoizationPolicy<Q>,
Q::Key: Send + Sync,
Q::Value: Send + Sync,
{
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<Slot<Q, MP>>();
}
/// Check that `Slot<Q, MP>: 'static` as long as
/// `DB::DatabaseData: 'static`, which in turn implies that
/// `Q::Key: 'static`, `Q::Value: 'static`.
#[allow(dead_code)]
fn check_static<Q, MP>()
where
Q: QueryFunction + 'static,
MP: MemoizationPolicy<Q> + 'static,
Q::Key: 'static,
Q::Value: 'static,
{
fn is_static<T: 'static>() {}
is_static::<Slot<Q, MP>>();
}

View file

@ -121,8 +121,8 @@ impl InternValueTrivial for String {}
#[derive(Debug)]
struct Slot<V> {
/// DatabaseKeyIndex for this slot.
database_key_index: DatabaseKeyIndex,
/// key index for this slot.
key_index: u32,
/// Value that was interned.
value: V,
@ -199,13 +199,8 @@ where
};
let create_slot = |index: InternId| {
let database_key_index = DatabaseKeyIndex {
group_index: self.group_index,
query_index: Q::QUERY_INDEX,
key_index: index.as_u32(),
};
Arc::new(Slot {
database_key_index,
key_index: index.as_u32(),
value: insert(Q::Value::from_intern_id(index)),
interned_at: revision_now,
})
@ -242,7 +237,11 @@ where
};
let changed_at = slot.interned_at;
db.salsa_runtime().report_query_read_and_unwind_if_cycle_resulted(
slot.database_key_index,
DatabaseKeyIndex {
group_index: self.group_index,
query_index: Q::QUERY_INDEX,
key_index: slot.key_index,
},
INTERN_DURABILITY,
changed_at,
);
@ -294,7 +293,11 @@ where
};
let changed_at = slot.interned_at;
db.salsa_runtime().report_query_read_and_unwind_if_cycle_resulted(
slot.database_key_index,
DatabaseKeyIndex {
group_index: self.group_index,
query_index: Q::QUERY_INDEX,
key_index: slot.key_index,
},
INTERN_DURABILITY,
changed_at,
);
@ -414,7 +417,11 @@ where
let value = slot.value.clone();
let interned_at = slot.interned_at;
db.salsa_runtime().report_query_read_and_unwind_if_cycle_resulted(
slot.database_key_index,
DatabaseKeyIndex {
group_index: interned_storage.group_index,
query_index: Q::QUERY_INDEX,
key_index: slot.key_index,
},
INTERN_DURABILITY,
interned_at,
);

View file

@ -10,6 +10,7 @@
//! from previous invocations as appropriate.
mod derived;
mod derived_lru;
mod durability;
mod hash;
mod input;
@ -577,7 +578,7 @@ where
/// cost of potential extra recalculations of evicted values.
///
/// If `cap` is zero, all values are preserved, this is the default.
pub fn set_lru_capacity(&self, cap: usize)
pub fn set_lru_capacity(&self, cap: u16)
where
Q::Storage: plumbing::LruQueryStorageOps,
{

View file

@ -1,7 +1,7 @@
use oorandom::Rand64;
use parking_lot::Mutex;
use std::fmt::Debug;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::AtomicU16;
use std::sync::atomic::Ordering;
use triomphe::Arc;
@ -20,15 +20,15 @@ pub(crate) struct Lru<Node>
where
Node: LruNode,
{
green_zone: AtomicUsize,
green_zone: AtomicU16,
data: Mutex<LruData<Node>>,
}
#[derive(Debug)]
struct LruData<Node> {
end_red_zone: usize,
end_yellow_zone: usize,
end_green_zone: usize,
end_red_zone: u16,
end_yellow_zone: u16,
end_green_zone: u16,
rng: Rand64,
entries: Vec<Arc<Node>>,
}
@ -39,9 +39,9 @@ pub(crate) trait LruNode: Sized + Debug {
#[derive(Debug)]
pub(crate) struct LruIndex {
/// Index in the appropriate LRU list, or std::usize::MAX if not a
/// Index in the appropriate LRU list, or std::u16::MAX if not a
/// member.
index: AtomicUsize,
index: AtomicU16,
}
impl<Node> Default for Lru<Node>
@ -68,12 +68,12 @@ where
#[cfg_attr(not(test), allow(dead_code))]
fn with_seed(seed: &str) -> Self {
Lru { green_zone: AtomicUsize::new(0), data: Mutex::new(LruData::with_seed(seed)) }
Lru { green_zone: AtomicU16::new(0), data: Mutex::new(LruData::with_seed(seed)) }
}
/// Adjust the total number of nodes permitted to have a value at
/// once. If `len` is zero, this disables LRU caching completely.
pub(crate) fn set_lru_capacity(&self, len: usize) {
pub(crate) fn set_lru_capacity(&self, len: u16) {
let mut data = self.data.lock();
// We require each zone to have at least 1 slot. Therefore,
@ -143,23 +143,24 @@ where
LruData { end_yellow_zone: 0, end_green_zone: 0, end_red_zone: 0, entries: Vec::new(), rng }
}
fn green_zone(&self) -> std::ops::Range<usize> {
fn green_zone(&self) -> std::ops::Range<u16> {
0..self.end_green_zone
}
fn yellow_zone(&self) -> std::ops::Range<usize> {
fn yellow_zone(&self) -> std::ops::Range<u16> {
self.end_green_zone..self.end_yellow_zone
}
fn red_zone(&self) -> std::ops::Range<usize> {
fn red_zone(&self) -> std::ops::Range<u16> {
self.end_yellow_zone..self.end_red_zone
}
fn resize(&mut self, len_green_zone: usize, len_yellow_zone: usize, len_red_zone: usize) {
fn resize(&mut self, len_green_zone: u16, len_yellow_zone: u16, len_red_zone: u16) {
self.end_green_zone = len_green_zone;
self.end_yellow_zone = self.end_green_zone + len_yellow_zone;
self.end_red_zone = self.end_yellow_zone + len_red_zone;
let entries = std::mem::replace(&mut self.entries, Vec::with_capacity(self.end_red_zone));
let entries =
std::mem::replace(&mut self.entries, Vec::with_capacity(self.end_red_zone as usize));
tracing::debug!("green_zone = {:?}", self.green_zone());
tracing::debug!("yellow_zone = {:?}", self.yellow_zone());
@ -207,7 +208,7 @@ where
// Easy case: we still have capacity. Push it, and then promote
// it up to the appropriate zone.
let len = self.entries.len();
let len = self.entries.len() as u16;
if len < self.end_red_zone {
self.entries.push(node.clone());
node.lru_index().store(len);
@ -218,7 +219,7 @@ where
// Harder case: no capacity. Create some by evicting somebody from red
// zone and then promoting.
let victim_index = self.pick_index(self.red_zone());
let victim_node = std::mem::replace(&mut self.entries[victim_index], node.clone());
let victim_node = std::mem::replace(&mut self.entries[victim_index as usize], node.clone());
tracing::debug!("evicting red node {:?} from {}", victim_node, victim_index);
victim_node.lru_index().clear();
self.promote_red_to_green(node, victim_index);
@ -231,7 +232,7 @@ where
///
/// NB: It is not required that `node.lru_index()` is up-to-date
/// when entering this method.
fn promote_red_to_green(&mut self, node: &Arc<Node>, red_index: usize) {
fn promote_red_to_green(&mut self, node: &Arc<Node>, red_index: u16) {
debug_assert!(self.red_zone().contains(&red_index));
// Pick a yellow at random and switch places with it.
@ -242,12 +243,12 @@ where
let yellow_index = self.pick_index(self.yellow_zone());
tracing::debug!(
"demoting yellow node {:?} from {} to red at {}",
self.entries[yellow_index],
self.entries[yellow_index as usize],
yellow_index,
red_index,
);
self.entries.swap(yellow_index, red_index);
self.entries[red_index].lru_index().store(red_index);
self.entries.swap(yellow_index as usize, red_index as usize);
self.entries[red_index as usize].lru_index().store(red_index);
// Now move ourselves up into the green zone.
self.promote_yellow_to_green(node, yellow_index);
@ -259,51 +260,51 @@ where
///
/// NB: It is not required that `node.lru_index()` is up-to-date
/// when entering this method.
fn promote_yellow_to_green(&mut self, node: &Arc<Node>, yellow_index: usize) {
fn promote_yellow_to_green(&mut self, node: &Arc<Node>, yellow_index: u16) {
debug_assert!(self.yellow_zone().contains(&yellow_index));
// Pick a yellow at random and switch places with it.
let green_index = self.pick_index(self.green_zone());
tracing::debug!(
"demoting green node {:?} from {} to yellow at {}",
self.entries[green_index],
self.entries[green_index as usize],
green_index,
yellow_index
);
self.entries.swap(green_index, yellow_index);
self.entries[yellow_index].lru_index().store(yellow_index);
self.entries.swap(green_index as usize, yellow_index as usize);
self.entries[yellow_index as usize].lru_index().store(yellow_index);
node.lru_index().store(green_index);
tracing::debug!("promoted {:?} to green index {}", node, green_index);
}
fn pick_index(&mut self, zone: std::ops::Range<usize>) -> usize {
let end_index = std::cmp::min(zone.end, self.entries.len());
self.rng.rand_range(zone.start as u64..end_index as u64) as usize
fn pick_index(&mut self, zone: std::ops::Range<u16>) -> u16 {
let end_index = std::cmp::min(zone.end, self.entries.len() as u16);
self.rng.rand_range(zone.start as u64..end_index as u64) as u16
}
}
impl Default for LruIndex {
fn default() -> Self {
Self { index: AtomicUsize::new(usize::MAX) }
Self { index: AtomicU16::new(u16::MAX) }
}
}
impl LruIndex {
fn load(&self) -> usize {
fn load(&self) -> u16 {
self.index.load(Ordering::Acquire) // see note on ordering below
}
fn store(&self, value: usize) {
fn store(&self, value: u16) {
self.index.store(value, Ordering::Release) // see note on ordering below
}
fn clear(&self) {
self.store(usize::MAX);
self.store(u16::MAX);
}
fn is_in_lru(&self) -> bool {
self.load() != usize::MAX
self.load() != u16::MAX
}
}

View file

@ -12,8 +12,9 @@ use std::fmt::Debug;
use std::hash::Hash;
use triomphe::Arc;
pub use crate::derived::DependencyStorage;
pub use crate::derived::MemoizedStorage;
pub use crate::derived_lru::DependencyStorage as LruDependencyStorage;
pub use crate::derived_lru::MemoizedStorage as LruMemoizedStorage;
pub use crate::input::{InputStorage, UnitInputStorage};
pub use crate::interned::InternedStorage;
pub use crate::interned::LookupInternedStorage;
@ -228,7 +229,7 @@ where
/// that is, storage whose value is not derived from other storage but
/// is set independently.
pub trait LruQueryStorageOps {
fn set_lru_capacity(&self, new_capacity: usize);
fn set_lru_capacity(&self, new_capacity: u16);
}
pub trait DerivedQueryStorageOps<Q>

View file

@ -24,7 +24,9 @@ impl Drop for HotPotato {
#[salsa::query_group(QueryGroupStorage)]
trait QueryGroup: salsa::Database {
#[salsa::lru]
fn get(&self, x: u32) -> Arc<HotPotato>;
#[salsa::lru]
fn get_volatile(&self, x: u32) -> usize;
}

View file

@ -2252,7 +2252,8 @@
"null",
"integer"
],
"minimum": 0
"minimum": 0,
"maximum": 65535
}
}
},