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 pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
18 pub(crate) slots: Rc<SlotsHost>,
19 pub(crate) applier: Rc<ConcreteApplierHost<A>>,
20 pub(crate) runtime: Runtime,
21 pub(crate) root: Option<NodeId>,
22 pub(crate) root_key: Option<Key>,
23 pub(crate) root_render_requested: bool,
24 pub(crate) last_pass_stats: CompositionPassDebugStats,
25 teardown: Option<runtime::StateTeardownScope>,
26}
27
28pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
44
45impl<A: Applier + 'static> Composition<A> {
46 pub fn new(applier: A) -> Self {
49 Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
50 }
51
52 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
55 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
56 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
57 let applier = Rc::new(ConcreteApplierHost::new(applier));
58 Self {
59 composer_state,
60 slots,
61 applier,
62 runtime,
63 root: None,
64 root_key: None,
65 root_render_requested: false,
66 last_pass_stats: CompositionPassDebugStats::default(),
67 teardown: None,
68 }
69 }
70
71 pub fn root_key(&self) -> Option<Key> {
74 self.root_key
75 }
76
77 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
80 self.composer_state.set_retention_policy(policy);
81 }
82
83 fn slots_host(&self) -> Rc<SlotsHost> {
84 Rc::clone(&self.slots)
85 }
86
87 fn applier_host(&self) -> Rc<dyn ApplierHost> {
88 self.applier.clone()
89 }
90
91 fn reset_last_pass_stats(&mut self) {
92 self.last_pass_stats = CompositionPassDebugStats::default();
93 }
94
95 fn maybe_dump_slot_table(&self, label: &str) {
96 if !crate::env_flag!("COMPOSE_DEBUG_SLOT_TABLE") {
97 return;
98 }
99 eprintln!(
100 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
101 self.debug_slot_snapshot()
102 );
103 }
104
105 pub fn take_root_render_request(&mut self) -> bool {
107 std::mem::take(&mut self.root_render_requested)
108 }
109
110 pub fn request_root_render(&mut self) {
112 self.root_render_requested = true;
113 self.runtime.handle().schedule();
114 }
115
116 fn record_pass_stats(
117 &mut self,
118 commands: &CommandQueue,
119 side_effects: &Vec<Box<dyn FnOnce()>>,
120 ) {
121 self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
122 self.last_pass_stats.commands_cap =
123 self.last_pass_stats.commands_cap.max(commands.capacity());
124 self.last_pass_stats.command_payload_len_bytes = self
125 .last_pass_stats
126 .command_payload_len_bytes
127 .max(commands.payload_len_bytes());
128 self.last_pass_stats.command_payload_cap_bytes = self
129 .last_pass_stats
130 .command_payload_cap_bytes
131 .max(commands.payload_capacity_bytes());
132 self.last_pass_stats.sync_children_len = self
133 .last_pass_stats
134 .sync_children_len
135 .max(commands.sync_children.len());
136 self.last_pass_stats.sync_children_cap = self
137 .last_pass_stats
138 .sync_children_cap
139 .max(commands.sync_children.capacity());
140 self.last_pass_stats.sync_child_ids_len = self
141 .last_pass_stats
142 .sync_child_ids_len
143 .max(commands.sync_child_ids.len());
144 self.last_pass_stats.sync_child_ids_cap = self
145 .last_pass_stats
146 .sync_child_ids_cap
147 .max(commands.sync_child_ids.capacity());
148 self.last_pass_stats.side_effects_len = self
149 .last_pass_stats
150 .side_effects_len
151 .max(side_effects.len());
152 self.last_pass_stats.side_effects_cap = self
153 .last_pass_stats
154 .side_effects_cap
155 .max(side_effects.capacity());
156 }
157
158 fn finalize_runtime_state(&mut self) {
159 let runtime_handle = self.runtime_handle();
160 if !self.runtime.has_updates()
161 && !runtime_handle.has_invalid_scopes()
162 && !runtime_handle.has_frame_callbacks()
163 && !runtime_handle.has_pending_ui()
164 {
165 self.runtime.set_needs_frame(false);
166 }
167 }
168
169 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
170 host.abandon_after_apply_failure();
171 if Rc::ptr_eq(host, &self.slots) {
172 self.root = None;
173 }
174 self.root_render_requested = true;
175 self.finalize_runtime_state();
176 }
177
178 fn apply_commands_and_updates_for_host(
179 &mut self,
180 host: &Rc<SlotsHost>,
181 runtime_handle: &RuntimeHandle,
182 mut commands: CommandQueue,
183 ) -> Result<(), NodeError> {
184 let result = {
185 let mut applier = self.applier.borrow_dyn();
186 let mut result = commands.apply(&mut *applier);
187 if result.is_ok() {
188 for update in runtime_handle.take_updates() {
189 if let Err(err) = update.apply(&mut *applier) {
190 result = Err(err);
191 break;
192 }
193 }
194 }
195 result
196 };
197 crate::composer::recycle_commands(commands);
198 if result.is_err() {
199 self.abandon_host_after_apply_failure(host);
200 }
201 result
202 }
203
204 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
205 self.root_key = Some(key);
206 self.root_render_requested = false;
207 let runtime_handle = self.runtime_handle();
208 runtime_handle.drain_ui();
209 let side_effects = {
210 let _teardown = runtime::enter_state_teardown_scope();
211 let composer = Composer::new_with_shared_state(
212 Rc::clone(&self.composer_state),
213 Rc::clone(&self.slots),
214 self.applier.clone(),
215 runtime_handle.clone(),
216 self.root,
217 );
218 let (root, commands, side_effects, compact_applier) = composer.install(|composer| {
219 let ((), outcome) = composer.try_with_slot_host_pass(
220 Rc::clone(&self.slots),
221 crate::slot::SlotPassMode::Compose,
222 |composer| composer.with_group(key, |_| content()),
223 )?;
224 let root = composer.root();
225 let commands = composer.take_commands();
226 let side_effects = composer.take_side_effects();
227 Ok((root, commands, side_effects, outcome.compacted))
228 })?;
229 self.record_pass_stats(&commands, &side_effects);
230 self.apply_commands_and_updates_for_host(
231 &Rc::clone(&self.slots),
232 &runtime_handle,
233 commands,
234 )?;
235 if compact_applier {
236 self.applier.compact();
237 self.applier.borrow_dyn().clear_recycled_nodes();
238 }
239
240 self.root = root;
241 side_effects
242 };
243 runtime_handle.drain_ui();
244 for effect in side_effects {
245 effect();
246 }
247 runtime_handle.drain_ui();
248 self.maybe_dump_slot_table("root_render_pass");
249 Ok(())
250 }
251
252 fn reconcile_with_content(
253 &mut self,
254 key: Key,
255 content: &mut dyn FnMut(),
256 ) -> Result<bool, NodeError> {
257 self.root_key = Some(key);
258 let mut did_work = false;
259 let mut root_render_replays = 0usize;
260 loop {
261 did_work |= self.process_invalid_scopes_until_root_request()?;
262 if !self.take_root_render_request() {
263 return Ok(did_work);
264 }
265
266 root_render_replays += 1;
267 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
268 log::error!(
269 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
270 );
271 return Err(NodeError::RecompositionLimitExceeded {
272 operation: "root render replay",
273 limit: ROOT_RENDER_REPLAY_LIMIT,
274 });
275 }
276
277 self.render_root_pass(key, content)?;
278 did_work = true;
279 }
280 }
281
282 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
285 self.reset_last_pass_stats();
286 self.render_root_pass(key, &mut content)?;
287 let _ = self.process_invalid_scopes()?;
288 Ok(())
289 }
290
291 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
294 self.reset_last_pass_stats();
295 self.render_root_pass(key, &mut content)?;
296 let _ = self.reconcile_with_content(key, &mut content)?;
297 Ok(())
298 }
299
300 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
303 self.reconcile_with_content(key, &mut content)
304 }
305
306 pub fn should_render(&self) -> bool {
315 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
316 }
317
318 pub fn should_recompose(&self) -> bool {
334 self.root_render_requested || self.runtime.has_updates()
335 }
336
337 pub fn runtime_handle(&self) -> RuntimeHandle {
339 self.runtime.handle()
340 }
341
342 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
344 self.applier.borrow_typed()
345 }
346
347 pub fn root(&self) -> Option<NodeId> {
349 self.root
350 }
351
352 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
355 self.slots.borrow().debug_dump_groups()
356 }
357
358 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
360 self.slots.borrow().debug_dump_slot_entries()
361 }
362
363 pub fn slot_table_heap_bytes(&self) -> usize {
365 self.slots.borrow().heap_bytes()
366 }
367
368 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
370 self.slots.debug_stats()
371 }
372
373 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
375 self.slots.debug_snapshot()
376 }
377
378 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
380 self.last_pass_stats
381 }
382
383 #[cfg(test)]
384 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
385 let table = self.slots.borrow();
386 table.validate()?;
387 self.composer_state
388 .validate_host_retention(self.slots.as_ref(), &table)
389 }
390
391 fn host_for_scope(&self, scope: &RecomposeScope, root_host: &Rc<SlotsHost>) -> Rc<SlotsHost> {
395 scope
396 .slots_runtime_state()
397 .and_then(|state| {
398 scope
399 .slots_storage_key()
400 .and_then(|storage_key| state.host_for_storage_key(storage_key))
401 })
402 .or_else(|| {
403 scope
404 .slots_storage_key()
405 .and_then(|storage_key| self.composer_state.host_for_storage_key(storage_key))
406 })
407 .unwrap_or_else(|| Rc::clone(root_host))
408 }
409
410 fn group_scopes_by_host(
412 &self,
413 scopes: Vec<RecomposeScope>,
414 ) -> SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> {
415 let root_host = self.slots_host();
416 let first_identity = scopes.first().map(RecomposeScope::slots_host_identity);
419 if let Some(first) = scopes.first()
420 && scopes
421 .iter()
422 .all(|scope| Some(scope.slots_host_identity()) == first_identity)
423 {
424 let host = self.host_for_scope(first, &root_host);
425 return smallvec::smallvec![(host, scopes)];
426 }
427 let mut scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> = SmallVec::new();
428 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
429 let mut previous_host: Option<(_, usize)> = None;
430 for scope in scopes {
431 let identity = scope.slots_host_identity();
432 if let Some((previous_identity, index)) = previous_host
433 && previous_identity == identity
434 {
435 scope_groups[index].1.push(scope);
436 continue;
437 }
438 let host = self.host_for_scope(&scope, &root_host);
439 let host_key = host.storage_key();
440 let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
441 scope_groups[index].1.push(scope);
442 index
443 } else {
444 scope_group_index.insert(host_key, scope_groups.len());
445 scope_groups.push((host, vec![scope]));
446 scope_groups.len() - 1
447 };
448 previous_host = Some((identity, index));
449 }
450 scope_groups
451 }
452
453 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
454 let runtime_handle = self.runtime_handle();
455 let mut did_recompose = false;
456 let mut loop_count = 0;
457 loop {
458 loop_count += 1;
459 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
460 log::error!(
461 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
462 );
463 return Err(NodeError::RecompositionLimitExceeded {
464 operation: "process_invalid_scopes",
465 limit: ROOT_RENDER_REPLAY_LIMIT,
466 });
467 }
468 runtime_handle.drain_ui();
469 self.dispose_forgotten_movables()?;
470 let Some(scopes) = runtime_handle.take_invalidated_scopes() else {
471 break;
472 };
473 if scopes.is_empty() {
474 continue;
475 }
476 did_recompose |= recomposes_content(&scopes);
477 let runtime_clone = runtime_handle.clone();
478 let scope_groups = self.group_scopes_by_host(scopes);
479 let mut host_group_index = 0usize;
480 while host_group_index < scope_groups.len() {
481 let (host, scopes) = &scope_groups[host_group_index];
482 let scope_telemetry_threshold_ms =
483 crate::env_threshold_ms!("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS");
484 let shared_state = host
485 .runtime_state()
486 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
487 .unwrap_or_else(|| Rc::clone(&self.composer_state));
488 let side_effects = {
489 let _teardown = runtime::enter_state_teardown_scope();
490 let composer = Composer::new_with_shared_state(
491 shared_state,
492 Rc::clone(host),
493 self.applier_host(),
494 runtime_clone.clone(),
495 self.root,
496 );
497 composer.parent_stack().clear();
498 let (root, commands, side_effects, requested_root_render, compact_applier) =
499 composer.install(|composer| {
500 let ((), outcome) = composer.try_with_slot_host_pass(
501 Rc::clone(host),
502 crate::slot::SlotPassMode::Recompose,
503 |composer| {
504 for scope in scopes {
505 if let Some(threshold_ms) = scope_telemetry_threshold_ms {
506 let start = Instant::now();
507 composer.recompose_group(scope);
508 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
509 if elapsed_ms >= threshold_ms {
510 eprintln!(
511 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
512 scope.id(),
513 debug_scope_label(scope.id()),
514 debug_scope_invalidation_sources(scope.id())
515 );
516 }
517 } else {
518 composer.recompose_group(scope);
519 }
520 }
521 },
522 )?;
523 let root = composer.root();
524 let commands = composer.take_commands();
525 let side_effects = composer.take_side_effects();
526 let requested_root_render = composer.take_root_render_request();
527 Ok((
528 root,
529 commands,
530 side_effects,
531 requested_root_render,
532 outcome.compacted,
533 ))
534 })?;
535 self.record_pass_stats(&commands, &side_effects);
536 self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
537 if compact_applier {
538 self.applier.compact();
539 self.applier.borrow_dyn().clear_recycled_nodes();
540 }
541 if root.is_some() {
542 self.root = root;
543 }
544 if requested_root_render {
545 self.root_render_requested = true;
546 }
547 side_effects
548 };
549 runtime_handle.drain_ui();
550 for effect in side_effects {
551 effect();
552 }
553 runtime_handle.drain_ui();
554 self.maybe_dump_slot_table("recompose_pass");
555 if self.root_render_requested {
556 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
557 for scope in remaining_scopes {
558 runtime_handle.requeue_invalid_scope(scope);
559 }
560 }
561 break;
562 }
563 host_group_index += 1;
564 }
565 if self.root_render_requested {
566 break;
567 }
568 }
569 self.finalize_runtime_state();
570 Ok(did_recompose)
571 }
572
573 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
576 self.process_invalid_scopes_until_root_request()
577 }
578
579 fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
580 let runtime_handle = self.runtime_handle();
581 let ids = runtime_handle.take_forgotten_movables();
582 if ids.is_empty() {
583 return Ok(());
584 }
585 let _teardown = runtime::enter_state_teardown_scope();
586 let host = self.slots_host();
587 let composer = Composer::new_with_shared_state(
588 Rc::clone(&self.composer_state),
589 Rc::clone(&host),
590 self.applier_host(),
591 runtime_handle.clone(),
592 self.root,
593 );
594 let commands = composer.install(|composer| {
595 composer.forget_movables(&ids)?;
596 Ok::<_, NodeError>(composer.take_commands())
597 })?;
598 self.apply_commands_and_updates_for_host(&host, &runtime_handle, commands)
599 }
600}
601
602fn recomposes_content(scopes: &[RecomposeScope]) -> bool {
606 scopes.iter().any(|scope| !scope.is_derivation())
607}
608
609impl<A: Applier + 'static> Composition<A> {
610 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
612 let updates = self.runtime_handle().take_updates();
613 let mut applier = self.applier.borrow_dyn();
614 for update in updates {
615 update.apply(&mut *applier)?;
616 }
617 Ok(())
618 }
619}
620
621impl<A: Applier + 'static> Drop for Composition<A> {
622 fn drop(&mut self) {
623 #[cfg(target_os = "android")]
624 self.runtime.restore_thread_state();
625 self.teardown = Some(runtime::enter_state_teardown_scope());
626 }
627}