Skip to main content

cranpose_core/
composition.rs

1use crate::{
2    collections::map::{HashMap, HashSet},
3    remove_child_and_cleanup_now, runtime, snapshot_state_observer, Applier, ApplierGuard,
4    ApplierHost, CommandQueue, Composer, CompositionPassDebugStats, ConcreteApplierHost,
5    DefaultScheduler, Key, NodeError, NodeId, RecomposeScope, Runtime, RuntimeHandle, ScopeId,
6    SlotTable, SlotTableDebugStats, SlotValueTypeDebugStat, SlotsHost, SnapshotStateObserver,
7};
8use std::rc::Rc;
9use std::sync::Arc;
10
11pub struct Composition<A: Applier + 'static> {
12    pub(crate) slots: Rc<SlotsHost>,
13    pub(crate) applier: Rc<ConcreteApplierHost<A>>,
14    pub(crate) runtime: Runtime,
15    pub(crate) observer: SnapshotStateObserver,
16    pub(crate) root: Option<NodeId>,
17    pub(crate) root_key: Option<Key>,
18    pub(crate) root_render_requested: bool,
19    pub(crate) last_pass_stats: CompositionPassDebugStats,
20}
21
22/// Upper bound on chained root-render replays and scope-recomposition rounds.
23///
24/// Each root render clears `root_render_requested` but may re-raise it if a
25/// recompose pass inside `render()` promotes a scope callback to the root
26/// (see `Composer::recranpose_group` in recompose.rs — callbacks that cannot
27/// run invalidate their `callback_promotion_target`, and if no ancestor can
28/// absorb the callback, `request_root_render()` is called). Each promotion
29/// walks up one parent scope, so natural convergence is bounded by the
30/// composition depth. The same invariant bounds `process_invalid_scopes`:
31/// recomposing a scope may invalidate others, but the chain must terminate.
32///
33/// This constant is a safety net for reentrant-render bugs. Exceeding it
34/// trips a `debug_assert!` in dev/test builds (loud failure so regressions
35/// are caught immediately) and falls back to a break + `log::error!` in
36/// release builds so end users do not see the UI thread panic.
37pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
38
39impl<A: Applier + 'static> Composition<A> {
40    pub fn new(applier: A) -> Self {
41        Self::with_runtime(applier, Runtime::new(Arc::new(DefaultScheduler)))
42    }
43
44    pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
45        let slots = Rc::new(SlotsHost::new(SlotTable::new()));
46        let applier = Rc::new(ConcreteApplierHost::new(applier));
47        let observer_handle = runtime.handle();
48        let observer = SnapshotStateObserver::new(move |callback| {
49            observer_handle.enqueue_ui_task(callback);
50        });
51        observer.start();
52        Self {
53            slots,
54            applier,
55            runtime,
56            observer,
57            root: None,
58            root_key: None,
59            root_render_requested: false,
60            last_pass_stats: CompositionPassDebugStats::default(),
61        }
62    }
63
64    /// Returns the root group key captured from the most recent `render()` call,
65    /// or `None` before the first render.
66    pub fn root_key(&self) -> Option<Key> {
67        self.root_key
68    }
69
70    fn slots_host(&self) -> Rc<SlotsHost> {
71        Rc::clone(&self.slots)
72    }
73
74    fn applier_host(&self) -> Rc<dyn ApplierHost> {
75        self.applier.clone()
76    }
77
78    fn reset_last_pass_stats(&mut self) {
79        self.last_pass_stats = CompositionPassDebugStats::default();
80    }
81
82    pub fn take_root_render_request(&mut self) -> bool {
83        std::mem::take(&mut self.root_render_requested)
84    }
85
86    fn record_pass_stats(
87        &mut self,
88        commands: &CommandQueue,
89        side_effects: &Vec<Box<dyn FnOnce()>>,
90    ) {
91        self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
92        self.last_pass_stats.commands_cap =
93            self.last_pass_stats.commands_cap.max(commands.capacity());
94        self.last_pass_stats.command_payload_len_bytes = self
95            .last_pass_stats
96            .command_payload_len_bytes
97            .max(commands.payload_len_bytes());
98        self.last_pass_stats.command_payload_cap_bytes = self
99            .last_pass_stats
100            .command_payload_cap_bytes
101            .max(commands.payload_capacity_bytes());
102        self.last_pass_stats.sync_children_len = self
103            .last_pass_stats
104            .sync_children_len
105            .max(commands.sync_children.len());
106        self.last_pass_stats.sync_children_cap = self
107            .last_pass_stats
108            .sync_children_cap
109            .max(commands.sync_children.capacity());
110        self.last_pass_stats.sync_child_ids_len = self
111            .last_pass_stats
112            .sync_child_ids_len
113            .max(commands.sync_child_ids.len());
114        self.last_pass_stats.sync_child_ids_cap = self
115            .last_pass_stats
116            .sync_child_ids_cap
117            .max(commands.sync_child_ids.capacity());
118        self.last_pass_stats.side_effects_len = self
119            .last_pass_stats
120            .side_effects_len
121            .max(side_effects.len());
122        self.last_pass_stats.side_effects_cap = self
123            .last_pass_stats
124            .side_effects_cap
125            .max(side_effects.capacity());
126    }
127
128    fn finalize_runtime_state(&mut self) {
129        let runtime_handle = self.runtime_handle();
130        self.observer.prune_dead_scopes();
131        if !self.runtime.has_updates()
132            && !runtime_handle.has_invalid_scopes()
133            && !runtime_handle.has_frame_callbacks()
134            && !runtime_handle.has_pending_ui()
135        {
136            self.runtime.set_needs_frame(false);
137        }
138    }
139
140    pub(crate) fn finalize_compaction(&mut self) -> Result<bool, NodeError> {
141        let mut removed_orphaned = false;
142        let mut orphaned_node_count = 0usize;
143        self.slots.borrow_mut().compact();
144        let orphaned = self.slots.borrow_mut().drain_orphaned_node_ids();
145        {
146            let mut applier = self.applier.borrow_dyn();
147            for orphaned in orphaned {
148                if !matches!(
149                    self.slots.borrow().orphaned_node_state(orphaned),
150                    crate::slot_table::NodeSlotState::Missing
151                ) {
152                    continue;
153                }
154                if applier.node_generation(orphaned.id) != orphaned.generation {
155                    continue;
156                }
157                removed_orphaned = true;
158                orphaned_node_count += 1;
159                let parent_id = applier
160                    .get_mut(orphaned.id)
161                    .ok()
162                    .and_then(|node| node.parent());
163                if let Some(parent_id) = parent_id {
164                    let _ = remove_child_and_cleanup_now(&mut *applier, parent_id, orphaned.id);
165                    continue;
166                }
167                if let Ok(node) = applier.get_mut(orphaned.id) {
168                    node.on_removed_from_parent();
169                    node.unmount();
170                }
171                let _ = applier.remove(orphaned.id);
172            }
173        }
174        self.applier_host().compact();
175        self.applier.borrow_dyn().clear_recycled_nodes();
176        if removed_orphaned {
177            log::debug!(
178                "finalize_compaction: removing {} orphaned nodes",
179                orphaned_node_count
180            );
181        }
182        Ok(removed_orphaned)
183    }
184
185    fn clear_pending_invalid_scopes(&mut self) -> HashSet<ScopeId> {
186        let runtime_handle = self.runtime_handle();
187        let mut cleared = HashSet::default();
188        for (id, _) in runtime_handle.take_invalidated_scopes() {
189            runtime_handle.mark_scope_recomposed(id);
190            cleared.insert(id);
191        }
192        cleared
193    }
194
195    fn render_root_pass(
196        &mut self,
197        key: Key,
198        content: &mut dyn FnMut(),
199        clear_pending_invalid_scopes: bool,
200    ) -> Result<HashSet<ScopeId>, NodeError> {
201        self.root_key = Some(key);
202        self.root_render_requested = false;
203        let cleared_invalid_scopes = if clear_pending_invalid_scopes {
204            self.clear_pending_invalid_scopes()
205        } else {
206            HashSet::default()
207        };
208        self.slots.borrow_mut().reset();
209        let runtime_handle = self.runtime_handle();
210        runtime_handle.drain_ui();
211        let side_effects = {
212            let _teardown = runtime::enter_state_teardown_scope();
213            let composer = Composer::new(
214                Rc::clone(&self.slots),
215                self.applier.clone(),
216                runtime_handle.clone(),
217                self.observer.clone(),
218                self.root,
219            );
220            self.observer.begin_frame();
221            let (root, commands, side_effects) = composer.install(|composer| {
222                composer.with_group(key, |_| content());
223                let root = composer.root();
224                let commands = composer.take_commands();
225                let side_effects = composer.take_side_effects();
226                (root, commands, side_effects)
227            });
228            self.record_pass_stats(&commands, &side_effects);
229            {
230                let mut applier = self.applier.borrow_dyn();
231                commands.apply(&mut *applier)?;
232                for update in runtime_handle.take_updates() {
233                    update.apply(&mut *applier)?;
234                }
235            }
236
237            self.root = root;
238            {
239                let mut slots = self.slots.borrow_mut();
240                let _ = slots.finalize_current_group();
241                slots.flush();
242            }
243            let _ = self.finalize_compaction()?;
244            side_effects
245        };
246        runtime_handle.drain_ui();
247        for effect in side_effects {
248            effect();
249        }
250        runtime_handle.drain_ui();
251        Ok(cleared_invalid_scopes)
252    }
253
254    fn reconcile_with_content(
255        &mut self,
256        key: Key,
257        content: &mut dyn FnMut(),
258        mut suppressed_invalid_scopes: Option<HashSet<ScopeId>>,
259    ) -> Result<bool, NodeError> {
260        self.root_key = Some(key);
261        let mut did_work = false;
262        let mut root_render_replays = 0usize;
263        loop {
264            did_work |=
265                self.process_invalid_scopes_filtered(suppressed_invalid_scopes.take().as_ref())?;
266            if !self.take_root_render_request() {
267                return Ok(did_work);
268            }
269
270            root_render_replays += 1;
271            if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
272                debug_assert!(
273                    false,
274                    "root render replay exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant render bug"
275                );
276                log::error!(
277                    "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
278                );
279                return Ok(true);
280            }
281
282            suppressed_invalid_scopes = Some(self.render_root_pass(key, content, true)?);
283            did_work = true;
284        }
285    }
286
287    pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
288        self.reset_last_pass_stats();
289        let _ = self.render_root_pass(key, &mut content, false)?;
290        let _ = self.process_invalid_scopes()?;
291        Ok(())
292    }
293
294    /// Perform a root render and continue replaying any resulting root-render
295    /// requests until the composition reaches a stable fixpoint.
296    pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
297        self.reset_last_pass_stats();
298        let suppressed_invalid_scopes = self.render_root_pass(key, &mut content, true)?;
299        let _ = self.reconcile_with_content(key, &mut content, Some(suppressed_invalid_scopes))?;
300        Ok(())
301    }
302
303    /// Process invalid scopes and any resulting root-render requests until the
304    /// composition reaches a stable fixpoint for the supplied root content.
305    pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
306        self.reconcile_with_content(key, &mut content, None)
307    }
308
309    /// Returns true if composition needs to process invalid scopes (recompose).
310    ///
311    /// This checks both:
312    /// - `has_updates()`: composition scopes that were invalidated by state changes
313    /// - `needs_frame()`: animation callbacks that may have pending work
314    ///
315    /// Note: For scroll performance, ensure scroll state changes use Cell<T> instead
316    /// of MutableState<T> to avoid triggering recomposition on every scroll frame.
317    pub fn should_render(&self) -> bool {
318        self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
319    }
320
321    pub fn runtime_handle(&self) -> RuntimeHandle {
322        self.runtime.handle()
323    }
324
325    pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
326        ApplierGuard::new(self.applier.borrow_typed())
327    }
328
329    pub fn root(&self) -> Option<NodeId> {
330        self.root
331    }
332
333    pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
334        self.slots.borrow().debug_dump_groups()
335    }
336
337    pub fn debug_dump_all_slots(&self) -> Vec<(usize, String)> {
338        self.slots.borrow().debug_dump_all_slots()
339    }
340
341    pub fn slot_table_heap_bytes(&self) -> usize {
342        self.slots.borrow().heap_bytes()
343    }
344
345    pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
346        self.slots.borrow().debug_stats()
347    }
348
349    pub fn debug_slot_value_type_counts(&self, limit: usize) -> Vec<SlotValueTypeDebugStat> {
350        self.slots.borrow().debug_value_type_counts(limit)
351    }
352
353    pub fn debug_observer_stats(&self) -> snapshot_state_observer::SnapshotStateObserverDebugStats {
354        self.observer.debug_stats()
355    }
356
357    pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
358        self.last_pass_stats
359    }
360
361    fn process_invalid_scopes_filtered(
362        &mut self,
363        suppressed_invalid_scopes: Option<&HashSet<ScopeId>>,
364    ) -> Result<bool, NodeError> {
365        let runtime_handle = self.runtime_handle();
366        let mut did_recompose = false;
367        let mut loop_count = 0;
368        loop {
369            loop_count += 1;
370            if loop_count > ROOT_RENDER_REPLAY_LIMIT {
371                debug_assert!(
372                    false,
373                    "process_invalid_scopes exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant recomposition bug (a scope keeps re-invalidating)"
374                );
375                log::error!(
376                    "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
377                );
378                break;
379            }
380            runtime_handle.drain_ui();
381            let pending = runtime_handle.take_invalidated_scopes();
382            if pending.is_empty() {
383                break;
384            }
385            let mut scopes = Vec::new();
386            for (id, weak) in pending {
387                if suppressed_invalid_scopes.is_some_and(|suppressed| suppressed.contains(&id)) {
388                    runtime_handle.mark_scope_recomposed(id);
389                    continue;
390                }
391                if let Some(inner) = weak.upgrade() {
392                    scopes.push(RecomposeScope { inner });
393                } else {
394                    runtime_handle.mark_scope_recomposed(id);
395                }
396            }
397            if scopes.is_empty() {
398                continue;
399            }
400            did_recompose = true;
401            let runtime_clone = runtime_handle.clone();
402            let root_host = self.slots_host();
403            let mut scope_groups: Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> = Vec::new();
404            let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
405            for scope in scopes {
406                let host = scope.slots_host().unwrap_or_else(|| Rc::clone(&root_host));
407                let host_key = Rc::as_ptr(&host) as usize;
408                if let Some(index) = scope_group_index.get(&host_key).copied() {
409                    scope_groups[index].1.push(scope);
410                } else {
411                    scope_group_index.insert(host_key, scope_groups.len());
412                    scope_groups.push((host, vec![scope]));
413                }
414            }
415            let side_effects = {
416                let _teardown = runtime::enter_state_teardown_scope();
417                let composer = Composer::new(
418                    Rc::clone(&root_host),
419                    self.applier_host(),
420                    runtime_clone,
421                    self.observer.clone(),
422                    self.root,
423                );
424                self.observer.begin_frame();
425                let (root, commands, side_effects, requested_root_render) =
426                    composer.install(|composer| {
427                        for (host, scopes) in scope_groups.into_iter() {
428                            if Rc::ptr_eq(&host, &root_host) {
429                                for scope in &scopes {
430                                    composer.recranpose_group(scope);
431                                }
432                            } else {
433                                composer.with_slot_override(host, |composer| {
434                                    for scope in &scopes {
435                                        composer.recranpose_group(scope);
436                                    }
437                                });
438                            }
439                        }
440                        let root = composer.root();
441                        let commands = composer.take_commands();
442                        let side_effects = composer.take_side_effects();
443                        let requested_root_render = composer.take_root_render_request();
444                        (root, commands, side_effects, requested_root_render)
445                    });
446                self.record_pass_stats(&commands, &side_effects);
447                {
448                    let mut applier = self.applier.borrow_dyn();
449                    commands.apply(&mut *applier)?;
450                    for update in runtime_handle.take_updates() {
451                        update.apply(&mut *applier)?;
452                    }
453                }
454                if root.is_some() {
455                    self.root = root;
456                }
457                {
458                    let mut slots = self.slots.borrow_mut();
459                    slots.flush();
460                }
461                let removed_orphaned = self.finalize_compaction()?;
462                if removed_orphaned {
463                    did_recompose = true;
464                    self.root_render_requested = true;
465                }
466                if requested_root_render {
467                    self.root_render_requested = true;
468                }
469                side_effects
470            };
471            runtime_handle.drain_ui();
472            for effect in side_effects {
473                effect();
474            }
475            runtime_handle.drain_ui();
476            if self.root_render_requested {
477                break;
478            }
479        }
480        self.finalize_runtime_state();
481        Ok(did_recompose)
482    }
483
484    pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
485        self.process_invalid_scopes_filtered(None)
486    }
487
488    pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
489        let updates = self.runtime_handle().take_updates();
490        let mut applier = self.applier.borrow_dyn();
491        for update in updates {
492            update.apply(&mut *applier)?;
493        }
494        Ok(())
495    }
496}
497
498impl<A: Applier + 'static> Drop for Composition<A> {
499    fn drop(&mut self) {
500        self.observer.stop();
501    }
502}