use std::{
any::Any,
cell::RefCell,
collections::{HashMap, HashSet, VecDeque},
rc::Rc,
};
use slotmap::SlotMap;
#[cfg(debug_assertions)]
use slotmap::SparseSecondaryMap;
use crate::{LocalProperty, TransitionManager, TransitionQueueEntry};
use super::{private::PropertyId, PropertyValue};
#[derive(Clone, Debug, Default)]
pub struct EffectDrainReport {
pub ran: usize,
pub popped: usize,
pub skipped_clean: usize,
pub skipped_unregistered: usize,
pub skipped_missing: usize,
pub remaining: usize,
pub cutoffs_evaluated: usize,
pub cutoffs_suppressed: usize,
pub cutoffs_propagated: usize,
pub remaining_cutoffs: usize,
pub budget_exhausted: bool,
pub top_effects: Vec<(String, usize)>,
pub top_cutoffs: Vec<(String, usize)>,
}
thread_local! {
pub(crate) static PROPERTY_TABLE: PropertyTable = PropertyTable::default();
pub(crate) static PROPERTY_TIME: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
pub(crate) static PROPERTY_MILLIS: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
}
pub struct PropertyData {
owner_graph: Option<u64>,
owner_scope: Option<std::rc::Weak<super::shared_graph::BindingScopeInner>>,
typed_data: Box<dyn Any>,
pub inbound: Vec<PropertyId>,
pub outbound: Vec<PropertyId>,
pub dirty: bool,
pub(super) cutoff_settler: Option<Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>>,
}
impl PropertyData {
fn typed_data<T: 'static>(&mut self) -> &mut TypedPropertyData<T> {
self.typed_data
.downcast_mut::<TypedPropertyData<T>>()
.unwrap_or_else(|| panic!("Failed to downcast to TypedPropertyData<{}>. The actual type does not match the expected type.", std::any::type_name::<T>()))
}
}
pub struct TypedPropertyData<T> {
value: T,
shared: Option<std::sync::Weak<super::shared_property::SharedCell<T>>>,
publisher: Option<Rc<dyn Fn(&T)>>,
transition_manager: Option<TransitionManager<T>>,
property_type: PropertyType<T>,
}
#[derive(Clone)]
pub(crate) enum PropertyType<T> {
Literal,
Computed {
evaluator: Rc<dyn Fn() -> T>,
cutoff: Option<Cutoff<T>>,
},
}
#[derive(Clone)]
pub(crate) struct Cutoff<T> {
pub(super) predicate: Rc<dyn Fn(&T, &T) -> bool>,
pub(super) initialized: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CutoffEvaluation {
NotCutoff,
Initialized,
Suppressed,
Propagated,
}
#[derive(Default)]
pub(crate) struct PropertyTable {
pub(crate) property_map: RefCell<SlotMap<PropertyId, Entry>>,
#[cfg(debug_assertions)]
debug_names: RefCell<SparseSecondaryMap<PropertyId, String>>,
effect_debug_names: RefCell<HashMap<PropertyId, String>>,
effect_properties: RefCell<HashSet<PropertyId>>,
queued_effects: RefCell<VecDeque<PropertyId>>,
queued_effect_set: RefCell<HashSet<PropertyId>>,
queued_cutoffs: RefCell<VecDeque<PropertyId>>,
queued_cutoff_set: RefCell<HashSet<PropertyId>>,
}
pub struct Entry {
ref_count: usize,
data: Option<PropertyData>,
}
impl PropertyTable {
pub(super) fn set_shared<T: super::SharedPropertyValue>(
&self,
id: PropertyId,
shared: std::sync::Weak<super::shared_property::SharedCell<T>>,
publisher: Rc<dyn Fn(&T)>,
) {
self.with_property_data_mut(id, |data| {
let typed = data.typed_data::<T>();
typed.shared = Some(shared);
typed.publisher = Some(publisher);
});
}
pub(super) fn clear_shared<T: PropertyValue>(&self, id: PropertyId) {
self.with_property_data_mut(id, |data| {
let typed = data.typed_data::<T>();
typed.shared = None;
typed.publisher = None;
});
}
pub(super) fn shared_storage<T: super::SharedPropertyValue>(
&self,
id: PropertyId,
) -> Option<std::sync::Arc<super::shared_property::SharedCell<T>>> {
self.with_property_data_mut(id, |data| data.typed_data::<T>().shared.as_ref()?.upgrade())
}
fn publish_stored<T: PropertyValue>(&self, id: PropertyId) {
let publication = self.with_property_data_mut(id, |data| {
let typed = data.typed_data::<T>();
Some((typed.publisher.as_ref()?.clone(), typed.value.clone()))
});
if let Some((publish, value)) = publication {
publish(&value);
}
}
pub(crate) fn has_value_type<T: PropertyValue>(&self, id: PropertyId) -> bool {
self.with_property_data(id, |data| data.typed_data.is::<TypedPropertyData<T>>())
}
pub fn get_value<T: PropertyValue>(&self, id: PropertyId) -> T {
self.update_value::<T>(id);
self.with_property_data_mut(id, |property_data| {
property_data.typed_data::<T>().value.clone()
})
}
pub fn read_value<T: PropertyValue, V>(&self, id: PropertyId, f: impl FnOnce(&T) -> V) -> V {
self.update_value::<T>(id);
self.with_property_data_mut(id, |property_data| {
f(&property_data.typed_data::<T>().value)
})
}
pub fn set_value<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
self.import_shared(id, new_val);
self.publish_stored::<T>(id);
}
pub(super) fn import_shared<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
self.with_property_data_mut(id, |property_data: &mut PropertyData| {
let typed_data = property_data.typed_data();
typed_data.value = new_val;
property_data.dirty = false;
});
self.dirtify_outbound(id);
}
pub fn invalidate(&self, id: PropertyId) {
let (newly_dirty, is_cutoff) = self.with_property_data_mut(id, |property_data| {
if property_data.dirty {
(false, property_data.cutoff_settler.is_some())
} else {
property_data.dirty = true;
(true, property_data.cutoff_settler.is_some())
}
});
if newly_dirty {
if is_cutoff {
self.enqueue_cutoff(id);
} else {
self.enqueue_effect_if_registered(id);
self.dirtify_outbound(id);
}
}
}
pub fn add_entry<T: PropertyValue>(
&self,
start_val: T,
inbound: Vec<PropertyId>,
data: PropertyType<T>,
debug_name: Option<&str>,
) -> PropertyId {
#[cfg(not(debug_assertions))]
let _ = debug_name;
let is_cutoff = matches!(
&data,
PropertyType::Computed {
cutoff: Some(_),
..
}
);
let cutoff_settler = is_cutoff.then(|| {
Rc::new(|table: &PropertyTable, id| table.update_value::<T>(id))
as Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>
});
let id = {
let Ok(mut sm) = self.property_map.try_borrow_mut() else {
#[cfg(debug_assertions)]
panic!(
"couldn't create new property \"{}\"- table already borrowed",
debug_name.unwrap_or("<no name>")
);
#[cfg(not(debug_assertions))]
panic!("couldn't create new property - table already borrowed");
};
let entry = Entry {
ref_count: 1,
data: Some(PropertyData {
owner_graph: super::shared_graph::current_graph_id(),
owner_scope: super::shared_graph::current_binding_scope(),
inbound,
dirty: true,
typed_data: Box::new(TypedPropertyData {
value: start_val,
shared: None,
publisher: None,
property_type: data,
transition_manager: None,
}),
outbound: Vec::with_capacity(0),
cutoff_settler,
}),
};
sm.insert(entry)
};
#[cfg(debug_assertions)]
{
if let Some(name) = debug_name {
self.debug_names.borrow_mut().insert(id, name.to_owned());
}
}
self.connect_inbound(id);
id
}
pub fn transition<T: PropertyValue>(
&self,
id: PropertyId,
transition: TransitionQueueEntry<T>,
overwrite: bool,
) {
let mut should_reconnect_inbound = false;
let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
let (millis_id, curr_millis) =
PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
self.with_property_data_mut(id, |property_data: &mut PropertyData| {
let typed_data = property_data.typed_data::<T>();
let transition_manager = typed_data.transition_manager.get_or_insert_with(|| {
TransitionManager::new(typed_data.value.clone(), curr_time, curr_millis)
});
if overwrite {
transition_manager.reset_transitions(curr_time, curr_millis);
}
transition_manager.push_transition(transition);
if !property_data.inbound.contains(&time_id) {
should_reconnect_inbound = true;
property_data.inbound.push(time_id);
}
if !property_data.inbound.contains(&millis_id) {
should_reconnect_inbound = true;
property_data.inbound.push(millis_id);
}
});
if should_reconnect_inbound {
self.disconnect_inbound(id);
self.connect_inbound(id);
}
}
pub fn cancel_transitions<T: PropertyValue>(&self, id: PropertyId) {
let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
let (millis_id, curr_millis) =
PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
let has_transition = self.with_property_data(id, |property_data| {
property_data
.typed_data
.downcast_ref::<TypedPropertyData<T>>()
.expect("property type should match")
.transition_manager
.is_some()
});
if !has_transition {
return;
}
self.disconnect_inbound(id);
let value_changed = self.with_property_data_mut(id, |property_data| {
let typed_data = property_data.typed_data::<T>();
let current_value = typed_data
.transition_manager
.as_mut()
.and_then(|manager| manager.compute_eased_value(curr_time, curr_millis));
let value_changed = current_value.is_some();
if let Some(current_value) = current_value {
typed_data.value = current_value;
}
typed_data.transition_manager = None;
property_data
.inbound
.retain(|dependency| *dependency != time_id && *dependency != millis_id);
property_data.dirty = false;
value_changed
});
self.connect_inbound(id);
if value_changed {
self.dirtify_outbound(id);
}
}
fn with_entry_mut<V>(&self, id: PropertyId, f: impl FnOnce(&mut Entry) -> V) -> V {
let mut sm = self.property_map.borrow_mut();
let data = sm.get_mut(id).unwrap();
let return_value = f(data);
return_value
}
pub fn with_property_data_mut<V>(
&self,
id: PropertyId,
f: impl FnOnce(&mut PropertyData) -> V,
) -> V {
let mut property_data = self.with_entry_mut(id, |entry| {
entry
.data
.take()
.expect("property data should not have already been taken")
});
let res = f(&mut property_data);
self.with_entry_mut(id, |entry| {
entry.data = Some(property_data);
});
res
}
pub fn with_property_data<V>(&self, id: PropertyId, f: impl FnOnce(&PropertyData) -> V) -> V {
self.with_property_data_mut(id, |property_data| f(&*property_data))
}
pub fn increase_ref_count(&self, id: PropertyId) -> usize {
self.with_entry_mut(id, |entry| {
entry.ref_count += 1;
entry.ref_count
})
}
pub fn decrease_ref_count(&self, id: PropertyId) -> usize {
self.with_entry_mut(id, |entry| {
entry.ref_count -= 1;
entry.ref_count
})
}
pub fn replace_property_keep_outbound_connections<T: PropertyValue>(
&self,
source_id: PropertyId,
target_id: PropertyId,
) {
if source_id == target_id {
return;
}
self.disconnect_inbound(source_id);
self.with_property_data_mut(source_id, |source_property_data| {
self.with_property_data_mut(target_id, |target_property_data| {
source_property_data
.inbound
.clone_from(&target_property_data.inbound);
source_property_data.dirty = target_property_data.dirty
&& !matches!(
target_property_data.typed_data::<T>().property_type,
PropertyType::Literal
);
source_property_data.cutoff_settler = target_property_data.cutoff_settler.clone();
source_property_data.owner_graph = target_property_data.owner_graph;
source_property_data.owner_scope = target_property_data.owner_scope.clone();
let source_typed = source_property_data.typed_data::<T>();
let target_typed = target_property_data.typed_data::<T>();
source_typed.value = target_typed.value.clone();
source_typed.property_type = target_typed.property_type.clone();
if target_property_data.dirty {
if let PropertyType::Computed {
cutoff: Some(cutoff),
..
} = &mut source_typed.property_type
{
cutoff.initialized = false;
}
}
});
});
self.connect_inbound(source_id);
if self.is_registered_effect(source_id) && self.is_cutoff(source_id) {
panic!("cutoff properties cannot be registered as effects");
}
if self.is_cutoff(source_id) && self.is_dirty(source_id) {
self.enqueue_cutoff(source_id);
}
self.dirtify_outbound(source_id);
#[cfg(debug_assertions)]
{
let target_name = self.debug_name(target_id);
let mut names = self.debug_names.borrow_mut();
names.insert(source_id, format!("{}", target_name));
}
self.enqueue_effect_if_registered(source_id);
if !self.is_dirty(source_id) {
self.publish_stored::<T>(source_id);
}
}
pub fn update_value<T: PropertyValue>(&self, id: PropertyId) -> CutoffEvaluation {
let mut remove_dep_from_literal = false;
let mut publish_literal = false;
let evaluator = self.with_property_data_mut(id, |property_data| {
if property_data.dirty == false {
return None;
}
property_data.dirty = false;
let typed_data = property_data.typed_data::<T>();
match &mut typed_data.property_type {
PropertyType::Computed { evaluator, cutoff } => Some((
Rc::clone(&evaluator),
cutoff.as_ref().map(|cutoff| {
(
Rc::clone(&cutoff.predicate),
cutoff.initialized,
typed_data.value.clone(),
)
}),
)),
PropertyType::Literal => {
let tm = typed_data.transition_manager.as_mut()?;
let curr_time = PROPERTY_TIME.with_borrow(|time| time.get());
let curr_millis = PROPERTY_MILLIS.with_borrow(|time| time.get());
let value = tm.compute_eased_value(curr_time, curr_millis);
if let Some(interp_value) = value {
typed_data.value = interp_value;
publish_literal = true;
} else {
remove_dep_from_literal = true;
typed_data.transition_manager = None;
}
None
}
}
});
if remove_dep_from_literal {
self.disconnect_inbound(id);
self.with_property_data_mut(id, |property_data| {
property_data.inbound.clear();
});
}
if publish_literal {
self.publish_stored::<T>(id);
}
if let Some((evaluator, cutoff)) = evaluator {
let new_value = { evaluator() };
let cutoff_evaluation = match cutoff.as_ref() {
None => CutoffEvaluation::NotCutoff,
Some((_, false, _)) => CutoffEvaluation::Initialized,
Some((predicate, true, old_value)) if predicate(old_value, &new_value) => {
CutoffEvaluation::Suppressed
}
Some(_) => CutoffEvaluation::Propagated,
};
self.with_property_data_mut(id, |property_data| {
let typed_data = property_data.typed_data();
if let PropertyType::Computed {
cutoff: Some(cutoff),
..
} = &mut typed_data.property_type
{
cutoff.initialized = true;
}
if cutoff_evaluation != CutoffEvaluation::Suppressed {
typed_data.value = new_value;
}
});
if cutoff_evaluation == CutoffEvaluation::Propagated {
self.dirtify_outbound(id);
}
if cutoff_evaluation != CutoffEvaluation::Suppressed {
self.publish_stored::<T>(id);
}
cutoff_evaluation
} else {
CutoffEvaluation::NotCutoff
}
}
pub fn remove_entry(&self, id: PropertyId) {
let res = {
self.unregister_effect(id);
self.queued_cutoff_set.borrow_mut().remove(&id);
self.disconnect_outbound(id);
self.disconnect_inbound(id);
let Ok(mut sm) = self.property_map.try_borrow_mut() else {
#[cfg(debug_assertions)]
panic!(
"failed to remove property \"{}\" - propertytable already borrowed",
self.debug_name(id),
);
#[cfg(not(debug_assertions))]
panic!("failed to remove property - propertytable already borrowed");
};
sm.remove(id).expect("tried to remove non-existent prop")
};
drop(res);
}
#[cfg(debug_assertions)]
pub fn debug_name(&self, id: PropertyId) -> String {
self.debug_names
.borrow()
.get(id)
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<NO DEBUG NAME>")
.to_owned()
}
pub(crate) fn total_properties_count(&self) -> usize {
self.property_map.borrow().len()
}
pub(crate) fn register_effect(&self, id: PropertyId) {
self.register_effect_with_name(id, None);
}
pub(crate) fn register_effect_with_name(&self, id: PropertyId, debug_name: Option<&str>) {
if self.is_cutoff(id) {
panic!("cutoff properties cannot be registered as effects");
}
if let Some(debug_name) = debug_name {
self.effect_debug_names
.borrow_mut()
.insert(id, debug_name.to_owned());
}
self.effect_properties.borrow_mut().insert(id);
self.enqueue_effect_if_registered(id);
}
pub(crate) fn drain_effects(&self, max_iterations: usize) -> usize {
let mut work_ran = 0;
while work_ran < max_iterations {
if let Some(id) = self.pop_queued_cutoff() {
if !self.has_live_entry(id)
|| !self.is_cutoff(id)
|| !self.is_dirty(id)
|| !self.work_enabled(id)
{
continue;
}
let settler = self.cutoff_settler(id);
settler(self, id);
work_ran += 1;
continue;
}
let Some(id) = self.pop_queued_effect() else {
break;
};
if !self.is_registered_effect(id) || !self.has_live_entry(id) {
continue;
}
let dirty = self.with_property_data(id, |property_data| property_data.dirty);
if !self.work_enabled(id) {
continue;
}
if dirty {
self.update_value::<()>(id);
work_ran += 1;
}
}
work_ran
}
pub(crate) fn drain_effects_with_report(&self, max_iterations: usize) -> EffectDrainReport {
let mut report = EffectDrainReport::default();
let mut effect_counts = HashMap::new();
let mut cutoff_counts = HashMap::new();
let mut work_ran = 0;
while work_ran < max_iterations {
if let Some(id) = self.pop_queued_cutoff() {
if !self.has_live_entry(id)
|| !self.is_cutoff(id)
|| !self.is_dirty(id)
|| !self.work_enabled(id)
{
continue;
}
let cutoff_name = self.debug_name_for_diagnostics(id);
let cutoff_category = cutoff_name
.split_once(" (")
.map(|(category, _)| category)
.unwrap_or(&cutoff_name)
.to_owned();
let settler = self.cutoff_settler(id);
match settler(self, id) {
CutoffEvaluation::Suppressed => report.cutoffs_suppressed += 1,
CutoffEvaluation::Propagated => report.cutoffs_propagated += 1,
CutoffEvaluation::NotCutoff | CutoffEvaluation::Initialized => {}
}
report.cutoffs_evaluated += 1;
work_ran += 1;
*cutoff_counts.entry(cutoff_category).or_insert(0) += 1;
continue;
}
let Some(id) = self.pop_queued_effect() else {
break;
};
report.popped += 1;
if !self.is_registered_effect(id) {
report.skipped_unregistered += 1;
continue;
}
if !self.has_live_entry(id) {
report.skipped_missing += 1;
continue;
}
if !self.work_enabled(id) {
continue;
}
let dirty = self.with_property_data(id, |property_data| property_data.dirty);
if dirty {
let effect_name = self.debug_name_for_diagnostics(id);
self.update_value::<()>(id);
report.ran += 1;
work_ran += 1;
*effect_counts.entry(effect_name).or_insert(0) += 1;
} else {
report.skipped_clean += 1;
}
}
report.remaining = self.queued_effects.borrow().len();
report.remaining_cutoffs = self.queued_cutoffs.borrow().len();
report.budget_exhausted =
work_ran == max_iterations && (report.remaining > 0 || report.remaining_cutoffs > 0);
report.top_effects = effect_counts.into_iter().collect();
report
.top_effects
.sort_by(|(left_name, left_count), (right_name, right_count)| {
right_count
.cmp(left_count)
.then_with(|| left_name.cmp(right_name))
});
report.top_effects.truncate(8);
report.top_cutoffs = cutoff_counts.into_iter().collect();
report
.top_cutoffs
.sort_by(|(left_name, left_count), (right_name, right_count)| {
right_count
.cmp(left_count)
.then_with(|| left_name.cmp(right_name))
});
report.top_cutoffs.truncate(8);
report
}
pub(crate) fn enqueue_effect_if_registered(&self, id: PropertyId) {
if self.is_registered_effect(id) {
self.enqueue_effect(id);
}
}
fn enqueue_effect(&self, id: PropertyId) {
let mut queued_set = self.queued_effect_set.borrow_mut();
if queued_set.insert(id) {
self.queued_effects.borrow_mut().push_back(id);
}
}
fn pop_queued_effect(&self) -> Option<PropertyId> {
let id = self.pop_owned_work(&self.queued_effects)?;
self.queued_effect_set.borrow_mut().remove(&id);
Some(id)
}
pub(crate) fn enqueue_cutoff(&self, id: PropertyId) {
let mut queued_set = self.queued_cutoff_set.borrow_mut();
if queued_set.insert(id) {
self.queued_cutoffs.borrow_mut().push_back(id);
}
}
fn pop_queued_cutoff(&self) -> Option<PropertyId> {
let id = self.pop_owned_work(&self.queued_cutoffs)?;
self.queued_cutoff_set.borrow_mut().remove(&id);
Some(id)
}
fn pop_owned_work(&self, queue: &RefCell<VecDeque<PropertyId>>) -> Option<PropertyId> {
let owner = super::shared_graph::current_graph_id();
let mut queue = queue.borrow_mut();
let position = queue.iter().position(|id| {
let map = self.property_map.borrow();
let graph = map
.get(*id)
.and_then(|entry| entry.data.as_ref())
.and_then(|data| data.owner_graph);
owner.is_none() || graph.is_none() || graph == owner
})?;
queue.remove(position)
}
fn work_enabled(&self, id: PropertyId) -> bool {
self.with_property_data(id, |data| {
data.owner_scope.as_ref().map_or(true, |scope| {
scope.upgrade().is_some_and(|scope| !scope.closed.get())
})
})
}
fn unregister_effect(&self, id: PropertyId) {
self.effect_properties.borrow_mut().remove(&id);
self.effect_debug_names.borrow_mut().remove(&id);
self.queued_effect_set.borrow_mut().remove(&id);
}
fn is_registered_effect(&self, id: PropertyId) -> bool {
self.effect_properties.borrow().contains(&id)
}
pub(crate) fn is_cutoff(&self, id: PropertyId) -> bool {
self.with_property_data(id, |data| data.cutoff_settler.is_some())
}
fn is_dirty(&self, id: PropertyId) -> bool {
self.with_property_data(id, |data| data.dirty)
}
fn cutoff_settler(
&self,
id: PropertyId,
) -> Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation> {
self.with_property_data(id, |data| {
data.cutoff_settler
.as_ref()
.expect("queued cutoff property must have a settler")
.clone()
})
}
pub(super) fn has_live_entry(&self, id: PropertyId) -> bool {
self.property_map
.borrow()
.get(id)
.and_then(|entry| entry.data.as_ref())
.is_some()
}
fn debug_name_for_diagnostics(&self, id: PropertyId) -> String {
if let Some(name) = self.effect_debug_names.borrow().get(&id) {
return name.clone();
}
#[cfg(debug_assertions)]
{
if let Some(name) = self.debug_names.borrow().get(id) {
return name.clone();
}
}
format!("{id:?}")
}
pub(crate) fn outbound_debug_names(&self, id: PropertyId) -> Vec<String> {
self.with_property_data(id, |data| {
data.outbound
.iter()
.copied()
.filter(|outbound_id| self.has_live_entry(*outbound_id))
.map(|outbound_id| self.debug_name_for_diagnostics(outbound_id))
.collect()
})
}
pub(crate) fn has_direct_outbound(
&self,
source_id: PropertyId,
outbound_id: PropertyId,
) -> bool {
self.with_property_data(source_id, |data| data.outbound.contains(&outbound_id))
}
}