1use std::rc::Rc;
2
3use smallvec::SmallVec;
4use web_time::Instant;
5
6use crate::{
7 Applier, ApplierGuard, ApplierHost, Composer, ConcreteApplierHost, DIAGNOSTICS,
8 DefaultScheduler, Key, NodeError, NodeId, PausableContent, 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
14pub struct Composition<A: Applier + 'static> {
17 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 #[cfg(any(test, feature = "leak-debug"))]
29 last_pass_stats: CompositionPassDebugStats,
30 teardown: Option<runtime::StateTeardownScope>,
31}
32
33#[doc(hidden)]
36pub struct LayoutBudget {
37 state: Rc<crate::composer::ComposerRuntimeState>,
38 runtime: RuntimeHandle,
39}
40
41impl Drop for LayoutBudget {
42 fn drop(&mut self) {
43 self.state.pausing.close_layout(&self.runtime);
44 }
45}
46
47pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
63
64impl<A: Applier + 'static> Composition<A> {
65 pub fn new(applier: A) -> Self {
68 Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
69 }
70
71 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
74 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
75 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
76 let applier = Rc::new(ConcreteApplierHost::new(applier));
77 Self {
78 parked_composer: None,
79 composer_state,
80 slots,
81 applier,
82 runtime,
83 root: None,
84 root_key: None,
85 root_render_requested: false,
86 #[cfg(any(test, feature = "leak-debug"))]
87 last_pass_stats: CompositionPassDebugStats::default(),
88 teardown: None,
89 }
90 }
91
92 pub fn root_key(&self) -> Option<Key> {
95 self.root_key
96 }
97
98 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
101 self.composer_state.set_retention_policy(policy);
102 }
103
104 fn slots_host(&self) -> Rc<SlotsHost> {
105 Rc::clone(&self.slots)
106 }
107
108 fn applier_host(&self) -> Rc<dyn ApplierHost> {
109 Rc::<ConcreteApplierHost<A>>::clone(&self.applier)
110 }
111
112 #[expect(clippy::print_stderr, reason = "COMPOSE_DEBUG_SLOT_TABLE asks for it")]
113 fn maybe_dump_slot_table(&self, label: &str) {
114 if !DIAGNOSTICS.compose_debug_slot_table.is_set() {
115 return;
116 }
117 eprintln!(
118 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
119 self.debug_slot_snapshot()
120 );
121 }
122
123 pub fn take_root_render_request(&mut self) -> bool {
125 std::mem::take(&mut self.root_render_requested)
126 }
127
128 pub fn request_root_render(&mut self) {
130 self.root_render_requested = true;
131 self.runtime.handle().schedule();
132 }
133
134 fn finalize_runtime_state(&mut self) {
135 if !self.runtime.has_updates()
136 && !self.runtime.has_invalid_scopes()
137 && !self.runtime.has_frame_callbacks()
138 && !self.runtime.has_pending_ui()
139 {
140 self.runtime.set_needs_frame(false);
141 }
142 }
143
144 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
145 host.abandon_after_apply_failure();
146 if Rc::ptr_eq(host, &self.slots) {
147 self.root = None;
148 }
149 self.root_render_requested = true;
150 self.finalize_runtime_state();
151 }
152
153 fn apply_commands_and_updates_for_host(
156 &mut self,
157 host: &Rc<SlotsHost>,
158 runtime_handle: &RuntimeHandle,
159 composer: &Composer,
160 ) -> Result<(), NodeError> {
161 let result = composer.with_commands(|commands| {
162 let mut applier = self.applier.borrow_dyn();
163 let mut result = commands.apply(&mut *applier);
164 if result.is_ok() {
165 for update in runtime_handle.take_updates() {
166 if let Err(err) = update.apply(&mut *applier) {
167 result = Err(err);
168 break;
169 }
170 }
171 }
172 result
173 });
174 if result.is_err() {
175 self.abandon_host_after_apply_failure(host);
176 }
177 result
178 }
179
180 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
181 self.root_key = Some(key);
182 self.root_render_requested = false;
183 let runtime_handle = self.runtime_handle();
184 runtime_handle.drain_ui();
185 let side_effects = {
186 let _teardown = runtime::enter_state_teardown_scope();
187 let composer = Composer::new_with_shared_state(
188 Rc::clone(&self.composer_state),
189 Rc::clone(&self.slots),
190 Rc::<ConcreteApplierHost<A>>::clone(&self.applier),
191 runtime_handle.clone(),
192 self.root,
193 );
194 let (root, side_effects, compact_applier) = composer.install(|composer| {
195 let ((), outcome) = composer.try_with_slot_host_pass(
196 Rc::clone(&self.slots),
197 crate::slot::SlotPassMode::Compose,
198 |composer| composer.with_group(key, |_| content()),
199 )?;
200 let root = composer.root();
201 let side_effects = composer.take_side_effects();
202 Ok((root, side_effects, outcome.compacted))
203 })?;
204 #[cfg(any(test, feature = "leak-debug"))]
205 composer.with_commands(|commands| self.last_pass_stats.record(commands, &side_effects));
206 self.apply_commands_and_updates_for_host(
207 &Rc::clone(&self.slots),
208 &runtime_handle,
209 &composer,
210 )?;
211 if compact_applier {
212 self.applier.compact();
213 self.applier.borrow_dyn().clear_recycled_nodes();
214 }
215
216 self.root = root;
217 side_effects
218 };
219 runtime_handle.drain_ui();
220 for effect in side_effects {
221 effect();
222 }
223 runtime_handle.drain_ui();
224 self.maybe_dump_slot_table("root_render_pass");
225 Ok(())
226 }
227
228 fn reconcile_with_content(
229 &mut self,
230 key: Key,
231 content: &mut dyn FnMut(),
232 ) -> Result<bool, NodeError> {
233 self.root_key = Some(key);
234 let mut did_work = false;
235 let mut root_render_replays = 0usize;
236 loop {
237 did_work |= self.process_invalid_scopes_within_budget()?;
238 if !self.take_root_render_request() {
239 return Ok(did_work);
240 }
241
242 root_render_replays += 1;
243 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
244 log::error!(
245 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
246 );
247 return Err(NodeError::RecompositionLimitExceeded {
248 operation: "root render replay",
249 limit: ROOT_RENDER_REPLAY_LIMIT,
250 });
251 }
252
253 self.render_root_pass(key, content)?;
254 did_work = true;
255 }
256 }
257
258 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
261 #[cfg(any(test, feature = "leak-debug"))]
262 {
263 self.last_pass_stats = CompositionPassDebugStats::default();
264 }
265 self.render_root_pass(key, &mut content)?;
266 let _ = self.process_invalid_scopes()?;
267 Ok(())
268 }
269
270 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
273 #[cfg(any(test, feature = "leak-debug"))]
274 {
275 self.last_pass_stats = CompositionPassDebugStats::default();
276 }
277 self.render_root_pass(key, &mut content)?;
278 let _ = self.reconcile_with_content(key, &mut content)?;
279 Ok(())
280 }
281
282 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
285 self.reconcile_with_content(key, &mut content)
286 }
287
288 pub fn should_render(&self) -> bool {
297 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
298 }
299
300 pub fn should_recompose(&self) -> bool {
316 self.root_render_requested || self.runtime.has_updates()
317 }
318
319 pub fn set_pausable_budget(&self, budget: Option<std::time::Duration>) {
324 self.composer_state.pausing.set_budget(budget);
325 }
326
327 #[doc(hidden)]
332 pub fn open_layout_budget(&self) -> LayoutBudget {
333 self.composer_state.pausing.open_layout();
334 LayoutBudget {
335 state: Rc::clone(&self.composer_state),
336 runtime: self.runtime.handle(),
337 }
338 }
339
340 pub fn runtime_handle(&self) -> RuntimeHandle {
342 self.runtime.handle()
343 }
344
345 pub fn runtime(&self) -> &Runtime {
347 &self.runtime
348 }
349
350 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
352 self.applier.borrow_typed()
353 }
354
355 pub fn root(&self) -> Option<NodeId> {
357 self.root
358 }
359
360 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
363 self.slots.borrow().debug_dump_groups()
364 }
365
366 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
368 self.slots.borrow().debug_dump_slot_entries()
369 }
370
371 pub fn slot_table_heap_bytes(&self) -> usize {
373 self.slots.borrow().heap_bytes()
374 }
375
376 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
378 self.slots.debug_stats()
379 }
380
381 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
383 self.slots.debug_snapshot()
384 }
385
386 #[cfg(any(test, feature = "leak-debug"))]
388 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
389 self.last_pass_stats
390 }
391
392 #[cfg(test)]
393 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
394 let table = self.slots.borrow();
395 table.validate()?;
396 self.composer_state
397 .validate_host_retention(self.slots.as_ref(), &table)
398 }
399
400 fn host_for_scope(&self, scope: &RecomposeScope, root_host: &Rc<SlotsHost>) -> Rc<SlotsHost> {
404 scope
405 .slots_runtime_state()
406 .and_then(|state| {
407 scope
408 .slots_storage_key()
409 .and_then(|storage_key| state.host_for_storage_key(storage_key))
410 })
411 .or_else(|| {
412 scope
413 .slots_storage_key()
414 .and_then(|storage_key| self.composer_state.host_for_storage_key(storage_key))
415 })
416 .unwrap_or_else(|| Rc::clone(root_host))
417 }
418
419 fn group_scopes_by_host(
421 &self,
422 scopes: Vec<RecomposeScope>,
423 ) -> SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> {
424 let root_host = self.slots_host();
425 let first_identity = scopes.first().map(RecomposeScope::slots_host_identity);
428 if let Some(first) = scopes.first()
429 && scopes
430 .iter()
431 .all(|scope| Some(scope.slots_host_identity()) == first_identity)
432 {
433 let host = self.host_for_scope(first, &root_host);
434 return smallvec::smallvec![(host, scopes)];
435 }
436 let mut scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> = SmallVec::new();
437 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
438 let mut previous_host: Option<(_, usize)> = None;
439 for scope in scopes {
440 let identity = scope.slots_host_identity();
441 if let Some((previous_identity, index)) = previous_host
442 && previous_identity == identity
443 {
444 scope_groups[index].1.push(scope);
445 continue;
446 }
447 let host = self.host_for_scope(&scope, &root_host);
448 let host_key = host.storage_key();
449 let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
450 scope_groups[index].1.push(scope);
451 index
452 } else {
453 scope_group_index.insert(host_key, scope_groups.len());
454 scope_groups.push((host, vec![scope]));
455 scope_groups.len() - 1
456 };
457 previous_host = Some((identity, index));
458 }
459 scope_groups
460 }
461
462 fn process_invalid_scopes_within_budget(&mut self) -> Result<bool, NodeError> {
465 self.composer_state.pausing.start_budget();
466 let result = self.process_invalid_scopes_until_root_request();
467 self.composer_state.pausing.end_budget(&self.runtime);
468 result
469 }
470
471 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
472 let runtime_handle = self.runtime_handle();
473 let mut did_recompose = false;
474 let mut loop_count = 0;
475 loop {
476 loop_count += 1;
477 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
478 log::error!(
479 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
480 );
481 return Err(NodeError::RecompositionLimitExceeded {
482 operation: "process_invalid_scopes",
483 limit: ROOT_RENDER_REPLAY_LIMIT,
484 });
485 }
486 runtime_handle.drain_ui();
487 self.dispose_forgotten_movables()?;
488 let (scopes, batch) = match runtime_handle.take_invalidated_scopes() {
489 Some(scopes) => (scopes, Batch::Invalid),
490 None => match self.composer_state.pausing.take_resumable() {
491 Some((content, scopes)) => (scopes, Batch::Waiting(content)),
492 None => break,
493 },
494 };
495 let Some(scopes) = self.rerun_derivations(&runtime_handle, scopes) else {
496 continue;
497 };
498 did_recompose = true;
499 let scope_groups = self.group_scopes_by_host(scopes);
500 for (index, (host, scopes)) in scope_groups.iter().enumerate() {
501 let side_effects =
502 self.recompose_host_scopes(host, scopes, &batch, &runtime_handle)?;
503 runtime_handle.drain_ui();
504 for effect in side_effects {
505 effect();
506 }
507 runtime_handle.drain_ui();
508 self.maybe_dump_slot_table("recompose_pass");
509 if self.root_render_requested {
510 requeue_scopes(&runtime_handle, &batch, &scope_groups[index + 1..]);
511 break;
512 }
513 }
514 recycle_scope_groups(&runtime_handle, scope_groups);
515 if let Batch::Waiting(content) = batch {
516 self.composer_state.pausing.finish_resumed(content);
517 }
518 if self.root_render_requested {
519 break;
520 }
521 }
522 self.finalize_runtime_state();
523 Ok(did_recompose)
524 }
525
526 fn recompose_host_scopes(
529 &mut self,
530 host: &Rc<SlotsHost>,
531 scopes: &[RecomposeScope],
532 batch: &Batch,
533 runtime_handle: &RuntimeHandle,
534 ) -> Result<Vec<Box<dyn FnOnce()>>, NodeError> {
535 let shared_state = host
536 .runtime_state()
537 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
538 .unwrap_or_else(|| Rc::clone(&self.composer_state));
539 let threshold_ms = DIAGNOSTICS.recompose_scope_telemetry_ms.threshold_ms();
540 let _teardown = runtime::enter_state_teardown_scope();
541 let composer = Composer::for_pass(
542 self.parked_composer.take(),
543 shared_state,
544 Rc::clone(host),
545 self.applier_host(),
546 runtime_handle.clone(),
547 self.root,
548 );
549 composer.parent_stack().clear();
550 let (root, side_effects, requested_root_render, compact_applier) =
551 composer.install(|composer| {
552 let ((), outcome) = composer.try_with_slot_host_pass(
553 Rc::clone(host),
554 crate::slot::SlotPassMode::Recompose,
555 |composer| match (batch, threshold_ms) {
556 (Batch::Invalid, None) => {
557 for scope in scopes {
558 composer.recompose_group(scope);
559 }
560 }
561 (Batch::Invalid, Some(threshold_ms)) => {
562 recompose_timed(composer, scopes, threshold_ms);
563 }
564 (Batch::Waiting(content), _) => {
565 composer.resume_waiting_bodies(content, scopes);
566 }
567 },
568 )?;
569 let root = composer.root();
570 let side_effects = composer.take_side_effects();
571 let requested_root_render = composer.take_root_render_request();
572 Ok((root, side_effects, requested_root_render, outcome.compacted))
573 })?;
574 #[cfg(any(test, feature = "leak-debug"))]
575 composer.with_commands(|commands| self.last_pass_stats.record(commands, &side_effects));
576 self.apply_commands_and_updates_for_host(host, runtime_handle, &composer)?;
577 if compact_applier {
578 self.applier.compact();
579 self.applier.borrow_dyn().clear_recycled_nodes();
580 }
581 if root.is_some() {
582 self.root = root;
583 }
584 if requested_root_render {
585 self.root_render_requested = true;
586 }
587 self.park_composer(host, composer);
588 Ok(side_effects)
589 }
590
591 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
594 self.process_invalid_scopes_within_budget()
595 }
596
597 fn rerun_derivations(
603 &mut self,
604 runtime: &RuntimeHandle,
605 mut scopes: Vec<RecomposeScope>,
606 ) -> Option<Vec<RecomposeScope>> {
607 if scopes.iter().any(RecomposeScope::is_derivation) {
608 self.rerun_derivations_of(runtime, &mut scopes);
609 }
610 if scopes.is_empty() {
611 runtime.recycle_scopes(scopes);
612 return None;
613 }
614 Some(scopes)
615 }
616
617 fn rerun_derivations_of(&mut self, runtime: &RuntimeHandle, scopes: &mut Vec<RecomposeScope>) {
618 let _teardown = runtime::enter_state_teardown_scope();
619 let host = self.slots_host();
620 let composer = Composer::for_pass(
621 self.parked_composer.take(),
622 Rc::clone(&self.composer_state),
623 Rc::clone(&host),
624 self.applier_host(),
625 runtime.clone(),
626 self.root,
627 );
628 composer.install(|composer| {
629 for scope in scopes.iter().filter(|scope| scope.is_derivation()) {
630 composer.recompose_group(scope);
631 }
632 });
633 self.park_composer(&host, composer);
634 scopes.retain(|scope| !scope.is_derivation());
635 }
636
637 fn park_composer(&mut self, host: &Rc<SlotsHost>, composer: Composer) {
639 if Rc::ptr_eq(host, &self.slots) {
640 self.parked_composer = composer.park();
641 }
642 }
643
644 fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
645 let ids = self.runtime.take_forgotten_movables();
646 if ids.is_empty() {
647 return Ok(());
648 }
649 self.forget_movables(&ids)
650 }
651
652 #[inline(never)]
655 fn forget_movables(&mut self, ids: &[Key]) -> Result<(), NodeError> {
656 let runtime_handle = self.runtime_handle();
657 let _teardown = runtime::enter_state_teardown_scope();
658 let host = self.slots_host();
659 let composer = Composer::new_with_shared_state(
660 Rc::clone(&self.composer_state),
661 Rc::clone(&host),
662 self.applier_host(),
663 runtime_handle.clone(),
664 self.root,
665 );
666 composer.install(|composer| composer.forget_movables(ids))?;
667 self.apply_commands_and_updates_for_host(&host, &runtime_handle, &composer)
668 }
669
670 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
672 let updates = self.runtime.take_updates();
673 let mut applier = self.applier.borrow_dyn();
674 for update in updates {
675 update.apply(&mut *applier)?;
676 }
677 Ok(())
678 }
679}
680
681#[expect(clippy::print_stderr, reason = "a scope telemetry threshold asks")]
684fn recompose_timed(composer: &Composer, scopes: &[RecomposeScope], threshold_ms: f64) {
685 for scope in scopes {
686 let start = Instant::now();
687 composer.recompose_group(scope);
688 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
689 if elapsed_ms >= threshold_ms {
690 eprintln!(
691 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
692 scope.id(),
693 debug_scope_label(scope.id()),
694 debug_scope_invalidation_sources(scope.id())
695 );
696 }
697 }
698}
699
700enum Batch {
702 Invalid,
704 Waiting(PausableContent),
706}
707
708fn requeue_scopes(
711 runtime_handle: &RuntimeHandle,
712 batch: &Batch,
713 scope_groups: &[(Rc<SlotsHost>, Vec<RecomposeScope>)],
714) {
715 for scope in scope_groups.iter().flat_map(|(_, scopes)| scopes) {
716 match batch {
717 Batch::Invalid => runtime_handle.requeue_invalid_scope(scope),
718 Batch::Waiting(content) => content.keep_waiting(scope),
719 }
720 }
721}
722
723fn recycle_scope_groups(
726 runtime: &RuntimeHandle,
727 scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]>,
728) {
729 for (_, scopes) in scope_groups {
730 runtime.recycle_scopes(scopes);
731 }
732}
733
734impl<A: Applier + 'static> Drop for Composition<A> {
735 fn drop(&mut self) {
736 #[cfg(target_os = "android")]
737 self.runtime.restore_thread_state();
738 self.teardown = Some(runtime::enter_state_teardown_scope());
739 }
740}
741
742#[cfg(any(test, feature = "leak-debug"))]
745#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
746pub struct CompositionPassDebugStats {
747 pub commands_len: usize,
749 pub commands_cap: usize,
751 pub command_payload_len_bytes: usize,
753 pub command_payload_cap_bytes: usize,
755 pub sync_children_len: usize,
757 pub sync_children_cap: usize,
759 pub sync_child_ids_len: usize,
761 pub sync_child_ids_cap: usize,
763 pub side_effects_len: usize,
765 pub side_effects_cap: usize,
767}
768
769#[cfg(any(test, feature = "leak-debug"))]
770impl CompositionPassDebugStats {
771 fn record(&mut self, commands: &crate::CommandQueue, side_effects: &Vec<Box<dyn FnOnce()>>) {
772 self.commands_len = self.commands_len.max(commands.len());
773 self.commands_cap = self.commands_cap.max(commands.capacity());
774 self.command_payload_len_bytes = self
775 .command_payload_len_bytes
776 .max(commands.payload_len_bytes());
777 self.command_payload_cap_bytes = self
778 .command_payload_cap_bytes
779 .max(commands.payload_capacity_bytes());
780 self.sync_children_len = self.sync_children_len.max(commands.sync_children.len());
781 self.sync_children_cap = self
782 .sync_children_cap
783 .max(commands.sync_children.capacity());
784 self.sync_child_ids_len = self.sync_child_ids_len.max(commands.sync_child_ids.len());
785 self.sync_child_ids_cap = self
786 .sync_child_ids_cap
787 .max(commands.sync_child_ids.capacity());
788 self.side_effects_len = self.side_effects_len.max(side_effects.len());
789 self.side_effects_cap = self.side_effects_cap.max(side_effects.capacity());
790 }
791}