349: GC tracked structs r=nikomatsakis a=nikomatsakis

Extends the system to track when a tracked struct is no longer created in a new revision and eagerly delete data associated with it. It's not a *complete* answer for GC but it seems pretty useful. 

Fix #315 

Co-authored-by: Niko Matsakis <niko@alum.mit.edu>
This commit is contained in:
bors[bot] 2022-08-16 21:59:48 +00:00 committed by GitHub
commit c24ab8ffc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 676 additions and 132 deletions

View file

@ -116,7 +116,7 @@ fn ingredients_for_impl(args: &Args, struct_ty: &syn::Type, data_ty: &syn::Type)
where
DB: salsa::DbWithJar<Self::Jar> + salsa::storage::JarFromJars<Self::Jar>,
{
let index = routes.push_mut(
let index = routes.push(
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
<_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar)

View file

@ -150,6 +150,11 @@ fn has_jars_dyn_impl(input: &syn::ItemStruct, storage: &syn::Ident) -> syn::Item
let ingredient = self.#storage.ingredient(stale_output.ingredient_index());
ingredient.remove_stale_output(self, executor, stale_output.key_index());
}
fn salsa_struct_deleted(&self, ingredient: salsa::IngredientIndex, id: salsa::Id) {
let ingredient = self.#storage.ingredient(ingredient);
ingredient.salsa_struct_deleted(self, id);
}
}
}
}

View file

@ -159,6 +159,11 @@ impl InputStruct {
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
&ingredients.#all_field_indices
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient_mut(jar);
&mut ingredients.#all_field_indices
},
);
salsa::input_field::InputFieldIngredient::new(index)
},
@ -170,6 +175,11 @@ impl InputStruct {
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
&ingredients.#input_index
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient_mut(jar);
&mut ingredients.#input_index
},
);
salsa::input::InputIngredient::new(index)
},
@ -205,6 +215,10 @@ impl InputStruct {
where
DB: ?Sized + salsa::DbWithJar<#jar_ty>,
{
fn register_dependent_fn(_db: &DB, _index: salsa::routes::IngredientIndex) {
// Do nothing here, at least for now.
// If/when we add ability to delete inputs, this would become relevant.
}
}
}
}

View file

@ -136,6 +136,10 @@ impl InternedStruct {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
<_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar)
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
<_ as salsa::storage::HasIngredientsFor<Self>>::ingredient_mut(jar)
},
);
salsa::interned::InternedIngredient::new(index)
}
@ -152,6 +156,10 @@ impl InternedStruct {
where
DB: ?Sized + salsa::DbWithJar<#jar_ty>,
{
fn register_dependent_fn(_db: &DB, _index: salsa::routes::IngredientIndex) {
// Do nothing here, at least for now.
// If/when we add ability to delete inputs, this would become relevant.
}
}
}
}

View file

@ -205,12 +205,20 @@ fn ingredients_for_impl(
let intern_map: syn::Expr = if requires_interning(item_fn) {
parse_quote! {
{
let index = routes.push(|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
&ingredients.intern_map
});
let index = routes.push(
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
&ingredients.intern_map
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient_mut(jar);
&mut ingredients.intern_map
}
);
salsa::interned::InternedIngredient::new(index)
}
}
@ -233,12 +241,19 @@ fn ingredients_for_impl(
intern_map: #intern_map,
function: {
let index = routes.push(|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
&ingredients.function
});
let index = routes.push(
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient(jar);
&ingredients.function
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
let ingredients =
<_ as salsa::storage::HasIngredientsFor<Self::Ingredients>>::ingredient_mut(jar);
&mut ingredients.function
});
salsa::function::FunctionIngredient::new(index)
},
}

View file

