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
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 pub(crate) last_pass_stats: CompositionPassDebugStats,
29 teardown: Option<runtime::StateTeardownScope>,
30}
31
32pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
48
49impl<A: Applier + 'static> Composition<A> {
50 pub fn new(applier: A) -> Self {
53 Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
54 }
55
56 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 pub fn root_key(&self) -> Option<Key> {
79 self.root_key
80 }
81
82 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 pub fn take_root_render_request(&mut self) -> bool {
112 std::mem::take(&mut self.root_render_requested)
113 }
114
115 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 if !self.runtime.has_updates()
165 && !self.runtime.has_invalid_scopes()
166 && !self.runtime.has_frame_callbacks()
167 && !self.runtime.has_pending_ui()
168 {
169 self.runtime.set_needs_frame(false);
170 }
171 }
172
173 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
174 host.abandon_after_apply_failure();
175 if Rc::ptr_eq(host, &self.slots) {
176 self.root = None;
177 }
178 self.root_render_requested = true;
179 self.finalize_runtime_state();
180 }
181
182 fn apply_pass(
185 &mut self,
186 host: &Rc<SlotsHost>,
187 runtime_handle: &RuntimeHandle,
188 composer: &Composer,
189 side_effects: &Vec<Box<dyn FnOnce()>>,
190 ) -> Result<(), NodeError> {
191 composer.with_commands(|commands| self.record_pass_stats(commands, side_effects));
192 self.apply_commands_and_updates_for_host(host, runtime_handle, composer)
193 }
194
195 fn apply_commands_and_updates_for_host(
198 &mut self,
199 host: &Rc<SlotsHost>,
200 runtime_handle: &RuntimeHandle,
201 composer: &Composer,
202 ) -> Result<(), NodeError> {
203 let result = composer.with_commands(|commands| {
204 let mut applier = self.applier.borrow_dyn();
205 let mut result = commands.apply(&mut *applier);
206 if result.is_ok() {
207 for update in runtime_handle.take_updates() {
208 if let Err(err) = update.apply(&mut *applier) {
209 result = Err(err);
210 break;
211 }
212 }
213 }
214 result
215 });
216 if result.is_err() {
217 self.abandon_host_after_apply_failure(host);
218 }
219 result
220 }
221
222 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
223 self.root_key = Some(key);
224 self.root_render_requested = false;
225 let runtime_handle = self.runtime_handle();
226 runtime_handle.drain_ui();
227 let side_effects = {
228 let _teardown = runtime::enter_state_teardown_scope();
229 let composer = Composer::new_with_shared_state(
230 Rc::clone(&self.composer_state),
231 Rc::clone(&self.slots),
232 self.applier.clone(),
233 runtime_handle.clone(),
234 self.root,
235 );
236 let (root, side_effects, compact_applier) = composer.install(|composer| {
237 let ((), outcome) = composer.try_with_slot_host_pass(
238 Rc::clone(&self.slots),
239 crate::slot::SlotPassMode::Compose,
240 |composer| composer.with_group(key, |_| content()),
241 )?;
242 let root = composer.root();
243 let side_effects = composer.take_side_effects();
244 Ok((root, side_effects, outcome.compacted))
245 })?;
246 self.apply_pass(
247 &Rc::clone(&self.slots),
248 &runtime_handle,
249 &composer,
250 &side_effects,
251 )?;
252 if compact_applier {
253 self.applier.compact();
254 self.applier.borrow_dyn().clear_recycled_nodes();
255 }
256
257 self.root = root;
258 side_effects
259 };
260 runtime_handle.drain_ui();
261 for effect in side_effects {
262 effect();
263 }
264 runtime_handle.drain_ui();
265 self.maybe_dump_slot_table("root_render_pass");
266 Ok(())
267 }
268
269 fn reconcile_with_content(
270 &mut self,
271 key: Key,
272 content: &mut dyn FnMut(),
273 ) -> Result<bool, NodeError> {
274 self.root_key = Some(key);
275 let mut did_work = false;
276 let mut root_render_replays = 0usize;
277 loop {
278 did_work |= self.process_invalid_scopes_until_root_request()?;
279 if !self.take_root_render_request() {
280 return Ok(did_work);
281 }
282
283 root_render_replays += 1;
284 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
285 log::error!(
286 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
287 );
288 return Err(NodeError::RecompositionLimitExceeded {
289 operation: "root render replay",
290 limit: ROOT_RENDER_REPLAY_LIMIT,
291 });
292 }
293
294 self.render_root_pass(key, content)?;
295 did_work = true;
296 }
297 }
298
299 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
302 self.reset_last_pass_stats();
303 self.render_root_pass(key, &mut content)?;
304 let _ = self.process_invalid_scopes()?;
305 Ok(())
306 }
307
308 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
311 self.reset_last_pass_stats();
312 self.render_root_pass(key, &mut content)?;
313 let _ = self.reconcile_with_content(key, &mut content)?;
314 Ok(())
315 }
316
317 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
320 self.reconcile_with_content(key, &mut content)
321 }
322
323 pub fn should_render(&self) -> bool {
332 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
333 }
334
335 pub fn should_recompose(&self) -> bool {
351 self.root_render_requested || self.runtime.has_updates()
352 }
353
354 pub fn runtime_handle(&self) -> RuntimeHandle {
356 self.runtime.handle()
357 }
358
359 pub fn runtime(&self) -> &Runtime {
361 &self.runtime
362 }
363
364 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
366 self.applier.borrow_typed()
367 }
368
369 pub fn root(&self) -> Option<NodeId> {
371 self.root
372 }
373
374 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
377 self.slots.borrow().debug_dump_groups()
378 }
379
380 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
382 self.slots.borrow().debug_dump_slot_entries()
383 }
384
385 pub fn slot_table_heap_bytes(&self) -> usize {
387 self.slots.borrow().heap_bytes()
388 }
389
390 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
392 self.slots.debug_stats()
393 }
394
395 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
397 self.slots.debug_snapshot()
398 }
399
400 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
402 self.last_pass_stats
403 }
404
405 #[cfg(test)]
406 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
407 let table = self.slots.borrow();
408 table.validate()?;
409 self.composer_state
410 .validate_host_retention(self.slots.as_ref(), &table)
411 }
412
413 fn host_for_scope(&self, scope: &RecomposeScope, root_host: &Rc<SlotsHost>) -> Rc<SlotsHost> {
417 scope
418 .slots_runtime_state()
419 .and_then(|state| {
420 scope
421 .slots_storage_key()
422 .and_then(|storage_key| state.host_for_storage_key(storage_key))
423 })
424 .or_else(|| {
425 scope
426 .slots_storage_key()
427 .and_then(|storage_key| self.composer_state.host_for_storage_key(storage_key))
428 })
429 .unwrap_or_else(|| Rc::clone(root_host))
430 }
431
432 fn group_scopes_by_host(
434 &self,
435 scopes: Vec<RecomposeScope>,
436 ) -> SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> {
437 let root_host = self.slots_host();
438 let first_identity = scopes.first().map(RecomposeScope::slots_host_identity);
441 if let Some(first) = scopes.first()
442 && scopes
443 .iter()
444 .all(|scope| Some(scope.slots_host_identity()) == first_identity)
445 {
446 let host = self.host_for_scope(first, &root_host);
447 return smallvec::smallvec![(host, scopes)];
448 }
449 let mut scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> = SmallVec::new();
450 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
451 let mut previous_host: Option<(_, usize)> = None;
452 for scope in scopes {
453 let identity = scope.slots_host_identity();
454 if let Some((previous_identity, index)) = previous_host
455 && previous_identity == identity
456 {
457 scope_groups[index].1.push(scope);
458 continue;
459 }
460 let host = self.host_for_scope(&scope, &root_host);
461 let host_key = host.storage_key();
462 let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
463 scope_groups[index].1.push(scope);
464 index
465 } else {
466 scope_group_index.insert(host_key, scope_groups.len());
467 scope_groups.push((host, vec![scope]));
468 scope_groups.len() - 1
469 };
470 previous_host = Some((identity, index));
471 }
472 scope_groups
473 }
474
475 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
476 let runtime_handle = self.runtime_handle();
477 let mut did_recompose = false;
478 let mut loop_count = 0;
479 loop {
480 loop_count += 1;
481 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
482 log::error!(
483 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
484 );
485 return Err(NodeError::RecompositionLimitExceeded {
486 operation: "process_invalid_scopes",
487 limit: ROOT_RENDER_REPLAY_LIMIT,
488 });
489 }
490 runtime_handle.drain_ui();
491 self.dispose_forgotten_movables()?;
492 let Some(scopes) = runtime_handle.take_invalidated_scopes() else {
493 break;
494 };
495 if scopes.is_empty() {
496 continue;
497 }
498 did_recompose |= recomposes_content(&scopes);
499 let runtime_clone = runtime_handle.clone();
500 let scope_groups = self.group_scopes_by_host(scopes);
501 let mut host_group_index = 0usize;
502 while host_group_index < scope_groups.len() {
503 let (host, scopes) = &scope_groups[host_group_index];
504 let scope_telemetry_threshold_ms =
505 crate::env_threshold_ms!("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS");
506 let shared_state = host
507 .runtime_state()
508 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
509 .unwrap_or_else(|| Rc::clone(&self.composer_state));
510 let side_effects = {
511 let _teardown = runtime::enter_state_teardown_scope();
512 let composer = Composer::for_pass(
513 self.parked_composer.take(),
514 shared_state,
515 Rc::clone(host),
516 self.applier_host(),
517 runtime_clone.clone(),
518 self.root,
519 );
520 composer.parent_stack().clear();
521 let (root, side_effects, requested_root_render, compact_applier) =
522 composer.install(|composer| {
523 let ((), outcome) = composer.try_with_slot_host_pass(
524 Rc::clone(host),
525 crate::slot::SlotPassMode::Recompose,
526 |composer| {
527 for scope in scopes {
528 if let Some(threshold_ms) = scope_telemetry_threshold_ms {
529 let start = Instant::now();
530 composer.recompose_group(scope);
531 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
532 if elapsed_ms >= threshold_ms {
533 eprintln!(
534 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
535 scope.id(),
536 debug_scope_label(scope.id()),
537 debug_scope_invalidation_sources(scope.id())
538 );
539 }
540 } else {
541 composer.recompose_group(scope);
542 }
543 }
544 },
545 )?;
546 let root = composer.root();
547 let side_effects = composer.take_side_effects();
548 let requested_root_render = composer.take_root_render_request();
549 Ok((
550 root,
551 side_effects,
552 requested_root_render,
553 outcome.compacted,
554 ))
555 })?;
556 self.apply_pass(host, &runtime_handle, &composer, &side_effects)?;
557 if compact_applier {
558 self.applier.compact();
559 self.applier.borrow_dyn().clear_recycled_nodes();
560 }
561 if root.is_some() {
562 self.root = root;
563 }
564 if requested_root_render {
565 self.root_render_requested = true;
566 }
567 self.park_composer(host, composer);
568 side_effects
569 };
570 runtime_handle.drain_ui();
571 for effect in side_effects {
572 effect();
573 }
574 runtime_handle.drain_ui();
575 self.maybe_dump_slot_table("recompose_pass");
576 if self.root_render_requested {
577 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
578 for scope in remaining_scopes {
579 runtime_handle.requeue_invalid_scope(scope);
580 }
581 }
582 break;
583 }
584 host_group_index += 1;
585 }
586 recycle_scope_groups(&runtime_handle, scope_groups);
587 if self.root_render_requested {
588 break;
589 }
590 }
591 self.finalize_runtime_state();
592 Ok(did_recompose)
593 }
594
595 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
598 self.process_invalid_scopes_until_root_request()
599 }
600
601 fn park_composer(&mut self, host: &Rc<SlotsHost>, composer: Composer) {
603 if Rc::ptr_eq(host, &self.slots) {
604 self.parked_composer = composer.park();
605 }
606 }
607
608 fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
609 let ids = self.runtime.take_forgotten_movables();
610 if ids.is_empty() {
611 return Ok(());
612 }
613 let runtime_handle = self.runtime_handle();
614 let _teardown = runtime::enter_state_teardown_scope();
615 let host = self.slots_host();
616 let composer = Composer::new_with_shared_state(
617 Rc::clone(&self.composer_state),
618 Rc::clone(&host),
619 self.applier_host(),
620 runtime_handle.clone(),
621 self.root,
622 );
623 composer.install(|composer| composer.forget_movables(&ids))?;
624 self.apply_commands_and_updates_for_host(&host, &runtime_handle, &composer)
625 }
626}
627
628fn recomposes_content(scopes: &[RecomposeScope]) -> bool {
632 scopes.iter().any(|scope| !scope.is_derivation())
633}
634
635fn recycle_scope_groups(
638 runtime: &RuntimeHandle,
639 scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]>,
640) {
641 for (_, scopes) in scope_groups {
642 runtime.recycle_scopes(scopes);
643 }
644}
645
646impl<A: Applier + 'static> Composition<A> {
647 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
649 let updates = self.runtime.take_updates();
650 let mut applier = self.applier.borrow_dyn();
651 for update in updates {
652 update.apply(&mut *applier)?;
653 }
654 Ok(())
655 }
656}
657
658impl<A: Applier + 'static> Drop for Composition<A> {
659 fn drop(&mut self) {
660 #[cfg(target_os = "android")]
661 self.runtime.restore_thread_state();
662 self.teardown = Some(runtime::enter_state_teardown_scope());
663 }
664}