Skip to main content

cranpose_core/
composition.rs

1use std::rc::Rc;
2
3use smallvec::SmallVec;
4use web_time::Instant;
5
6use crate::{
7    Applier, ApplierGuard, ApplierHost, CommandQueue, Composer, CompositionPassDebugStats,
8    ConcreteApplierHost, DefaultScheduler, Key, NodeError, NodeId, RecomposeScope, RetentionPolicy,
9    Runtime, RuntimeHandle, ScopeId, SlotDebugSnapshot, SlotTable, SlotTableDebugStats, SlotsHost,
10    collections::map::HashMap, debug_scope_invalidation_sources, debug_scope_label, runtime,
11    scheduler_ref,
12};
13
14/// A running composition that turns root content into nodes. `A` is the applier type that creates
15/// and edits those nodes.
16pub struct Composition<A: Applier + 'static> {
17    /// The composer of the last pass over the root slots, kept so the next
18    /// pass reuses its core. First, so it lets go of the tree before the
19    /// fields below do.
20    parked_composer: Option<Composer>,
21    pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
22    pub(crate) slots: Rc<SlotsHost>,
23    pub(crate) applier: Rc<ConcreteApplierHost<A>>,
24    pub(crate) runtime: Runtime,
25    pub(crate) root: Option<NodeId>,
26    pub(crate) root_key: Option<Key>,
27    pub(crate) root_render_requested: bool,
28    pub(crate) last_pass_stats: CompositionPassDebugStats,
29    teardown: Option<runtime::StateTeardownScope>,
30}
31
32/// Upper bound on chained root-render replays and scope-recomposition rounds.
33///
34/// Each root render clears `root_render_requested` but may re-raise it if a
35/// recompose pass inside `render()` promotes a scope callback to the root
36/// (see `Composer::recompose_group` in recompose.rs — callbacks that cannot
37/// run invalidate their `callback_promotion_target`, and if no ancestor can
38/// absorb the callback, `request_root_render()` is called). Each promotion
39/// walks up one parent scope, so natural convergence is bounded by the
40/// composition depth. The same invariant bounds `process_invalid_scopes`:
41/// recomposing a scope may invalidate others, but the chain must terminate.
42///
43/// This constant is a safety net for reentrant-render bugs. Exceeding it
44/// trips a `debug_assert!` in dev/test builds (loud failure so regressions
45/// are caught immediately) and falls back to a break + `log::error!` in
46/// release builds so end users do not see the UI thread panic.
47pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
48
49impl<A: Applier + 'static> Composition<A> {
50    /// Creates a composition that builds its nodes through `applier` and uses a new runtime with
51    /// the default scheduler.
52    pub fn new(applier: A) -> Self {
53        Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
54    }
55
56    /// Creates a composition that builds its nodes through `applier` and uses `runtime` for state
57    /// invalidation and scheduling.
58    pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
59        let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
60        let slots = Rc::new(SlotsHost::new(SlotTable::new()));
61        let applier = Rc::new(ConcreteApplierHost::new(applier));
62        Self {
63            parked_composer: None,
64            composer_state,
65            slots,
66            applier,
67            runtime,
68            root: None,
69            root_key: None,
70            root_render_requested: false,
71            last_pass_stats: CompositionPassDebugStats::default(),
72            teardown: None,
73        }
74    }
75
76    /// Returns the root group key captured from the most recent `render()` call,
77    /// or `None` before the first render.
78    pub fn root_key(&self) -> Option<Key> {
79        self.root_key
80    }
81
82    /// Sets the budget and the eviction order for groups retained when they leave the composition.
83    /// See `RetentionMode`.
84    pub fn set_retention_policy(&self, policy: RetentionPolicy) {
85        self.composer_state.set_retention_policy(policy);
86    }
87
88    fn slots_host(&self) -> Rc<SlotsHost> {
89        Rc::clone(&self.slots)
90    }
91
92    fn applier_host(&self) -> Rc<dyn ApplierHost> {
93        self.applier.clone()
94    }
95
96    fn reset_last_pass_stats(&mut self) {
97        self.last_pass_stats = CompositionPassDebugStats::default();
98    }
99
100    fn maybe_dump_slot_table(&self, label: &str) {
101        if !crate::env_flag!("COMPOSE_DEBUG_SLOT_TABLE") {
102            return;
103        }
104        eprintln!(
105            "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
106            self.debug_slot_snapshot()
107        );
108    }
109
110    /// Returns whether a root render was requested and clears the request.
111    pub fn take_root_render_request(&mut self) -> bool {
112        std::mem::take(&mut self.root_render_requested)
113    }
114
115    /// Requests another render of the root content and schedules a frame for it.
116    pub fn request_root_render(&mut self) {
117        self.root_render_requested = true;
118        self.runtime.handle().schedule();
119    }
120
121    fn record_pass_stats(
122        &mut self,
123        commands: &CommandQueue,
124        side_effects: &Vec<Box<dyn FnOnce()>>,
125    ) {
126        self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
127        self.last_pass_stats.commands_cap =
128            self.last_pass_stats.commands_cap.max(commands.capacity());
129        self.last_pass_stats.command_payload_len_bytes = self
130            .last_pass_stats
131            .command_payload_len_bytes
132            .max(commands.payload_len_bytes());
133        self.last_pass_stats.command_payload_cap_bytes = self
134            .last_pass_stats
135            .command_payload_cap_bytes
136            .max(commands.payload_capacity_bytes());
137        self.last_pass_stats.sync_children_len = self
138            .last_pass_stats
139            .sync_children_len
140            .max(commands.sync_children.len());
141        self.last_pass_stats.sync_children_cap = self
142            .last_pass_stats
143            .sync_children_cap
144            .max(commands.sync_children.capacity());
145        self.last_pass_stats.sync_child_ids_len = self
146            .last_pass_stats
147            .sync_child_ids_len
148            .max(commands.sync_child_ids.len());
149        self.last_pass_stats.sync_child_ids_cap = self
150            .last_pass_stats
151            .sync_child_ids_cap
152            .max(commands.sync_child_ids.capacity());
153        self.last_pass_stats.side_effects_len = self
154            .last_pass_stats
155            .side_effects_len
156            .max(side_effects.len());
157        self.last_pass_stats.side_effects_cap = self
158            .last_pass_stats
159            .side_effects_cap
160            .max(side_effects.capacity());
161    }
162
163    fn finalize_runtime_state(&mut self) {
164        let runtime_handle = self.runtime_handle();
165        if !self.runtime.has_updates()
166            && !runtime_handle.has_invalid_scopes()
167            && !runtime_handle.has_frame_callbacks()
168            && !runtime_handle.has_pending_ui()
169        {
170            self.runtime.set_needs_frame(false);
171        }
172    }
173
174    fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
175        host.abandon_after_apply_failure();
176        if Rc::ptr_eq(host, &self.slots) {
177            self.root = None;
178        }
179        self.root_render_requested = true;
180        self.finalize_runtime_state();
181    }
182
183    fn apply_commands_and_updates_for_host(
184        &mut self,
185        host: &Rc<SlotsHost>,
186        runtime_handle: &RuntimeHandle,
187        mut commands: CommandQueue,
188    ) -> Result<(), NodeError> {
189        let result = {
190            let mut applier = self.applier.borrow_dyn();
191            let mut result = commands.apply(&mut *applier);
192            if result.is_ok() {
193                for update in runtime_handle.take_updates() {
194                    if let Err(err) = update.apply(&mut *applier) {
195                        result = Err(err);
196                        break;
197                    }
198                }
199            }
200            result
201        };
202        crate::composer::recycle_commands(commands);
203        if result.is_err() {
204            self.abandon_host_after_apply_failure(host);
205        }
206        result
207    }
208
209    fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
210        self.root_key = Some(key);
211        self.root_render_requested = false;
212        let runtime_handle = self.runtime_handle();
213        runtime_handle.drain_ui();
214        let side_effects = {
215            let _teardown = runtime::enter_state_teardown_scope();
216            let composer = Composer::new_with_shared_state(
217                Rc::clone(&self.composer_state),
218                Rc::clone(&self.slots),
219                self.applier.clone(),
220                runtime_handle.clone(),
221                self.root,
222            );
223            let (root, commands, side_effects, compact_applier) = composer.install(|composer| {
224                let ((), outcome) = composer.try_with_slot_host_pass(
225                    Rc::clone(&self.slots),
226                    crate::slot::SlotPassMode::Compose,
227                    |composer| composer.with_group(key, |_| content()),
228                )?;
229                let root = composer.root();
230                let commands = composer.take_commands();
231                let side_effects = composer.take_side_effects();
232                Ok((root, commands, side_effects, outcome.compacted))
233            })?;
234            self.record_pass_stats(&commands, &side_effects);
235            self.apply_commands_and_updates_for_host(
236                &Rc::clone(&self.slots),
237                &runtime_handle,
238                commands,
239            )?;
240            if compact_applier {
241                self.applier.compact();
242                self.applier.borrow_dyn().clear_recycled_nodes();
243            }
244
245            self.root = root;
246            side_effects
247        };
248        runtime_handle.drain_ui();
249        for effect in side_effects {
250            effect();
251        }
252        runtime_handle.drain_ui();
253        self.maybe_dump_slot_table("root_render_pass");
254        Ok(())
255    }
256
257    fn reconcile_with_content(
258        &mut self,
259        key: Key,
260        content: &mut dyn FnMut(),
261    ) -> Result<bool, NodeError> {
262        self.root_key = Some(key);
263        let mut did_work = false;
264        let mut root_render_replays = 0usize;
265        loop {
266            did_work |= self.process_invalid_scopes_until_root_request()?;
267            if !self.take_root_render_request() {
268                return Ok(did_work);
269            }
270
271            root_render_replays += 1;
272            if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
273                log::error!(
274                    "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
275                );
276                return Err(NodeError::RecompositionLimitExceeded {
277                    operation: "root render replay",
278                    limit: ROOT_RENDER_REPLAY_LIMIT,
279                });
280            }
281
282            self.render_root_pass(key, content)?;
283            did_work = true;
284        }
285    }
286
287    /// Composes `content` as the root group identified by `key`, then recomposes the scopes that
288    /// became invalid during the pass.
289    pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
290        self.reset_last_pass_stats();
291        self.render_root_pass(key, &mut content)?;
292        let _ = self.process_invalid_scopes()?;
293        Ok(())
294    }
295
296    /// Perform a root render and continue replaying any resulting root-render
297    /// requests until the composition reaches a stable fixpoint.
298    pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
299        self.reset_last_pass_stats();
300        self.render_root_pass(key, &mut content)?;
301        let _ = self.reconcile_with_content(key, &mut content)?;
302        Ok(())
303    }
304
305    /// Process invalid scopes and any resulting root-render requests until the
306    /// composition reaches a stable fixpoint for the supplied root content.
307    pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
308        self.reconcile_with_content(key, &mut content)
309    }
310
311    /// Returns true if composition needs to process invalid scopes (recompose).
312    ///
313    /// This checks both:
314    /// - `has_updates()`: composition scopes that were invalidated by state changes
315    /// - `needs_frame()`: animation callbacks that may have pending work
316    ///
317    /// Note: For scroll performance, ensure scroll state changes use `Cell<T>` instead
318    /// of `MutableState<T>` to avoid triggering recomposition on every scroll frame.
319    pub fn should_render(&self) -> bool {
320        self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
321    }
322
323    /// Whether any composition scope is actually invalid, i.e. whether running
324    /// the composable tree could produce a different result than last time.
325    ///
326    /// Deliberately excludes [`Runtime::needs_frame`], which [`Self::should_render`]
327    /// includes. An armed frame callback means the *runtime* owes someone a
328    /// tick - a future to resume, a dispatcher to drain - and every app with a
329    /// game loop or a polling effect has one armed at all times. Re-running the
330    /// composition for it recomposes a tree that nothing invalidated. Callers
331    /// deciding "should I tick" want [`Self::should_render`]; callers deciding
332    /// "should I recompose" want this.
333    ///
334    /// It is only meaningful *after* the frame callbacks have been drained: a
335    /// callback that writes state invalidates its readers as it runs, so the
336    /// answer is a question about work already discovered, not work still to
337    /// come.
338    pub fn should_recompose(&self) -> bool {
339        self.root_render_requested || self.runtime.has_updates()
340    }
341
342    /// Returns a handle to the runtime this composition uses.
343    pub fn runtime_handle(&self) -> RuntimeHandle {
344        self.runtime.handle()
345    }
346
347    /// Borrows the applier mutably. Panics if it is already borrowed.
348    pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
349        self.applier.borrow_typed()
350    }
351
352    /// Returns the root node a pass produced, or `None` if none has yet.
353    pub fn root(&self) -> Option<NodeId> {
354        self.root
355    }
356
357    /// Returns one `(group index, call site key, scope id, subtree length)` tuple per group in the
358    /// slot table, for debugging.
359    pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
360        self.slots.borrow().debug_dump_groups()
361    }
362
363    /// Returns the entries of the slot table, for debugging.
364    pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
365        self.slots.borrow().debug_dump_slot_entries()
366    }
367
368    /// Returns the heap memory in bytes that the slot table holds.
369    pub fn slot_table_heap_bytes(&self) -> usize {
370        self.slots.borrow().heap_bytes()
371    }
372
373    /// Returns size counters of the slot table, for debugging.
374    pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
375        self.slots.debug_stats()
376    }
377
378    /// Returns a copy of the slot table's structure, for debugging.
379    pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
380        self.slots.debug_snapshot()
381    }
382
383    /// Returns the buffer sizes of the last composition pass, for debugging.
384    pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
385        self.last_pass_stats
386    }
387
388    #[cfg(test)]
389    pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
390        let table = self.slots.borrow();
391        table.validate()?;
392        self.composer_state
393            .validate_host_retention(self.slots.as_ref(), &table)
394    }
395
396    /// The scopes grouped by the slot host each recomposes in, in the order
397    /// each host first appears.
398    /// The slots host `scope` recomposes in.
399    fn host_for_scope(&self, scope: &RecomposeScope, root_host: &Rc<SlotsHost>) -> Rc<SlotsHost> {
400        scope
401            .slots_runtime_state()
402            .and_then(|state| {
403                scope
404                    .slots_storage_key()
405                    .and_then(|storage_key| state.host_for_storage_key(storage_key))
406            })
407            .or_else(|| {
408                scope
409                    .slots_storage_key()
410                    .and_then(|storage_key| self.composer_state.host_for_storage_key(storage_key))
411            })
412            .unwrap_or_else(|| Rc::clone(root_host))
413    }
414
415    /// The slots host of each scope, with its scopes in order.
416    fn group_scopes_by_host(
417        &self,
418        scopes: Vec<RecomposeScope>,
419    ) -> SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> {
420        let root_host = self.slots_host();
421        // Scopes of one slots identity share a host: the usual frame keeps
422        // its scope list as the one group.
423        let first_identity = scopes.first().map(RecomposeScope::slots_host_identity);
424        if let Some(first) = scopes.first()
425            && scopes
426                .iter()
427                .all(|scope| Some(scope.slots_host_identity()) == first_identity)
428        {
429            let host = self.host_for_scope(first, &root_host);
430            return smallvec::smallvec![(host, scopes)];
431        }
432        let mut scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> = SmallVec::new();
433        let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
434        let mut previous_host: Option<(_, usize)> = None;
435        for scope in scopes {
436            let identity = scope.slots_host_identity();
437            if let Some((previous_identity, index)) = previous_host
438                && previous_identity == identity
439            {
440                scope_groups[index].1.push(scope);
441                continue;
442            }
443            let host = self.host_for_scope(&scope, &root_host);
444            let host_key = host.storage_key();
445            let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
446                scope_groups[index].1.push(scope);
447                index
448            } else {
449                scope_group_index.insert(host_key, scope_groups.len());
450                scope_groups.push((host, vec![scope]));
451                scope_groups.len() - 1
452            };
453            previous_host = Some((identity, index));
454        }
455        scope_groups
456    }
457
458    fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
459        let runtime_handle = self.runtime_handle();
460        let mut did_recompose = false;
461        let mut loop_count = 0;
462        loop {
463            loop_count += 1;
464            if loop_count > ROOT_RENDER_REPLAY_LIMIT {
465                log::error!(
466                    "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
467                );
468                return Err(NodeError::RecompositionLimitExceeded {
469                    operation: "process_invalid_scopes",
470                    limit: ROOT_RENDER_REPLAY_LIMIT,
471                });
472            }
473            runtime_handle.drain_ui();
474            self.dispose_forgotten_movables()?;
475            let Some(scopes) = runtime_handle.take_invalidated_scopes() else {
476                break;
477            };
478            if scopes.is_empty() {
479                continue;
480            }
481            did_recompose |= recomposes_content(&scopes);
482            let runtime_clone = runtime_handle.clone();
483            let scope_groups = self.group_scopes_by_host(scopes);
484            let mut host_group_index = 0usize;
485            while host_group_index < scope_groups.len() {
486                let (host, scopes) = &scope_groups[host_group_index];
487                let scope_telemetry_threshold_ms =
488                    crate::env_threshold_ms!("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS");
489                let shared_state = host
490                    .runtime_state()
491                    .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
492                    .unwrap_or_else(|| Rc::clone(&self.composer_state));
493                let side_effects = {
494                    let _teardown = runtime::enter_state_teardown_scope();
495                    let composer = Composer::for_pass(
496                        self.parked_composer.take(),
497                        shared_state,
498                        Rc::clone(host),
499                        self.applier_host(),
500                        runtime_clone.clone(),
501                        self.root,
502                    );
503                    composer.parent_stack().clear();
504                    let (root, commands, side_effects, requested_root_render, compact_applier) =
505                        composer.install(|composer| {
506                            let ((), outcome) = composer.try_with_slot_host_pass(
507                                Rc::clone(host),
508                                crate::slot::SlotPassMode::Recompose,
509                                |composer| {
510                                    for scope in scopes {
511                                        if let Some(threshold_ms) = scope_telemetry_threshold_ms {
512                                            let start = Instant::now();
513                                            composer.recompose_group(scope);
514                                            let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
515                                            if elapsed_ms >= threshold_ms {
516                                                eprintln!(
517                                                    "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
518                                                    scope.id(),
519                                                    debug_scope_label(scope.id()),
520                                                    debug_scope_invalidation_sources(scope.id())
521                                                );
522                                            }
523                                        } else {
524                                            composer.recompose_group(scope);
525                                        }
526                                    }
527                                },
528                            )?;
529                            let root = composer.root();
530                            let commands = composer.take_commands();
531                            let side_effects = composer.take_side_effects();
532                            let requested_root_render = composer.take_root_render_request();
533                            Ok((
534                                root,
535                                commands,
536                                side_effects,
537                                requested_root_render,
538                                outcome.compacted,
539                            ))
540                        })?;
541                    self.record_pass_stats(&commands, &side_effects);
542                    self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
543                    if compact_applier {
544                        self.applier.compact();
545                        self.applier.borrow_dyn().clear_recycled_nodes();
546                    }
547                    if root.is_some() {
548                        self.root = root;
549                    }
550                    if requested_root_render {
551                        self.root_render_requested = true;
552                    }
553                    self.park_composer(host, composer);
554                    side_effects
555                };
556                runtime_handle.drain_ui();
557                for effect in side_effects {
558                    effect();
559                }
560                runtime_handle.drain_ui();
561                self.maybe_dump_slot_table("recompose_pass");
562                if self.root_render_requested {
563                    for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
564                        for scope in remaining_scopes {
565                            runtime_handle.requeue_invalid_scope(scope);
566                        }
567                    }
568                    break;
569                }
570                host_group_index += 1;
571            }
572            recycle_scope_groups(&runtime_handle, scope_groups);
573            if self.root_render_requested {
574                break;
575            }
576        }
577        self.finalize_runtime_state();
578        Ok(did_recompose)
579    }
580
581    /// Recomposes the scopes that state changes invalidated, until none remain or a root render is
582    /// requested. Returns `true` if a pass recomposed content.
583    pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
584        self.process_invalid_scopes_until_root_request()
585    }
586
587    /// Keeps the composer of a pass over the root slots for the next pass.
588    fn park_composer(&mut self, host: &Rc<SlotsHost>, composer: Composer) {
589        if Rc::ptr_eq(host, &self.slots) {
590            self.parked_composer = composer.park();
591        }
592    }
593
594    fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
595        let runtime_handle = self.runtime_handle();
596        let ids = runtime_handle.take_forgotten_movables();
597        if ids.is_empty() {
598            return Ok(());
599        }
600        let _teardown = runtime::enter_state_teardown_scope();
601        let host = self.slots_host();
602        let composer = Composer::new_with_shared_state(
603            Rc::clone(&self.composer_state),
604            Rc::clone(&host),
605            self.applier_host(),
606            runtime_handle.clone(),
607            self.root,
608        );
609        let commands = composer.install(|composer| {
610            composer.forget_movables(&ids)?;
611            Ok::<_, NodeError>(composer.take_commands())
612        })?;
613        self.apply_commands_and_updates_for_host(&host, &runtime_handle, commands)
614    }
615}
616
617/// Whether running these scopes can change what the composition shows: a pass
618/// that only recomputes derived states changes nothing unless a derived value
619/// moved, and then its readers run in a pass of their own.
620fn recomposes_content(scopes: &[RecomposeScope]) -> bool {
621    scopes.iter().any(|scope| !scope.is_derivation())
622}
623
624/// Gives a pass's scope lists back to the runtime, for the next pass to
625/// reuse their storage.
626fn recycle_scope_groups(
627    runtime: &RuntimeHandle,
628    scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]>,
629) {
630    for (_, scopes) in scope_groups {
631        runtime.recycle_scopes(scopes);
632    }
633}
634
635impl<A: Applier + 'static> Composition<A> {
636    /// Applies the node updates that the runtime has queued to the applier, without recomposing.
637    pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
638        let updates = self.runtime_handle().take_updates();
639        let mut applier = self.applier.borrow_dyn();
640        for update in updates {
641            update.apply(&mut *applier)?;
642        }
643        Ok(())
644    }
645}
646
647impl<A: Applier + 'static> Drop for Composition<A> {
648    fn drop(&mut self) {
649        #[cfg(target_os = "android")]
650        self.runtime.restore_thread_state();
651        self.teardown = Some(runtime::enter_state_teardown_scope());
652    }
653}