@ -182,12 +182,17 @@ impl TrackedStruct {
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
&ingredients.#value_field_indices
},
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars_mut(jars);
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient_mut(jar);
&mut ingredients.#value_field_indices
},
);
salsa::function::FunctionIngredient::new(index)
},
)*
{
let index = routes.push_mut(
let index = routes.push(
|jars| {
let jar = <DB as salsa::storage::JarFromJars<Self::Jar>>::jar_from_jars(jars);
let ingredients = <_ as salsa::storage::HasIngredientsFor<Self>>::ingredient(jar);
@ -211,11 +216,17 @@ impl TrackedStruct {
fn salsa_struct_in_db_impl(&self) -> syn::ItemImpl {
let ident = self.id_ident();
let jar_ty = self.jar_ty();
let tracked_struct_index: Literal = self.tracked_struct_index();
parse_quote! {
impl<DB> salsa::salsa_struct::SalsaStructInDb<DB> for #ident
where
DB: ?Sized + salsa::DbWithJar<#jar_ty>,
{
fn register_dependent_fn(db: &DB, index: salsa::routes::IngredientIndex) {
let (jar, _) = <_ as salsa::storage::HasJar<#jar_ty>>::jar(db);
let ingredients = <#jar_ty as salsa::storage::HasIngredientsFor<#ident>>::ingredient(jar);
ingredients.#tracked_struct_index.register_dependent_fn(index)
}
}
}
}

View file

@ -1,7 +1,7 @@
use crate::{
cycle::CycleRecoveryStrategy,
hash::FxDashMap,
ingredient::{Ingredient, MutIngredient},
ingredient::{Ingredient, IngredientRequiresReset},
key::DependencyIndex,
runtime::{local_state::QueryOrigin, StampedValue},
storage::HasJar,
@ -96,14 +96,19 @@ where
// FIXME
drop((executor, stale_output_key));
}
fn reset_for_new_revision(&mut self) {
panic!("unexpected reset on accumulator")
}
fn salsa_struct_deleted(&self, _db: &DB, _id: crate::Id) {
panic!("unexpected call: accumulator is not registered as a dependent fn");
}
}
impl<DB: ?Sized, Data> MutIngredient<DB> for AccumulatorIngredient<Data>
impl<Data> IngredientRequiresReset for AccumulatorIngredient<Data>
where
Data: Clone,
{
fn reset_for_new_revision(&mut self) {
// FIXME: We could certainly drop things here if we knew which ones
// to drop. There's a fixed point algorithm we could be doing.
}
const RESET_ON_NEW_REVISION: bool = false;
}

View file

@ -83,6 +83,12 @@ pub enum EventKind {
/// Key for the query that is no longer output
output_key: DatabaseKeyIndex,
},
/// Tracked structs or memoized data were discarded (freed).
DidDiscard {
/// Value being discarded.
key: DatabaseKeyIndex,
},
}
impl fmt::Debug for EventKind {
@ -113,6 +119,9 @@ impl fmt::Debug for EventKind {
.field("execute_key", &execute_key)
.field("output_key", &output_key)
.finish(),
EventKind::DidDiscard { key } => {
fmt.debug_struct("DidDiscard").field("key", &key).finish()
}
}
}
}
@ -148,6 +157,10 @@ where
.field("execute_key", &execute_key.debug(db))
.field("output_key", &output_key.debug(db))
.finish(),
EventKind::DidDiscard { key } => fmt
.debug_struct("DidDiscard")
.field("key", &key.debug(db))
.finish(),
}
}
}

View file

@ -1,16 +1,16 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use crossbeam::queue::SegQueue;
use crossbeam::{atomic::AtomicCell, queue::SegQueue};
use crate::{
cycle::CycleRecoveryStrategy,
ingredient::MutIngredient,
ingredient::IngredientRequiresReset,
jar::Jar,
key::{DatabaseKeyIndex, DependencyIndex},
runtime::local_state::QueryOrigin,
salsa_struct::SalsaStructInDb,
Cycle, DbWithJar, Id, Revision,
Cycle, DbWithJar, Event, EventKind, Id, Revision,
};
use super::{ingredient::Ingredient, routes::IngredientIndex, AsId};
@ -67,6 +67,10 @@ pub struct FunctionIngredient<C: Configuration> {
/// we don't know that we can trust the database to give us the same runtime
/// everytime and so forth.
deleted_entries: SegQueue<ArcSwap<memo::Memo<C::Value>>>,
/// Set to true once we invoke `register_dependent_fn` for `C::SalsaStruct`.
/// Prevents us from registering more than once.
registered: AtomicCell<bool>,
}
pub trait Configuration {
@ -140,6 +144,7 @@ where
lru: Default::default(),
sync_map: Default::default(),
deleted_entries: Default::default(),
registered: Default::default(),
}
}
@ -169,7 +174,13 @@ where
std::mem::transmute(memo_value)
}
fn insert_memo(&self, key: C::Key, memo: memo::Memo<C::Value>) -> Option<&C::Value> {
fn insert_memo(
&self,
db: &DynDb<'_, C>,
key: C::Key,
memo: memo::Memo<C::Value>,
) -> Option<&C::Value> {
self.register(db);
let memo = Arc::new(memo);
let value = unsafe {
// Unsafety conditions: memo must be in the map (it's not yet, but it will be by the time this
@ -183,6 +194,15 @@ where
}
value
}
/// Register this function as a dependent fn of the given salsa struct.
/// When instances of that salsa struct are deleted, we'll get a callback
/// so we can remove any data keyed by them.
fn register(&self, db: &DynDb<'_, C>) {
if !self.registered.fetch_or(true) {
<C::SalsaStruct as SalsaStructInDb<_>>::register_dependent_fn(db, self.index)
}
}
}
impl<DB, C> Ingredient<DB> for FunctionIngredient<C>
@ -220,14 +240,35 @@ where
// but not in rev 2. We don't do anything in this case, we just leave the (now stale) memo.
// Since its `verified_at` field has not changed, it will be considered dirty if it is invoked.
}
}
impl<DB, C> MutIngredient<DB> for FunctionIngredient<C>
where
DB: ?Sized + DbWithJar<C::Jar>,
C: Configuration,
{
fn reset_for_new_revision(&mut self) {
std::mem::take(&mut self.deleted_entries);
}
fn salsa_struct_deleted(&self, db: &DB, id: crate::Id) {
// Remove any data keyed by `id`, since `id` no longer
// exists in this revision.
let id: C::Key = C::key_from_id(id);
if let Some(origin) = self.delete_memo(id) {
let key = self.database_key_index(id);
db.salsa_event(Event {
runtime_id: db.salsa_runtime().id(),
kind: EventKind::DidDiscard { key },
});
// Anything that was output by this memoized execution
// is now itself stale.
for stale_output in origin.outputs() {
db.remove_stale_output(key, stale_output)
}
}
}
}
impl<C> IngredientRequiresReset for FunctionIngredient<C>
where
C: Configuration,
{
const RESET_ON_NEW_REVISION: bool = true;
}

