1use std::rc::Rc;
2
3use web_time::Instant;
4
5use crate::{
6 Applier, ApplierGuard, ApplierHost, CommandQueue, Composer, CompositionPassDebugStats,
7 ConcreteApplierHost, DefaultScheduler, Key, NodeError, NodeId, RecomposeScope, RetentionPolicy,
8 Runtime, RuntimeHandle, ScopeId, SlotDebugSnapshot, SlotTable, SlotTableDebugStats, SlotsHost,
9 collections::map::HashMap, debug_scope_invalidation_sources, debug_scope_label, runtime,
10 scheduler_ref,
11};
12
13pub struct Composition<A: Applier + 'static> {
14 pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
15 pub(crate) slots: Rc<SlotsHost>,
16 pub(crate) applier: Rc<ConcreteApplierHost<A>>,
17 pub(crate) runtime: Runtime,
18 pub(crate) root: Option<NodeId>,
19 pub(crate) root_key: Option<Key>,
20 pub(crate) root_render_requested: bool,
21 pub(crate) last_pass_stats: CompositionPassDebugStats,
22 teardown: Option<runtime::StateTeardownScope>,
23}
24
25pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
41
42impl<A: Applier + 'static> Composition<A> {
43 pub fn new(applier: A) -> Self {
44 Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
45 }
46
47 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
48 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
49 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
50 let applier = Rc::new(ConcreteApplierHost::new(applier));
51 Self {
52 composer_state,
53 slots,
54 applier,
55 runtime,
56 root: None,
57 root_key: None,
58 root_render_requested: false,
59 last_pass_stats: CompositionPassDebugStats::default(),
60 teardown: None,
61 }
62 }
63
64 pub fn root_key(&self) -> Option<Key> {
67 self.root_key
68 }
69
70 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
71 self.composer_state.set_retention_policy(policy);
72 }
73
74 fn slots_host(&self) -> Rc<SlotsHost> {
75 Rc::clone(&self.slots)
76 }
77
78 fn applier_host(&self) -> Rc<dyn ApplierHost> {
79 self.applier.clone()
80 }
81
82 fn reset_last_pass_stats(&mut self) {
83 self.last_pass_stats = CompositionPassDebugStats::default();
84 }
85
86 fn maybe_dump_slot_table(&self, label: &str) {
87 if !crate::env_flag!("COMPOSE_DEBUG_SLOT_TABLE") {
88 return;
89 }
90 eprintln!(
91 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
92 self.debug_slot_snapshot()
93 );
94 }
95
96 pub fn take_root_render_request(&mut self) -> bool {
97 std::mem::take(&mut self.root_render_requested)
98 }
99
100 pub fn request_root_render(&mut self) {
101 self.root_render_requested = true;
102 self.runtime.handle().schedule();
103 }
104
105 fn record_pass_stats(
106 &mut self,
107 commands: &CommandQueue,
108 side_effects: &Vec<Box<dyn FnOnce()>>,
109 ) {
110 self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
111 self.last_pass_stats.commands_cap =
112 self.last_pass_stats.commands_cap.max(commands.capacity());
113 self.last_pass_stats.command_payload_len_bytes = self
114 .last_pass_stats
115 .command_payload_len_bytes
116 .max(commands.payload_len_bytes());
117 self.last_pass_stats.command_payload_cap_bytes = self
118 .last_pass_stats
119 .command_payload_cap_bytes
120 .max(commands.payload_capacity_bytes());
121 self.last_pass_stats.sync_children_len = self
122 .last_pass_stats
123 .sync_children_len
124 .max(commands.sync_children.len());
125 self.last_pass_stats.sync_children_cap = self
126 .last_pass_stats
127 .sync_children_cap
128 .max(commands.sync_children.capacity());
129 self.last_pass_stats.sync_child_ids_len = self
130 .last_pass_stats
131 .sync_child_ids_len
132 .max(commands.sync_child_ids.len());
133 self.last_pass_stats.sync_child_ids_cap = self
134 .last_pass_stats
135 .sync_child_ids_cap
136 .max(commands.sync_child_ids.capacity());
137 self.last_pass_stats.side_effects_len = self
138 .last_pass_stats
139 .side_effects_len
140 .max(side_effects.len());
141 self.last_pass_stats.side_effects_cap = self
142 .last_pass_stats
143 .side_effects_cap
144 .max(side_effects.capacity());
145 }
146
147 fn finalize_runtime_state(&mut self) {
148 let runtime_handle = self.runtime_handle();
149 if !self.runtime.has_updates()
150 && !runtime_handle.has_invalid_scopes()
151 && !runtime_handle.has_frame_callbacks()
152 && !runtime_handle.has_pending_ui()
153 {
154 self.runtime.set_needs_frame(false);
155 }
156 }
157
158 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
159 host.abandon_after_apply_failure();
160 if Rc::ptr_eq(host, &self.slots) {
161 self.root = None;
162 }
163 self.root_render_requested = true;
164 self.finalize_runtime_state();
165 }
166
167 fn apply_commands_and_updates_for_host(
168 &mut self,
169 host: &Rc<SlotsHost>,
170 runtime_handle: &RuntimeHandle,
171 mut commands: CommandQueue,
172 ) -> Result<(), NodeError> {
173 let result = {
174 let mut applier = self.applier.borrow_dyn();
175 let mut result = commands.apply(&mut *applier);
176 if result.is_ok() {
177 for update in runtime_handle.take_updates() {
178 if let Err(err) = update.apply(&mut *applier) {
179 result = Err(err);
180 break;
181 }
182 }
183 }
184 result
185 };
186 crate::composer::recycle_commands(commands);
187 if result.is_err() {
188 self.abandon_host_after_apply_failure(host);
189 }
190 result
191 }
192
193 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
194 self.root_key = Some(key);
195 self.root_render_requested = false;
196 let runtime_handle = self.runtime_handle();
197 runtime_handle.drain_ui();
198 let side_effects = {
199 let _teardown = runtime::enter_state_teardown_scope();
200 let composer = Composer::new_with_shared_state(
201 Rc::clone(&self.composer_state),
202 Rc::clone(&self.slots),
203 self.applier.clone(),
204 runtime_handle.clone(),
205 self.root,
206 );
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 log::error!(
258 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
259 );
260 return Err(NodeError::RecompositionLimitExceeded {
261 operation: "root render replay",
262 limit: ROOT_RENDER_REPLAY_LIMIT,
263 });
264 }
265
266 self.render_root_pass(key, content)?;
267 did_work = true;
268 }
269 }
270
271 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
272 self.reset_last_pass_stats();
273 self.render_root_pass(key, &mut content)?;
274 let _ = self.process_invalid_scopes()?;
275 Ok(())
276 }
277
278 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
281 self.reset_last_pass_stats();
282 self.render_root_pass(key, &mut content)?;
283 let _ = self.reconcile_with_content(key, &mut content)?;
284 Ok(())
285 }
286
287 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
290 self.reconcile_with_content(key, &mut content)
291 }
292
293 pub fn should_render(&self) -> bool {
302 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
303 }
304
305 pub fn should_recompose(&self) -> bool {
321 self.root_render_requested || self.runtime.has_updates()
322 }
323
324 pub fn runtime_handle(&self) -> RuntimeHandle {
325 self.runtime.handle()
326 }
327
328 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
329 self.applier.borrow_typed()
330 }
331
332 pub fn root(&self) -> Option<NodeId> {
333 self.root
334 }
335
336 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
337 self.slots.borrow().debug_dump_groups()
338 }
339
340 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
341 self.slots.borrow().debug_dump_slot_entries()
342 }
343
344 pub fn slot_table_heap_bytes(&self) -> usize {
345 self.slots.borrow().heap_bytes()
346 }
347
348 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
349 self.slots.debug_stats()
350 }
351
352 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
353 self.slots.debug_snapshot()
354 }
355
356 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
357 self.last_pass_stats
358 }
359
360 #[cfg(test)]
361 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
362 let table = self.slots.borrow();
363 table.validate()?;
364 self.composer_state
365 .validate_host_retention(self.slots.as_ref(), &table)
366 }
367
368 fn group_scopes_by_host(
371 &self,
372 scopes: Vec<RecomposeScope>,
373 ) -> Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> {
374 let root_host = self.slots_host();
375 let mut scope_groups: Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> = Vec::new();
376 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
377 let mut previous_host: Option<(_, usize)> = None;
378 for scope in scopes {
379 let identity = scope.slots_host_identity();
380 if let Some((previous_identity, index)) = previous_host
381 && previous_identity == identity
382 {
383 scope_groups[index].1.push(scope);
384 continue;
385 }
386 let host = scope
387 .slots_runtime_state()
388 .and_then(|state| {
389 scope
390 .slots_storage_key()
391 .and_then(|storage_key| state.host_for_storage_key(storage_key))
392 })
393 .or_else(|| {
394 scope.slots_storage_key().and_then(|storage_key| {
395 self.composer_state.host_for_storage_key(storage_key)
396 })
397 })
398 .unwrap_or_else(|| Rc::clone(&root_host));
399 let host_key = host.storage_key();
400 let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
401 scope_groups[index].1.push(scope);
402 index
403 } else {
404 scope_group_index.insert(host_key, scope_groups.len());
405 scope_groups.push((host, vec![scope]));
406 scope_groups.len() - 1
407 };
408 previous_host = Some((identity, index));
409 }
410 scope_groups
411 }
412
413 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
414 let runtime_handle = self.runtime_handle();
415 let mut did_recompose = false;
416 let mut loop_count = 0;
417 loop {
418 loop_count += 1;
419 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
420 log::error!(
421 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
422 );
423 return Err(NodeError::RecompositionLimitExceeded {
424 operation: "process_invalid_scopes",
425 limit: ROOT_RENDER_REPLAY_LIMIT,
426 });
427 }
428 runtime_handle.drain_ui();
429 self.dispose_forgotten_movables()?;
430 let Some(scopes) = runtime_handle.take_invalidated_scopes() else {
431 break;
432 };
433 if scopes.is_empty() {
434 continue;
435 }
436 did_recompose |= recomposes_content(&scopes);
437 let runtime_clone = runtime_handle.clone();
438 let scope_groups = self.group_scopes_by_host(scopes);
439 let mut host_group_index = 0usize;
440 while host_group_index < scope_groups.len() {
441 let (host, scopes) = &scope_groups[host_group_index];
442 let scope_telemetry_threshold_ms =
443 crate::env_threshold_ms!("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS");
444 let shared_state = host
445 .runtime_state()
446 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
447 .unwrap_or_else(|| Rc::clone(&self.composer_state));
448 let side_effects = {
449 let _teardown = runtime::enter_state_teardown_scope();
450 let composer = Composer::new_with_shared_state(
451 shared_state,
452 Rc::clone(host),
453 self.applier_host(),
454 runtime_clone.clone(),
455 self.root,
456 );
457 composer.parent_stack().clear();
458 let (root, commands, side_effects, requested_root_render, compact_applier) =
459 composer.install(|composer| {
460 let ((), outcome) = composer.try_with_slot_host_pass(
461 Rc::clone(host),
462 crate::slot::SlotPassMode::Recompose,
463 |composer| {
464 for scope in scopes {
465 if let Some(threshold_ms) = scope_telemetry_threshold_ms {
466 let start = Instant::now();
467 composer.recompose_group(scope);
468 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
469 if elapsed_ms >= threshold_ms {
470 eprintln!(
471 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
472 scope.id(),
473 debug_scope_label(scope.id()),
474 debug_scope_invalidation_sources(scope.id())
475 );
476 }
477 } else {
478 composer.recompose_group(scope);
479 }
480 }
481 },
482 )?;
483 let root = composer.root();
484 let commands = composer.take_commands();
485 let side_effects = composer.take_side_effects();
486 let requested_root_render = composer.take_root_render_request();
487 Ok((
488 root,
489 commands,
490 side_effects,
491 requested_root_render,
492 outcome.compacted,
493 ))
494 })?;
495 self.record_pass_stats(&commands, &side_effects);
496 self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
497 if compact_applier {
498 self.applier.compact();
499 self.applier.borrow_dyn().clear_recycled_nodes();
500 }
501 if root.is_some() {
502 self.root = root;
503 }
504 if requested_root_render {
505 self.root_render_requested = true;
506 }
507 side_effects
508 };
509 runtime_handle.drain_ui();
510 for effect in side_effects {
511 effect();
512 }
513 runtime_handle.drain_ui();
514 self.maybe_dump_slot_table("recompose_pass");
515 if self.root_render_requested {
516 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
517 for scope in remaining_scopes {
518 runtime_handle.requeue_invalid_scope(scope);
519 }
520 }
521 break;
522 }
523 host_group_index += 1;
524 }
525 if self.root_render_requested {
526 break;
527 }
528 }
529 self.finalize_runtime_state();
530 Ok(did_recompose)
531 }
532
533 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
534 self.process_invalid_scopes_until_root_request()
535 }
536
537 fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
538 let runtime_handle = self.runtime_handle();
539 let ids = runtime_handle.take_forgotten_movables();
540 if ids.is_empty() {
541 return Ok(());
542 }
543 let host = self.slots_host();
544 let composer = Composer::new_with_shared_state(
545 Rc::clone(&self.composer_state),
546 Rc::clone(&host),
547 self.applier_host(),
548 runtime_handle.clone(),
549 self.root,
550 );
551 let commands = composer.install(|composer| {
552 composer.forget_movables(&ids)?;
553 Ok::<_, NodeError>(composer.take_commands())
554 })?;
555 self.apply_commands_and_updates_for_host(&host, &runtime_handle, commands)
556 }
557}
558
559fn recomposes_content(scopes: &[RecomposeScope]) -> bool {
563 scopes.iter().any(|scope| !scope.is_derivation())
564}
565
566impl<A: Applier + 'static> Composition<A> {
567 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
568 let updates = self.runtime_handle().take_updates();
569 let mut applier = self.applier.borrow_dyn();
570 for update in updates {
571 update.apply(&mut *applier)?;
572 }
573 Ok(())
574 }
575}
576
577impl<A: Applier + 'static> Drop for Composition<A> {
578 fn drop(&mut self) {
579 self.teardown = Some(runtime::enter_state_teardown_scope());
580 }
581}