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