View file

@ -1,3 +1,5 @@
use crate::runtime::local_state::QueryOrigin;
use super::{Configuration, FunctionIngredient};
impl<C> FunctionIngredient<C>
@ -6,9 +8,13 @@ where
{
/// Removes the memoized value for `key` from the memo-map.
/// Pushes the memo onto `deleted_entries` to ensure that any references into that memo which were handed out remain valid.
pub(super) fn delete_memo(&self, key: C::Key) {
pub(super) fn delete_memo(&self, key: C::Key) -> Option<QueryOrigin> {
if let Some(memo) = self.memo_map.remove(key) {
let origin = memo.load().revisions.origin.clone();
self.deleted_entries.push(memo);
Some(origin)
} else {
None
}
}
}

View file

@ -1,6 +1,6 @@
use crate::{
key::DependencyIndex, runtime::local_state::QueryRevisions, storage::HasJarsDyn, Database,
DatabaseKeyIndex, Event, EventKind,
runtime::local_state::QueryRevisions, storage::HasJarsDyn, Database, DatabaseKeyIndex, Event,
EventKind,
};
use super::{memo::Memo, Configuration, DynDb, FunctionIngredient};

View file

@ -91,7 +91,11 @@ where
}
let value = self
.insert_memo(key, Memo::new(Some(value), revision_now, revisions.clone()))
.insert_memo(
db,
key,
Memo::new(Some(value), revision_now, revisions.clone()),
)
.unwrap();
let stamped_value = revisions.stamped_value(value);

View file

@ -75,7 +75,7 @@ where
};
log::debug!("specify: about to add memo {:#?} for key {:?}", memo, key);
self.insert_memo(key, memo);
self.insert_memo(db, key, memo);
}
/// Specify the value for `key` but do not record it is an output.

View file

@ -33,13 +33,28 @@ pub trait Ingredient<DB: ?Sized> {
///
/// This hook is used to clear out the stale value so others cannot read it.
fn remove_stale_output(&self, db: &DB, executor: DatabaseKeyIndex, stale_output_key: Id);
}
/// Optional trait for ingredients that wish to be notified when new revisions are
/// about to occur. If ingredients wish to receive these method calls,
/// they need to indicate that by invoking [`Routes::push_mut`] during initialization.
pub trait MutIngredient<DB: ?Sized>: Ingredient<DB> {
/// Invoked when a new revision is about to start. This gives ingredients
/// a chance to flush data and so forth.
/// Informs the ingredient `self` that the salsa struct with id `id` has been deleted.
/// This gives `self` a chance to remove any memoized data dependent on `id`.
/// To receive this callback, `self` must register itself as a dependent function using
/// [`SalsaStructInDb::register_dependent_fn`](`crate::salsa_struct::SalsaStructInDb::register_dependent_fn`).
fn salsa_struct_deleted(&self, db: &DB, id: Id);
/// Invoked when a new revision is about to start.
/// This moment is important because it means that we have an `&mut`-reference to the database,
/// and hence any pre-existing `&`-references must have expired.
/// Many ingredients, given an `&'db`-reference to the database,
/// use unsafe code to return `&'db`-references to internal values.
/// The backing memory for those values can only be freed once an `&mut`-reference to the database is created.
///
/// **Important:** to actually receive resets, the ingredient must set
/// [`IngredientRequiresReset::RESET_ON_NEW_REVISION`] to true.
fn reset_for_new_revision(&mut self);
}
/// Defines a const indicating if an ingredient needs to be reset each round.
/// This const probably *should* be a member of `Ingredient` trait but then `Ingredient` would not be dyn-safe.
pub trait IngredientRequiresReset {
/// If this is true, then `reset_for_new_revision` will be called every new revision.
const RESET_ON_NEW_REVISION: bool;
}

View file

