Skip to main content

cranpose_core/
composition.rs

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