Skip to main content

pax_runtime_api/properties/
properties_table.rs

1use std::{
2    any::Any,
3    cell::RefCell,
4    collections::{HashMap, HashSet, VecDeque},
5    rc::Rc,
6};
7
8use slotmap::SlotMap;
9#[cfg(debug_assertions)]
10use slotmap::SparseSecondaryMap;
11
12use crate::{LocalProperty, TransitionManager, TransitionQueueEntry};
13
14use super::{private::PropertyId, PropertyValue};
15
16#[derive(Clone, Debug, Default)]
17pub struct EffectDrainReport {
18    pub ran: usize,
19    pub popped: usize,
20    pub skipped_clean: usize,
21    pub skipped_unregistered: usize,
22    pub skipped_missing: usize,
23    pub remaining: usize,
24    pub cutoffs_evaluated: usize,
25    pub cutoffs_suppressed: usize,
26    pub cutoffs_propagated: usize,
27    pub remaining_cutoffs: usize,
28    pub budget_exhausted: bool,
29    pub top_effects: Vec<(String, usize)>,
30    pub top_cutoffs: Vec<(String, usize)>,
31}
32
33thread_local! {
34    // Global property table used to store data backing dirty-dag
35    pub(crate) static PROPERTY_TABLE: PropertyTable = PropertyTable::default();
36    // Global frame timestamp.
37    pub(crate) static PROPERTY_TIME: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
38    // Global wall-clock timestamp in milliseconds.
39    pub(crate) static PROPERTY_MILLIS: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
40}
41
42// The main collection of data associated with a specific property id
43pub struct PropertyData {
44    owner_graph: Option<u64>,
45    owner_scope: Option<std::rc::Weak<super::shared_graph::BindingScopeInner>>,
46    // typed data for this property,
47    // can always be downcast to TypedPropertyData<T>
48    // where T matches the property type
49    typed_data: Box<dyn Any>,
50    // List of properties that this property depends on
51    pub inbound: Vec<PropertyId>,
52    // List of properties that depend on this value
53    pub outbound: Vec<PropertyId>,
54    // Dirty bit set if a dependency further up in the dirty-dag tree
55    // has been changed. For computed this can be any other props,
56    // for literals, only time variable
57    pub dirty: bool,
58    // Type-erased entry point used to settle cutoff properties from the
59    // synchronous reactive worklist.
60    pub(super) cutoff_settler: Option<Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>>,
61}
62
63impl PropertyData {
64    fn typed_data<T: 'static>(&mut self) -> &mut TypedPropertyData<T> {
65        self.typed_data
66            .downcast_mut::<TypedPropertyData<T>>()
67            .unwrap_or_else(|| panic!("Failed to downcast to TypedPropertyData<{}>. The actual type does not match the expected type.", std::any::type_name::<T>()))
68    }
69}
70
71// Type-specialized payload for one `LocalProperty<T>` table entry.
72pub struct TypedPropertyData<T> {
73    value: T,
74    shared: Option<std::sync::Weak<super::shared_property::SharedCell<T>>>,
75    publisher: Option<Rc<dyn Fn(&T)>>,
76    transition_manager: Option<TransitionManager<T>>,
77    // Specialization data (computed/literal etc)
78    property_type: PropertyType<T>,
79}
80
81// Specialization data only needed for different kinds of properties
82#[derive(Clone)]
83pub(crate) enum PropertyType<T> {
84    Literal,
85    Computed {
86        // Information needed to recompute on change
87        evaluator: Rc<dyn Fn() -> T>,
88        cutoff: Option<Cutoff<T>>,
89    },
90}
91
92#[derive(Clone)]
93pub(crate) struct Cutoff<T> {
94    pub(super) predicate: Rc<dyn Fn(&T, &T) -> bool>,
95    pub(super) initialized: bool,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub(super) enum CutoffEvaluation {
100    NotCutoff,
101    Initialized,
102    Suppressed,
103    Propagated,
104}
105
106// Main propertytable, containing data associated with each property
107#[derive(Default)]
108pub(crate) struct PropertyTable {
109    // Main property table containing property data
110    // Box<dyn Any> is of type Box<Entry<T>> where T is the proptype
111    pub(crate) property_map: RefCell<SlotMap<PropertyId, Entry>>,
112    #[cfg(debug_assertions)]
113    debug_names: RefCell<SparseSecondaryMap<PropertyId, String>>,
114    effect_debug_names: RefCell<HashMap<PropertyId, String>>,
115    effect_properties: RefCell<HashSet<PropertyId>>,
116    queued_effects: RefCell<VecDeque<PropertyId>>,
117    queued_effect_set: RefCell<HashSet<PropertyId>>,
118    queued_cutoffs: RefCell<VecDeque<PropertyId>>,
119    queued_cutoff_set: RefCell<HashSet<PropertyId>>,
120}
121
122// Reference-counted slotmap entry for one property id.
123pub struct Entry {
124    ref_count: usize,
125    data: Option<PropertyData>,
126}
127
128impl PropertyTable {
129    pub(super) fn set_shared<T: super::SharedPropertyValue>(
130        &self,
131        id: PropertyId,
132        shared: std::sync::Weak<super::shared_property::SharedCell<T>>,
133        publisher: Rc<dyn Fn(&T)>,
134    ) {
135        self.with_property_data_mut(id, |data| {
136            let typed = data.typed_data::<T>();
137            typed.shared = Some(shared);
138            typed.publisher = Some(publisher);
139        });
140    }
141
142    pub(super) fn clear_shared<T: PropertyValue>(&self, id: PropertyId) {
143        self.with_property_data_mut(id, |data| {
144            let typed = data.typed_data::<T>();
145            typed.shared = None;
146            typed.publisher = None;
147        });
148    }
149
150    pub(super) fn shared_storage<T: super::SharedPropertyValue>(
151        &self,
152        id: PropertyId,
153    ) -> Option<std::sync::Arc<super::shared_property::SharedCell<T>>> {
154        self.with_property_data_mut(id, |data| data.typed_data::<T>().shared.as_ref()?.upgrade())
155    }
156
157    fn publish_stored<T: PropertyValue>(&self, id: PropertyId) {
158        let publication = self.with_property_data_mut(id, |data| {
159            let typed = data.typed_data::<T>();
160            Some((typed.publisher.as_ref()?.clone(), typed.value.clone()))
161        });
162        if let Some((publish, value)) = publication {
163            publish(&value);
164        }
165    }
166
167    pub(crate) fn has_value_type<T: PropertyValue>(&self, id: PropertyId) -> bool {
168        self.with_property_data(id, |data| data.typed_data.is::<TypedPropertyData<T>>())
169    }
170
171    // Main function to get access to a value inside of a property.
172    // Makes sure the value is up to date before returning in the case
173    // of computed properties.
174    pub fn get_value<T: PropertyValue>(&self, id: PropertyId) -> T {
175        self.update_value::<T>(id);
176        self.with_property_data_mut(id, |property_data| {
177            property_data.typed_data::<T>().value.clone()
178        })
179    }
180
181    // Reads a property value by reference after updating dirty dependencies.
182    pub fn read_value<T: PropertyValue, V>(&self, id: PropertyId, f: impl FnOnce(&T) -> V) -> V {
183        self.update_value::<T>(id);
184        self.with_property_data_mut(id, |property_data| {
185            f(&property_data.typed_data::<T>().value)
186        })
187    }
188
189    // Main function to set a value of a property.
190    // NOTE: This always assumes the underlying data was changed, and marks
191    // it and its dependents as dirty irrespective of actual modification
192    pub fn set_value<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
193        self.import_shared(id, new_val);
194        self.publish_stored::<T>(id);
195    }
196
197    // Importing must not echo a worker publication back into shared storage.
198    pub(super) fn import_shared<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
199        self.with_property_data_mut(id, |property_data: &mut PropertyData| {
200            let typed_data = property_data.typed_data();
201            typed_data.value = new_val;
202            property_data.dirty = false;
203        });
204        self.dirtify_outbound(id);
205    }
206
207    pub fn invalidate(&self, id: PropertyId) {
208        let (newly_dirty, is_cutoff) = self.with_property_data_mut(id, |property_data| {
209            if property_data.dirty {
210                (false, property_data.cutoff_settler.is_some())
211            } else {
212                property_data.dirty = true;
213                (true, property_data.cutoff_settler.is_some())
214            }
215        });
216        if newly_dirty {
217            if is_cutoff {
218                self.enqueue_cutoff(id);
219            } else {
220                self.enqueue_effect_if_registered(id);
221                self.dirtify_outbound(id);
222            }
223        }
224    }
225
226    // Adds a new untyped property entry
227    pub fn add_entry<T: PropertyValue>(
228        &self,
229        start_val: T,
230        inbound: Vec<PropertyId>,
231        data: PropertyType<T>,
232        debug_name: Option<&str>,
233    ) -> PropertyId {
234        #[cfg(not(debug_assertions))]
235        let _ = debug_name;
236
237        let is_cutoff = matches!(
238            &data,
239            PropertyType::Computed {
240                cutoff: Some(_),
241                ..
242            }
243        );
244        let cutoff_settler = is_cutoff.then(|| {
245            Rc::new(|table: &PropertyTable, id| table.update_value::<T>(id))
246                as Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>
247        });
248
249        let id = {
250            let Ok(mut sm) = self.property_map.try_borrow_mut() else {
251                #[cfg(debug_assertions)]
252                panic!(
253                    "couldn't create new property \"{}\"- table already borrowed",
254                    debug_name.unwrap_or("<no name>")
255                );
256                #[cfg(not(debug_assertions))]
257                panic!("couldn't create new property - table already borrowed");
258            };
259            let entry = Entry {
260                ref_count: 1,
261                data: Some(PropertyData {
262                    owner_graph: super::shared_graph::current_graph_id(),
263                    owner_scope: super::shared_graph::current_binding_scope(),
264                    inbound,
265                    dirty: true,
266                    typed_data: Box::new(TypedPropertyData {
267                        value: start_val,
268                        shared: None,
269                        publisher: None,
270                        property_type: data,
271                        transition_manager: None,
272                    }),
273                    outbound: Vec::with_capacity(0),
274                    cutoff_settler,
275                }),
276            };
277            sm.insert(entry)
278        };
279        #[cfg(debug_assertions)]
280        {
281            if let Some(name) = debug_name {
282                self.debug_names.borrow_mut().insert(id, name.to_owned());
283            }
284        }
285        self.connect_inbound(id);
286        id
287    }
288
289    // Add a transition to the transitionmanager, making the value slowly change over time
290    // Currently this only transitions the literal value of the property (and updates dependents accordingly)
291    // This has no special interactions with computed properties
292    pub fn transition<T: PropertyValue>(
293        &self,
294        id: PropertyId,
295        transition: TransitionQueueEntry<T>,
296        overwrite: bool,
297    ) {
298        let mut should_reconnect_inbound = false;
299        let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
300        let (millis_id, curr_millis) =
301            PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
302        self.with_property_data_mut(id, |property_data: &mut PropertyData| {
303            let typed_data = property_data.typed_data::<T>();
304            let transition_manager = typed_data.transition_manager.get_or_insert_with(|| {
305                TransitionManager::new(typed_data.value.clone(), curr_time, curr_millis)
306            });
307            if overwrite {
308                transition_manager.reset_transitions(curr_time, curr_millis);
309            }
310            transition_manager.push_transition(transition);
311            if !property_data.inbound.contains(&time_id) {
312                should_reconnect_inbound = true;
313                property_data.inbound.push(time_id);
314            }
315            if !property_data.inbound.contains(&millis_id) {
316                should_reconnect_inbound = true;
317                property_data.inbound.push(millis_id);
318            }
319        });
320        if should_reconnect_inbound {
321            self.disconnect_inbound(id);
322            self.connect_inbound(id);
323        }
324    }
325
326    pub fn cancel_transitions<T: PropertyValue>(&self, id: PropertyId) {
327        let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
328        let (millis_id, curr_millis) =
329            PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
330        let has_transition = self.with_property_data(id, |property_data| {
331            property_data
332                .typed_data
333                .downcast_ref::<TypedPropertyData<T>>()
334                .expect("property type should match")
335                .transition_manager
336                .is_some()
337        });
338        if !has_transition {
339            return;
340        }
341
342        self.disconnect_inbound(id);
343        let value_changed = self.with_property_data_mut(id, |property_data| {
344            let typed_data = property_data.typed_data::<T>();
345            let current_value = typed_data
346                .transition_manager
347                .as_mut()
348                .and_then(|manager| manager.compute_eased_value(curr_time, curr_millis));
349            let value_changed = current_value.is_some();
350            if let Some(current_value) = current_value {
351                typed_data.value = current_value;
352            }
353            typed_data.transition_manager = None;
354            property_data
355                .inbound
356                .retain(|dependency| *dependency != time_id && *dependency != millis_id);
357            property_data.dirty = false;
358            value_changed
359        });
360        self.connect_inbound(id);
361
362        if value_changed {
363            self.dirtify_outbound(id);
364        }
365    }
366
367    // Gives mutable access to a entry in the property table
368    // WARNING: this function is dangerous, f can not drop, create, set, get
369    // or in any other way modify the global property table or this will panic
370    // with multiple mutable borrows. Letting f contain any form of userland
371    // code is NOT a good idea.
372    fn with_entry_mut<V>(&self, id: PropertyId, f: impl FnOnce(&mut Entry) -> V) -> V {
373        let mut sm = self.property_map.borrow_mut();
374        let data = sm.get_mut(id).unwrap();
375        let return_value = f(data);
376        return_value
377    }
378
379    // Allows mutable access to the data associated with a property id.
380    // WARNING while this method is being run, the entry corresponding to id
381    // is not present in the table, and methods such as .get(), .set(), and
382    // possibly others on the property with the id parameter bellow will panic.
383    pub fn with_property_data_mut<V>(
384        &self,
385        id: PropertyId,
386        f: impl FnOnce(&mut PropertyData) -> V,
387    ) -> V {
388        // take the value out of the table
389        let mut property_data = self.with_entry_mut(id, |entry| {
390            entry
391                .data
392                .take()
393                .expect("property data should not have already been taken")
394        });
395
396        // run f, without table being borrowed
397        // NOTE: need to make sure with_property_data_mut is not run recursively
398        // with the same id
399        let res = f(&mut property_data);
400
401        // return value to the table that was taken
402        self.with_entry_mut(id, |entry| {
403            entry.data = Some(property_data);
404        });
405
406        res
407    }
408
409    // Allows access to the data associated with a property id.
410    // WARNING while this method is being run, the entry corresponding to id
411    // is not present in the table, and methods such as .get(), .set(), and
412    // possibly others on the property with the id parameter bellow will panic.
413    pub fn with_property_data<V>(&self, id: PropertyId, f: impl FnOnce(&PropertyData) -> V) -> V {
414        self.with_property_data_mut(id, |property_data| f(&*property_data))
415    }
416
417    // Increase the ref count of a property
418    pub fn increase_ref_count(&self, id: PropertyId) -> usize {
419        self.with_entry_mut(id, |entry| {
420            entry.ref_count += 1;
421            entry.ref_count
422        })
423    }
424
425    // Decrease the ref count of a property
426    pub fn decrease_ref_count(&self, id: PropertyId) -> usize {
427        self.with_entry_mut(id, |entry| {
428            entry.ref_count -= 1;
429            entry.ref_count
430        })
431    }
432
433    // Replaces the way the source parameters property is being
434    // computed / its value to the way target does.
435    // NOTE: source_id and target_id need to both contain
436    // the type T, or else this panics
437    pub fn replace_property_keep_outbound_connections<T: PropertyValue>(
438        &self,
439        source_id: PropertyId,
440        target_id: PropertyId,
441    ) {
442        if source_id == target_id {
443            return;
444        }
445
446        // Reconnect even identical dependencies: their outbound order affects
447        // effect scheduling. Only the vector's allocation is reusable.
448        self.disconnect_inbound(source_id);
449
450        // copy necessary internal state from target to source
451        self.with_property_data_mut(source_id, |source_property_data| {
452            self.with_property_data_mut(target_id, |target_property_data| {
453                // Copy over inbound, dirty state, and current value to source
454                source_property_data
455                    .inbound
456                    .clone_from(&target_property_data.inbound);
457                // Literal replacement is already resolved. Mark it clean so
458                // the new authored value publishes below, without evaluating
459                // computations eagerly or echoing an old projection snapshot.
460                source_property_data.dirty = target_property_data.dirty
461                    && !matches!(
462                        target_property_data.typed_data::<T>().property_type,
463                        PropertyType::Literal
464                    );
465                source_property_data.cutoff_settler = target_property_data.cutoff_settler.clone();
466                source_property_data.owner_graph = target_property_data.owner_graph;
467                source_property_data.owner_scope = target_property_data.owner_scope.clone();
468                let source_typed = source_property_data.typed_data::<T>();
469                let target_typed = target_property_data.typed_data::<T>();
470                source_typed.value = target_typed.value.clone();
471                source_typed.property_type = target_typed.property_type.clone();
472                if target_property_data.dirty {
473                    if let PropertyType::Computed {
474                        cutoff: Some(cutoff),
475                        ..
476                    } = &mut source_typed.property_type
477                    {
478                        cutoff.initialized = false;
479                    }
480                }
481            });
482        });
483
484        // Reconnect the new dependencies and enqueue any dirty upstream cutoffs.
485        self.connect_inbound(source_id);
486
487        if self.is_registered_effect(source_id) && self.is_cutoff(source_id) {
488            panic!("cutoff properties cannot be registered as effects");
489        }
490        if self.is_cutoff(source_id) && self.is_dirty(source_id) {
491            self.enqueue_cutoff(source_id);
492        }
493
494        // make sure dependencies of self
495        // know that something has changed
496        self.dirtify_outbound(source_id);
497
498        #[cfg(debug_assertions)]
499        {
500            // overwrite with more descriptive name
501            let target_name = self.debug_name(target_id);
502            let mut names = self.debug_names.borrow_mut();
503            names.insert(source_id, format!("{}", target_name));
504        }
505
506        self.enqueue_effect_if_registered(source_id);
507        if !self.is_dirty(source_id) {
508            self.publish_stored::<T>(source_id);
509        }
510    }
511
512    // re-computes the value if dirty
513    pub fn update_value<T: PropertyValue>(&self, id: PropertyId) -> CutoffEvaluation {
514        let mut remove_dep_from_literal = false;
515        let mut publish_literal = false;
516        let evaluator = self.with_property_data_mut(id, |property_data| {
517            //short circuit if the value is still up to date
518            if property_data.dirty == false {
519                return None;
520            }
521            property_data.dirty = false;
522            let typed_data = property_data.typed_data::<T>();
523            match &mut typed_data.property_type {
524                PropertyType::Computed { evaluator, cutoff } => Some((
525                    Rc::clone(&evaluator),
526                    cutoff.as_ref().map(|cutoff| {
527                        (
528                            Rc::clone(&cutoff.predicate),
529                            cutoff.initialized,
530                            typed_data.value.clone(),
531                        )
532                    }),
533                )),
534                PropertyType::Literal => {
535                    let tm = typed_data.transition_manager.as_mut()?;
536                    let curr_time = PROPERTY_TIME.with_borrow(|time| time.get());
537                    let curr_millis = PROPERTY_MILLIS.with_borrow(|time| time.get());
538                    let value = tm.compute_eased_value(curr_time, curr_millis);
539                    if let Some(interp_value) = value {
540                        typed_data.value = interp_value;
541                        publish_literal = true;
542                    } else {
543                        //transition must be over, let's remove dependencies
544                        remove_dep_from_literal = true;
545                        typed_data.transition_manager = None;
546                    }
547                    None
548                }
549            }
550        });
551
552        if remove_dep_from_literal {
553            self.disconnect_inbound(id);
554            self.with_property_data_mut(id, |property_data| {
555                property_data.inbound.clear();
556            });
557        }
558
559        if publish_literal {
560            self.publish_stored::<T>(id);
561        }
562
563        if let Some((evaluator, cutoff)) = evaluator {
564            // WARNING: the evaluator should not be run while the table is in
565            // an invalid state (borrowed, in with_property_data closure, etc.)
566            // as this function is provided by a user of the property system and
567            // can do arbitrary sets/ gets/drops etc (that need the prop data)
568            let new_value = { evaluator() };
569            // Cutoff predicates are API callbacks too, so run them while the
570            // property is present in the table for the same reason as evaluators.
571            let cutoff_evaluation = match cutoff.as_ref() {
572                None => CutoffEvaluation::NotCutoff,
573                Some((_, false, _)) => CutoffEvaluation::Initialized,
574                Some((predicate, true, old_value)) if predicate(old_value, &new_value) => {
575                    CutoffEvaluation::Suppressed
576                }
577                Some(_) => CutoffEvaluation::Propagated,
578            };
579            self.with_property_data_mut(id, |property_data| {
580                let typed_data = property_data.typed_data();
581                if let PropertyType::Computed {
582                    cutoff: Some(cutoff),
583                    ..
584                } = &mut typed_data.property_type
585                {
586                    cutoff.initialized = true;
587                }
588
589                if cutoff_evaluation != CutoffEvaluation::Suppressed {
590                    typed_data.value = new_value;
591                }
592            });
593
594            if cutoff_evaluation == CutoffEvaluation::Propagated {
595                self.dirtify_outbound(id);
596            }
597            if cutoff_evaluation != CutoffEvaluation::Suppressed {
598                self.publish_stored::<T>(id);
599            }
600            cutoff_evaluation
601        } else {
602            CutoffEvaluation::NotCutoff
603        }
604    }
605
606    // drop a properties underlying data, making any subsequent calls invalid by panic
607    pub fn remove_entry(&self, id: PropertyId) {
608        let res = {
609            self.unregister_effect(id);
610            self.queued_cutoff_set.borrow_mut().remove(&id);
611            self.disconnect_outbound(id);
612            self.disconnect_inbound(id);
613            let Ok(mut sm) = self.property_map.try_borrow_mut() else {
614                #[cfg(debug_assertions)]
615                panic!(
616                    "failed to remove property \"{}\" - propertytable already borrowed",
617                    self.debug_name(id),
618                );
619                #[cfg(not(debug_assertions))]
620                panic!("failed to remove property - propertytable already borrowed");
621            };
622            sm.remove(id).expect("tried to remove non-existent prop")
623        };
624        drop(res);
625    }
626
627    // Returns a human-readable name for diagnostics, falling back to the raw id.
628    #[cfg(debug_assertions)]
629    pub fn debug_name(&self, id: PropertyId) -> String {
630        self.debug_names
631            .borrow()
632            .get(id)
633            .as_ref()
634            .map(|s| s.as_str())
635            .unwrap_or("<NO DEBUG NAME>")
636            .to_owned()
637    }
638
639    // Returns the number of live property-table slots.
640    pub(crate) fn total_properties_count(&self) -> usize {
641        self.property_map.borrow().len()
642    }
643
644    pub(crate) fn register_effect(&self, id: PropertyId) {
645        self.register_effect_with_name(id, None);
646    }
647
648    pub(crate) fn register_effect_with_name(&self, id: PropertyId, debug_name: Option<&str>) {
649        if self.is_cutoff(id) {
650            panic!("cutoff properties cannot be registered as effects");
651        }
652        if let Some(debug_name) = debug_name {
653            self.effect_debug_names
654                .borrow_mut()
655                .insert(id, debug_name.to_owned());
656        }
657        self.effect_properties.borrow_mut().insert(id);
658        self.enqueue_effect_if_registered(id);
659    }
660
661    pub(crate) fn drain_effects(&self, max_iterations: usize) -> usize {
662        let mut work_ran = 0;
663        while work_ran < max_iterations {
664            if let Some(id) = self.pop_queued_cutoff() {
665                if !self.has_live_entry(id)
666                    || !self.is_cutoff(id)
667                    || !self.is_dirty(id)
668                    || !self.work_enabled(id)
669                {
670                    continue;
671                }
672                let settler = self.cutoff_settler(id);
673                settler(self, id);
674                work_ran += 1;
675                continue;
676            }
677
678            let Some(id) = self.pop_queued_effect() else {
679                break;
680            };
681            if !self.is_registered_effect(id) || !self.has_live_entry(id) {
682                continue;
683            }
684            let dirty = self.with_property_data(id, |property_data| property_data.dirty);
685            if !self.work_enabled(id) {
686                continue;
687            }
688            if dirty {
689                self.update_value::<()>(id);
690                work_ran += 1;
691            }
692        }
693        work_ran
694    }
695
696    pub(crate) fn drain_effects_with_report(&self, max_iterations: usize) -> EffectDrainReport {
697        let mut report = EffectDrainReport::default();
698        let mut effect_counts = HashMap::new();
699        let mut cutoff_counts = HashMap::new();
700
701        let mut work_ran = 0;
702        while work_ran < max_iterations {
703            if let Some(id) = self.pop_queued_cutoff() {
704                if !self.has_live_entry(id)
705                    || !self.is_cutoff(id)
706                    || !self.is_dirty(id)
707                    || !self.work_enabled(id)
708                {
709                    continue;
710                }
711                let cutoff_name = self.debug_name_for_diagnostics(id);
712                let cutoff_category = cutoff_name
713                    .split_once(" (")
714                    .map(|(category, _)| category)
715                    .unwrap_or(&cutoff_name)
716                    .to_owned();
717                let settler = self.cutoff_settler(id);
718                match settler(self, id) {
719                    CutoffEvaluation::Suppressed => report.cutoffs_suppressed += 1,
720                    CutoffEvaluation::Propagated => report.cutoffs_propagated += 1,
721                    CutoffEvaluation::NotCutoff | CutoffEvaluation::Initialized => {}
722                }
723                report.cutoffs_evaluated += 1;
724                work_ran += 1;
725                *cutoff_counts.entry(cutoff_category).or_insert(0) += 1;
726                continue;
727            }
728
729            let Some(id) = self.pop_queued_effect() else {
730                break;
731            };
732            report.popped += 1;
733
734            if !self.is_registered_effect(id) {
735                report.skipped_unregistered += 1;
736                continue;
737            }
738            if !self.has_live_entry(id) {
739                report.skipped_missing += 1;
740                continue;
741            }
742
743            if !self.work_enabled(id) {
744                continue;
745            }
746            let dirty = self.with_property_data(id, |property_data| property_data.dirty);
747            if dirty {
748                let effect_name = self.debug_name_for_diagnostics(id);
749                self.update_value::<()>(id);
750                report.ran += 1;
751                work_ran += 1;
752                *effect_counts.entry(effect_name).or_insert(0) += 1;
753            } else {
754                report.skipped_clean += 1;
755            }
756        }
757
758        report.remaining = self.queued_effects.borrow().len();
759        report.remaining_cutoffs = self.queued_cutoffs.borrow().len();
760        report.budget_exhausted =
761            work_ran == max_iterations && (report.remaining > 0 || report.remaining_cutoffs > 0);
762        report.top_effects = effect_counts.into_iter().collect();
763        report
764            .top_effects
765            .sort_by(|(left_name, left_count), (right_name, right_count)| {
766                right_count
767                    .cmp(left_count)
768                    .then_with(|| left_name.cmp(right_name))
769            });
770        report.top_effects.truncate(8);
771        report.top_cutoffs = cutoff_counts.into_iter().collect();
772        report
773            .top_cutoffs
774            .sort_by(|(left_name, left_count), (right_name, right_count)| {
775                right_count
776                    .cmp(left_count)
777                    .then_with(|| left_name.cmp(right_name))
778            });
779        report.top_cutoffs.truncate(8);
780        report
781    }
782
783    pub(crate) fn enqueue_effect_if_registered(&self, id: PropertyId) {
784        if self.is_registered_effect(id) {
785            self.enqueue_effect(id);
786        }
787    }
788
789    fn enqueue_effect(&self, id: PropertyId) {
790        let mut queued_set = self.queued_effect_set.borrow_mut();
791        if queued_set.insert(id) {
792            self.queued_effects.borrow_mut().push_back(id);
793        }
794    }
795
796    fn pop_queued_effect(&self) -> Option<PropertyId> {
797        let id = self.pop_owned_work(&self.queued_effects)?;
798        self.queued_effect_set.borrow_mut().remove(&id);
799        Some(id)
800    }
801
802    pub(crate) fn enqueue_cutoff(&self, id: PropertyId) {
803        let mut queued_set = self.queued_cutoff_set.borrow_mut();
804        if queued_set.insert(id) {
805            self.queued_cutoffs.borrow_mut().push_back(id);
806        }
807    }
808
809    fn pop_queued_cutoff(&self) -> Option<PropertyId> {
810        let id = self.pop_owned_work(&self.queued_cutoffs)?;
811        self.queued_cutoff_set.borrow_mut().remove(&id);
812        Some(id)
813    }
814
815    fn pop_owned_work(&self, queue: &RefCell<VecDeque<PropertyId>>) -> Option<PropertyId> {
816        let owner = super::shared_graph::current_graph_id();
817        let mut queue = queue.borrow_mut();
818        let position = queue.iter().position(|id| {
819            let map = self.property_map.borrow();
820            let graph = map
821                .get(*id)
822                .and_then(|entry| entry.data.as_ref())
823                .and_then(|data| data.owner_graph);
824            owner.is_none() || graph.is_none() || graph == owner
825        })?;
826        queue.remove(position)
827    }
828
829    fn work_enabled(&self, id: PropertyId) -> bool {
830        self.with_property_data(id, |data| {
831            data.owner_scope.as_ref().map_or(true, |scope| {
832                scope.upgrade().is_some_and(|scope| !scope.closed.get())
833            })
834        })
835    }
836
837    fn unregister_effect(&self, id: PropertyId) {
838        self.effect_properties.borrow_mut().remove(&id);
839        self.effect_debug_names.borrow_mut().remove(&id);
840        self.queued_effect_set.borrow_mut().remove(&id);
841    }
842
843    fn is_registered_effect(&self, id: PropertyId) -> bool {
844        self.effect_properties.borrow().contains(&id)
845    }
846
847    pub(crate) fn is_cutoff(&self, id: PropertyId) -> bool {
848        self.with_property_data(id, |data| data.cutoff_settler.is_some())
849    }
850
851    fn is_dirty(&self, id: PropertyId) -> bool {
852        self.with_property_data(id, |data| data.dirty)
853    }
854
855    fn cutoff_settler(
856        &self,
857        id: PropertyId,
858    ) -> Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation> {
859        self.with_property_data(id, |data| {
860            data.cutoff_settler
861                .as_ref()
862                .expect("queued cutoff property must have a settler")
863                .clone()
864        })
865    }
866
867    pub(super) fn has_live_entry(&self, id: PropertyId) -> bool {
868        self.property_map
869            .borrow()
870            .get(id)
871            .and_then(|entry| entry.data.as_ref())
872            .is_some()
873    }
874
875    fn debug_name_for_diagnostics(&self, id: PropertyId) -> String {
876        if let Some(name) = self.effect_debug_names.borrow().get(&id) {
877            return name.clone();
878        }
879
880        #[cfg(debug_assertions)]
881        {
882            if let Some(name) = self.debug_names.borrow().get(id) {
883                return name.clone();
884            }
885        }
886
887        format!("{id:?}")
888    }
889
890    pub(crate) fn outbound_debug_names(&self, id: PropertyId) -> Vec<String> {
891        self.with_property_data(id, |data| {
892            data.outbound
893                .iter()
894                .copied()
895                .filter(|outbound_id| self.has_live_entry(*outbound_id))
896                .map(|outbound_id| self.debug_name_for_diagnostics(outbound_id))
897                .collect()
898        })
899    }
900
901    pub(crate) fn has_direct_outbound(
902        &self,
903        source_id: PropertyId,
904        outbound_id: PropertyId,
905    ) -> bool {
906        self.with_property_data(source_id, |data| data.outbound.contains(&outbound_id))
907    }
908}