@ -0,0 +1,83 @@
use std::sync::Arc;
use arc_swap::{ArcSwapOption, AsRaw};
use crate::IngredientIndex;
/// A list of ingredients that can be added to in parallel.
pub(crate) struct IngredientList {
/// A list of each tracked functions.
/// tracked struct.
///
/// Whenever an instance `i` of this struct is deleted,
/// each of these functions will be notified
/// so they can remove any data tied to that instance.
list: ArcSwapOption<Vec<IngredientIndex>>,
}
impl IngredientList {
pub fn new() -> Self {
Self {
list: ArcSwapOption::new(None),
}
}
/// Returns an iterator over the items in the list.
/// This is a snapshot of the list as it was when this function is called.
/// Items could still be added in parallel via `add_ingredient`
/// that will not be returned by this iterator.
pub(crate) fn iter(&self) -> impl Iterator<Item = IngredientIndex> {
let guard = self.list.load();
let mut index = 0;
std::iter::from_fn(move || match &*guard {
Some(list) if index < list.len() => {
let r = list[index];
index += 1;
Some(r)
}
_ => None,
})
}
/// Adds an ingredient to the list (if not already present).
pub(crate) fn push(&self, index: IngredientIndex) {
// This function is called whenever a value is stored,
// so other tracked functions and things may be executing,
// and there could even be two calls to this function in parallel.
//
// We use a "compare-and-swap" strategy of reading the old vector, creating a new vector,
// and then installing it, hoping that nobody has conflicted with us.
// If that fails, we start over.
loop {
let guard = self.list.load();
let empty_vec = vec![];
let old_vec = match &*guard {
Some(v) => v,
None => &empty_vec,
};
// First check whether the index is already present.
if old_vec.contains(&index) {
return;
}
// If not, construct a new vector that has all the old values, followed by `index`.
let vec: Arc<Vec<IngredientIndex>> = Arc::new(
old_vec
.iter()
.copied()
.chain(std::iter::once(index))
.collect(),
);
// Try to replace the old vector with the new one. If we fail, loop around again.
assert_eq!(vec.len(), vec.capacity());
let previous = self.list.compare_and_swap(&guard, Some(vec));
if guard.as_raw() == previous.as_raw() {
// swap was successful
break;
}
}
}
}

View file

@ -1,6 +1,6 @@
use crate::{
cycle::CycleRecoveryStrategy,
ingredient::Ingredient,
ingredient::{Ingredient, IngredientRequiresReset},
key::{DatabaseKeyIndex, DependencyIndex},
runtime::{local_state::QueryOrigin, Runtime},
AsId, IngredientIndex, Revision,
@ -80,4 +80,21 @@ where
executor, stale_output_key
);
}
fn reset_for_new_revision(&mut self) {
panic!("unexpected call to `reset_for_new_revision`")
}
fn salsa_struct_deleted(&self, _db: &DB, _id: crate::Id) {
panic!(
"unexpected call: input ingredients do not register for salsa struct deletion events"
);
}
}
impl<Id> IngredientRequiresReset for InputIngredient<Id>
where
Id: InputId,
{
const RESET_ON_NEW_REVISION: bool = false;
}

View file

@ -1,5 +1,5 @@
use crate::cycle::CycleRecoveryStrategy;
use crate::ingredient::Ingredient;
use crate::ingredient::{Ingredient, IngredientRequiresReset};
use crate::key::DependencyIndex;
use crate::runtime::local_state::QueryOrigin;
use crate::runtime::StampedValue;
@ -93,4 +93,19 @@ where
fn mark_validated_output(&self, _db: &DB, _executor: DatabaseKeyIndex, _output_key: Id) {}
fn remove_stale_output(&self, _db: &DB, _executor: DatabaseKeyIndex, _stale_output_key: Id) {}
fn salsa_struct_deleted(&self, _db: &DB, _id: Id) {
panic!("unexpected call: input fields are never deleted");
}
fn reset_for_new_revision(&mut self) {
panic!("unexpected call: input fields don't register for resets");
}
}
impl<K, F> IngredientRequiresReset for InputFieldIngredient<K, F>
where
K: AsId,
{
const RESET_ON_NEW_REVISION: bool = false;
}

View file

@ -5,6 +5,7 @@ use std::marker::PhantomData;
use crate::durability::Durability;
use crate::id::AsId;
use crate::ingredient::IngredientRequiresReset;
use crate::key::DependencyIndex;
use crate::runtime::local_state::QueryOrigin;
use crate::runtime::Runtime;
@ -217,6 +218,25 @@ where
executor, stale_output_key
);
}
fn reset_for_new_revision(&mut self) {
// Interned ingredients do not, normally, get deleted except when they are "reset" en masse.
// There ARE methods (e.g., `clear_deleted_entries` and `remove`) for deleting individual
// items, but those are only used for tracked struct ingredients.
panic!("unexpected call to `reset_for_new_revision`")
}
fn salsa_struct_deleted(&self, _db: &DB, _id: crate::Id) {
panic!("unexpected call: interned ingredients do not register for salsa struct deletion events");
}
}
impl<Id, Data> IngredientRequiresReset for InternedIngredient<Id, Data>
where
Id: InternedId,
Data: InternedData,
{
const RESET_ON_NEW_REVISION: bool = false;
}
pub struct IdentityInterner<Id: AsId> {

View file

@ -9,6 +9,7 @@ pub mod function;
pub mod hash;
pub mod id;
pub mod ingredient;
pub mod ingredient_list;
pub mod input;
pub mod input_field;
pub mod interned;

View file

@ -1,7 +1,6 @@
use super::{
ingredient::{Ingredient, MutIngredient},
storage::HasJars,
};
use crate::ingredient::IngredientRequiresReset;
use super::{ingredient::Ingredient, storage::HasJars};
/// An ingredient index identifies a particular [`Ingredient`] in the database.
/// The database contains a number of jars, and each jar contains a number of ingredients.
@ -39,7 +38,7 @@ pub type DynRoute<DB: HasJars> = dyn Fn(&DB::Jars) -> (&dyn Ingredient<DB>) + Se
#[allow(type_alias_bounds)]
#[allow(unused_parens)]
pub type DynMutRoute<DB: HasJars> =
dyn Fn(&mut DB::Jars) -> (&mut dyn MutIngredient<DB>) + Send + Sync;
dyn Fn(&mut DB::Jars) -> (&mut dyn Ingredient<DB>) + Send + Sync;
/// The "routes" structure is used to navigate the database.
/// The database contains a number of jars, and each jar contains a number of ingredients.
@ -52,13 +51,10 @@ pub struct Routes<DB: HasJars> {
/// Vector indexed by ingredient index. Yields the `DynRoute`,
/// a function which can be applied to the `DB::Jars` to yield
/// the `dyn Ingredient.
routes: Vec<Box<DynRoute<DB>>>,
routes: Vec<(Box<DynRoute<DB>>, Box<DynMutRoute<DB>>)>,
/// Vector if "mut routes". This vector is used to give callbacks
/// when new revisions are trigged. It is not indexed by ingredient
/// index as not every ingredient needs a callback; instead, you
/// just iterate over it and invoke each fn to get a `MutIngredient`.
mut_routes: Vec<Box<DynMutRoute<DB>>>,
/// Indices of routes which need a 'reset' call.
needs_reset: Vec<IngredientIndex>,
}
impl<DB: HasJars> Routes<DB> {
@ -66,7 +62,7 @@ impl<DB: HasJars> Routes<DB> {
pub(super) fn new() -> Self {
Routes {
routes: vec![],
mut_routes: vec![],
needs_reset: vec![],
}
}
@ -77,52 +73,53 @@ impl<DB: HasJars> Routes<DB> {
///
/// # Parameters
///
/// * `requires_reset` -- if true, the [`Ingredient::reset_for_new_revision`] method will be called on this ingredient
/// at each new revision. See that method for more information.
/// * `route` -- a closure which, given a database, will identify the ingredient.
/// This closure will be invoked to dispatch calls to `maybe_changed_after`.
/// * `mut_route` -- an optional closure which identifies the ingredient in a mut
/// * `mut_route` -- a closure which identifies the ingredient in a mut
/// database.
pub fn push(
pub fn push<I>(
&mut self,
route: impl (Fn(&DB::Jars) -> &dyn Ingredient<DB>) + Send + Sync + 'static,
) -> IngredientIndex {
route: impl (Fn(&DB::Jars) -> &I) + Send + Sync + 'static,
mut_route: impl (Fn(&mut DB::Jars) -> &mut I) + Send + Sync + 'static,
) -> IngredientIndex
where
I: Ingredient<DB> + IngredientRequiresReset + 'static,
{
let len = self.routes.len();
self.routes.push(Box::new(route));
self.routes.push((
Box::new(move |jars| route(jars)),
Box::new(move |jars| mut_route(jars)),
));
let index = IngredientIndex::from(len);
index
}
/// As [`Self::push`] but for an ingredient that wants a callback whenever
/// a new revision is published.
/// Many ingredients will, given an `&'db dyn Db` reference,
/// return a `&'db V` reference to one of their values.
/// This forces those values to live as long as the `&dyn Db` is valid.
/// This callback is invoked when a new revision begins.
/// It allows those values that were accessed to be freed.
/// We know it is safe to free them because, when a new revision starts,
/// we have an `&mut dyn Db`, and hence the `&dyn Db` lifetime must have ended.
pub fn push_mut(
&mut self,
route: impl (Fn(&DB::Jars) -> &dyn Ingredient<DB>) + Send + Sync + 'static,
mut_route: impl (Fn(&mut DB::Jars) -> &mut dyn MutIngredient<DB>) + Send + Sync + 'static,
) -> IngredientIndex {
let index = self.push(route);
self.mut_routes.push(Box::new(mut_route));
if I::RESET_ON_NEW_REVISION {
self.needs_reset.push(index);
}
index
}
/// Given an ingredient index, return the "route"
/// (a function that, given a `Jars`, returns the ingredient).
/// (a function that, given a `&Jars`, returns the ingredient).
pub fn route(&self, index: IngredientIndex) -> &dyn Fn(&DB::Jars) -> &dyn Ingredient<DB> {
&self.routes[index.as_usize()]
&self.routes[index.as_usize()].0
}
/// Returns the "mutable routes" for the purposes of giving callbacks when a new revision is published.
/// Not all ingredients need callbacks, so this returns an iterator of just those that do.
pub fn mut_routes(
/// Given an ingredient index, return the "mut route"
/// (a function that, given an `&mut Jars`, returns the ingredient).
pub fn route_mut(
&self,
) -> impl Iterator<Item = &dyn Fn(&mut DB::Jars) -> &mut dyn MutIngredient<DB>> + '_ {
self.mut_routes
.iter()
.map(|b| &**b as &dyn Fn(&mut DB::Jars) -> &mut dyn MutIngredient<DB>)
index: IngredientIndex,
) -> &dyn Fn(&mut DB::Jars) -> &mut dyn Ingredient<DB> {
&self.routes[index.as_usize()].1
}
/// Returns the mut routes for ingredients that need to be reset at the start of each revision.
pub fn reset_routes(
&self,
) -> impl Iterator<Item = &dyn Fn(&mut DB::Jars) -> &mut dyn Ingredient<DB>> + '_ {
self.needs_reset.iter().map(|&index| self.route_mut(index))
}
}

View file

@ -1,3 +1,5 @@
use crate::Database;
use crate::{Database, IngredientIndex};
pub trait SalsaStructInDb<DB: ?Sized + Database> {}
pub trait SalsaStructInDb<DB: ?Sized + Database> {
fn register_dependent_fn(db: &DB, index: IngredientIndex);
}

View file

@ -8,7 +8,7 @@ use crate::jar::Jar;
use crate::key::DependencyIndex;
use crate::runtime::local_state::QueryOrigin;
use crate::runtime::Runtime;
use crate::{Database, DatabaseKeyIndex, IngredientIndex};
use crate::{Database, DatabaseKeyIndex, Id, IngredientIndex};
use super::routes::Routes;
use super::{ParallelDatabase, Revision};
@ -94,7 +94,7 @@ where
let routes = self.routes.clone();
let shared = Arc::get_mut(&mut self.shared).unwrap();
for route in routes.mut_routes() {
for route in routes.reset_routes() {
route(&mut shared.jars).reset_for_new_revision();
}
@ -182,7 +182,19 @@ pub trait HasJarsDyn {
fn mark_validated_output(&self, executor: DatabaseKeyIndex, output: DatabaseKeyIndex);
/// Invoked when `executor` used to output `stale_output` but no longer does.
/// This method routes that into a call to the [`remove_stale_output`](`crate::ingredient::Ingredient::remove_stale_output`)
/// method on the ingredient for `stale_output`.
fn remove_stale_output(&self, executor: DatabaseKeyIndex, stale_output: DatabaseKeyIndex);
/// Informs `ingredient` that the salsa struct with id `id` has been deleted.
/// This means that `id` will not be used in this revision and hence
/// any memoized values keyed by that struct can be discarded.
///
/// In order to receive this callback, `ingredient` must have registered itself
/// as a dependent function using
/// [`SalsaStructInDb::register_dependent_fn`](`crate::salsa_struct::SalsaStructInDb::register_dependent_fn`).
fn salsa_struct_deleted(&self, ingredient: IngredientIndex, id: Id);
}
pub trait HasIngredientsFor<I>

View file

@ -1,11 +1,12 @@
use crate::{
cycle::CycleRecoveryStrategy,
ingredient::{Ingredient, MutIngredient},
ingredient::{Ingredient, IngredientRequiresReset},
ingredient_list::IngredientList,
interned::{InternedData, InternedId, InternedIngredient},
key::{DatabaseKeyIndex, DependencyIndex},
runtime::{local_state::QueryOrigin, Runtime},
salsa_struct::SalsaStructInDb,
Database, IngredientIndex, Revision,
Database, Event, IngredientIndex, Revision,
};
pub trait TrackedStructId: InternedId {}
@ -33,6 +34,14 @@ where
Data: TrackedStructData,
{
interned: InternedIngredient<Id, TrackedStructKey<Data>>,
/// A list of each tracked function `f` whose key is this
/// tracked struct.
///
/// Whenever an instance `i` of this struct is deleted,
/// each of these functions will be notified
/// so they can remove any data tied to that instance.
dependent_fns: IngredientList,
}
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)]
@ -53,6 +62,7 @@ where
pub fn new(index: IngredientIndex) -> Self {
Self {
interned: InternedIngredient::new(index),
dependent_fns: IngredientList::new(),
}
}
@ -94,17 +104,33 @@ where
/// Using this method on an entity id that MAY be used in the current revision will lead to
/// unspecified results (but not UB). See [`InternedIngredient::delete_index`] for more
/// discussion and important considerations.
pub(crate) fn delete_entities(&self, _runtime: &Runtime, ids: impl Iterator<Item = Id>) {
for id in ids {
self.interned.delete_index(id);
pub(crate) fn delete_entity(&self, db: &dyn crate::Database, id: Id) {
db.salsa_event(Event {
runtime_id: db.salsa_runtime().id(),
kind: crate::EventKind::DidDiscard {
key: self.database_key_index(id),
},
});
self.interned.delete_index(id);
for dependent_fn in self.dependent_fns.iter() {
db.salsa_struct_deleted(dependent_fn, id.as_id());
}
}
/// Adds a dependent function (one keyed by this tracked struct) to our list.
/// When instances of this struct are deleted, these dependent functions
/// will be notified.
pub fn register_dependent_fn(&self, index: IngredientIndex) {
self.dependent_fns.push(index);
}
}
impl<DB: ?Sized, Id, Data> Ingredient<DB> for TrackedStructIngredient<Id, Data>
where
Id: TrackedStructId,
Data: TrackedStructData,
DB: crate::Database,
{
fn maybe_changed_after(&self, db: &DB, input: DependencyIndex, revision: Revision) -> bool {
self.interned.maybe_changed_after(db, input, revision)
@ -125,22 +151,31 @@ where
fn remove_stale_output(
&self,
_db: &DB,
executor: DatabaseKeyIndex,
db: &DB,
_executor: DatabaseKeyIndex,
stale_output_key: crate::Id,
) {
let key: Id = Id::from_id(stale_output_key);
// FIXME -- we can delete this entity
drop((executor, key));
// This method is called when, in prior revisions,
// `executor` creates a tracked struct `salsa_output_key`,
// but it did not in the current revision.
// In that case, we can delete `stale_output_key` and any data associated with it.
let stale_output_key: Id = Id::from_id(stale_output_key);
self.delete_entity(db.as_salsa_database(), stale_output_key);
}
fn reset_for_new_revision(&mut self) {
self.interned.clear_deleted_indices();
}
fn salsa_struct_deleted(&self, _db: &DB, _id: crate::Id) {
panic!("unexpected call: interned ingredients do not register for salsa struct deletion events");
}
}
impl<DB: ?Sized, Id, Data> MutIngredient<DB> for TrackedStructIngredient<Id, Data>
impl<Id, Data> IngredientRequiresReset for TrackedStructIngredient<Id, Data>
where
Id: TrackedStructId,
Data: TrackedStructData,
{
fn reset_for_new_revision(&mut self) {
self.interned.clear_deleted_indices();
}
const RESET_ON_NEW_REVISION: bool = true;
}

View file

@ -0,0 +1,134 @@
//! Delete cascade:
//!
//! * when we delete memoized data, also delete outputs from that data
use salsa::DebugWithDb;
use salsa_2022_tests::{HasLogger, Logger};
use expect_test::expect;
use test_log::test;
#[salsa::jar(db = Db)]
struct Jar(
MyInput,
MyTracked,
final_result,
create_tracked_structs,
contribution_from_struct,
copy_field,
);
trait Db: salsa::DbWithJar<Jar> + HasLogger {}
#[salsa::input]
struct MyInput {
field: u32,
}
#[salsa::tracked]
fn final_result(db: &dyn Db, input: MyInput) -> u32 {
db.push_log(format!("final_result({:?})", input));
let mut sum = 0;
for tracked_struct in create_tracked_structs(db, input) {
sum += contribution_from_struct(db, tracked_struct);
}
sum
}
#[salsa::tracked]
struct MyTracked {
field: u32,
}
#[salsa::tracked]
fn create_tracked_structs(db: &dyn Db, input: MyInput) -> Vec<MyTracked> {
db.push_log(format!("intermediate_result({:?})", input));
(0..input.field(db))
.map(|i| MyTracked::new(db, i))
.collect()
}
#[salsa::tracked]
fn contribution_from_struct(db: &dyn Db, tracked: MyTracked) -> u32 {
let m = MyTracked::new(db, tracked.field(db));
copy_field(db, m) * 2
}
#[salsa::tracked]
fn copy_field(db: &dyn Db, tracked: MyTracked) -> u32 {
tracked.field(db)
}
#[salsa::db(Jar)]
#[derive(Default)]
struct Database {
storage: salsa::Storage<Self>,
logger: Logger,
}
impl salsa::Database for Database {
fn salsa_event(&self, event: salsa::Event) {
match event.kind {
salsa::EventKind::WillDiscardStaleOutput { .. }
| salsa::EventKind::DidDiscard { .. } => {
self.push_log(format!("salsa_event({:?})", event.kind.debug(self)));
}
_ => {}
}
}
fn salsa_runtime(&self) -> &salsa::Runtime {
self.storage.runtime()
}
}
impl Db for Database {}
impl HasLogger for Database {
fn logger(&self) -> &Logger {
&self.logger
}
}
#[test]
fn basic() {
let mut db = Database::default();
// Creates 3 tracked structs
let input = MyInput::new(&mut db, 3);
assert_eq!(final_result(&db, input), 2 * 2 + 1 * 2 + 0 * 2);
db.assert_logs(expect![[r#"
[
"final_result(MyInput(Id { value: 1 }))",
"intermediate_result(MyInput(Id { value: 1 }))",
]"#]]);
// Creates only 2 tracked structs in this revision, should delete 1
//
// Expect to see 6 DidDiscard events. Three from the primary struct:
//
// * the struct itself
// * the struct's field
// * the `contribution_from_struct` result
//
// and then 3 more from the struct created by `contribution_from_struct`:
//
// * the struct itself
// * the struct's field
// * the `copy_field` result
input.set_field(&mut db, 2);
assert_eq!(final_result(&db, input), 1 * 2 + 0 * 2);
db.assert_logs(expect![[r#"
[
"intermediate_result(MyInput(Id { value: 1 }))",
"salsa_event(WillDiscardStaleOutput { execute_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) }, output_key: DependencyIndex { ingredient_index: IngredientIndex(3), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(3), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(2), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(3), key_index: Some(Id { value: 6 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(2), key_index: Some(Id { value: 6 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(7), key_index: Some(Id { value: 6 }) } })",
"final_result(MyInput(Id { value: 1 }))",
]"#]]);
}

View file

@ -0,0 +1,117 @@
//! Basic deletion test:
//!
//! * entities not created in a revision are deleted, as is any memoized data keyed on them.
use salsa::DebugWithDb;
use salsa_2022_tests::{HasLogger, Logger};
use expect_test::expect;
use test_log::test;
#[salsa::jar(db = Db)]
struct Jar(
MyInput,
MyTracked,
final_result,
create_tracked_structs,
contribution_from_struct,
);
trait Db: salsa::DbWithJar<Jar> + HasLogger {}
#[salsa::input]
struct MyInput {
field: u32,
}
#[salsa::tracked]
fn final_result(db: &dyn Db, input: MyInput) -> u32 {
db.push_log(format!("final_result({:?})", input));
let mut sum = 0;
for tracked_struct in create_tracked_structs(db, input) {
sum += contribution_from_struct(db, tracked_struct);
}
sum
}
#[salsa::tracked]
struct MyTracked {
field: u32,
}
#[salsa::tracked]
fn create_tracked_structs(db: &dyn Db, input: MyInput) -> Vec<MyTracked> {
db.push_log(format!("intermediate_result({:?})", input));
(0..input.field(db))
.map(|i| MyTracked::new(db, i))
.collect()
}
#[salsa::tracked]
fn contribution_from_struct(db: &dyn Db, tracked: MyTracked) -> u32 {
tracked.field(db) * 2
}
#[salsa::db(Jar)]
#[derive(Default)]
struct Database {
storage: salsa::Storage<Self>,
logger: Logger,
}
impl salsa::Database for Database {
fn salsa_event(&self, event: salsa::Event) {
match event.kind {
salsa::EventKind::WillDiscardStaleOutput { .. }
| salsa::EventKind::DidDiscard { .. } => {
self.push_log(format!("salsa_event({:?})", event.kind.debug(self)));
}
_ => {}
}
}
fn salsa_runtime(&self) -> &salsa::Runtime {
self.storage.runtime()
}
}
impl Db for Database {}
impl HasLogger for Database {
fn logger(&self) -> &Logger {
&self.logger
}
}
#[test]
fn basic() {
let mut db = Database::default();
// Creates 3 tracked structs
let input = MyInput::new(&mut db, 3);
assert_eq!(final_result(&db, input), 2 * 2 + 1 * 2 + 0 * 2);
db.assert_logs(expect![[r#"
[
"final_result(MyInput(Id { value: 1 }))",
"intermediate_result(MyInput(Id { value: 1 }))",
]"#]]);
// Creates only 2 tracked structs in this revision, should delete 1
//
// Expect to see 3 DidDiscard events--
//
// * the struct itself
// * the struct's field
// * the `contribution_from_struct` result
input.set_field(&mut db, 2);
assert_eq!(final_result(&db, input), 1 * 2 + 0 * 2);
db.assert_logs(expect![[r#"
[
"intermediate_result(MyInput(Id { value: 1 }))",
"salsa_event(WillDiscardStaleOutput { execute_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) }, output_key: DependencyIndex { ingredient_index: IngredientIndex(3), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(3), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(2), key_index: Some(Id { value: 3 }) } })",
"salsa_event(DidDiscard { key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 3 }) } })",
"final_result(MyInput(Id { value: 1 }))",
]"#]]);
}

View file

@ -104,7 +104,6 @@ fn test_run_0() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -126,7 +125,6 @@ fn test_run_5() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -148,14 +146,12 @@ fn test_run_10() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
]"#]]);
}
@ -174,14 +170,12 @@ fn test_run_20() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
]"#]]);
}
@ -204,7 +198,6 @@ fn test_run_0_then_5_then_20() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -218,14 +211,12 @@ fn test_run_0_then_5_then_20() {
assert_eq!(final_result(&db, input), 100);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(7), key_index: Some(Id { value: 1 }) } } }",
]"#]]);
@ -239,19 +230,16 @@ fn test_run_0_then_5_then_20() {
assert_eq!(final_result(&db, input), 200);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillDiscardStaleOutput { execute_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) }, output_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -281,7 +269,6 @@ fn test_run_0_then_5_then_10_then_20() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -295,14 +282,12 @@ fn test_run_0_then_5_then_10_then_20() {
assert_eq!(final_result(&db, input), 100);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(7), key_index: Some(Id { value: 1 }) } } }",
]"#]]);
@ -316,19 +301,16 @@ fn test_run_0_then_5_then_10_then_20() {
assert_eq!(final_result(&db, input), 100);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillDiscardStaleOutput { execute_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) }, output_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: DependencyIndex { ingredient_index: IngredientIndex(7), key_index: Some(Id { value: 1 }) } } }",
]"#]]);
@ -340,7 +322,6 @@ fn test_run_0_then_5_then_10_then_20() {
assert_eq!(final_result(&db, input), 200);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
@ -348,12 +329,9 @@ fn test_run_0_then_5_then_10_then_20() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -379,7 +357,6 @@ fn test_run_5_then_20() {
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
@ -389,19 +366,16 @@ fn test_run_5_then_20() {
assert_eq!(final_result(&db, input), 200);
db.assert_logs(expect![[r#"
[
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) } } }",
"create_tracked(MyInput(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillDiscardStaleOutput { execute_key: DependencyIndex { ingredient_index: IngredientIndex(6), key_index: Some(Id { value: 1 }) }, output_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(4), key_index: Some(Id { value: 1 }) } } }",
"maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: DependencyIndex { ingredient_index: IngredientIndex(5), key_index: Some(Id { value: 1 }) } } }",
"read_maybe_specified(MyTracked(Id { value: 1 }))",
"Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",