1use crate::{
2 collections::map::HashMap, debug_scope_invalidation_sources, debug_scope_label, runtime,
3 scheduler_ref, snapshot_state_observer, Applier, ApplierGuard, ApplierHost, CommandQueue,
4 Composer, CompositionPassDebugStats, ConcreteApplierHost, DefaultScheduler, Key, NodeError,
5 NodeId, RecomposeScope, RetentionPolicy, Runtime, RuntimeHandle, ScopeId, SlotDebugSnapshot,
6 SlotTable, SlotTableDebugStats, SlotsHost, SnapshotStateObserver,
7};
8use std::rc::Rc;
9use web_time::Instant;
10
11pub struct Composition<A: Applier + 'static> {
12 pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
13 pub(crate) slots: Rc<SlotsHost>,
14 pub(crate) applier: Rc<ConcreteApplierHost<A>>,
15 pub(crate) runtime: Runtime,
16 pub(crate) observer: SnapshotStateObserver,
17 pub(crate) root: Option<NodeId>,
18 pub(crate) root_key: Option<Key>,
19 pub(crate) root_render_requested: bool,
20 pub(crate) last_pass_stats: CompositionPassDebugStats,
21}
22
23pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
39
40fn recompose_scope_telemetry_threshold_ms() -> Option<f64> {
41 std::env::var("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS")
42 .ok()
43 .and_then(|value| value.parse::<f64>().ok())
44 .filter(|value| value.is_finite() && *value >= 0.0)
45}
46
47impl<A: Applier + 'static> Composition<A> {
48 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 {
53 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
54 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
55 let applier = Rc::new(ConcreteApplierHost::new(applier));
56 let observer_handle = runtime.handle();
57 let observer = SnapshotStateObserver::new(move |callback| {
58 observer_handle.enqueue_ui_task(callback);
59 });
60 observer.start();
61 Self {
62 composer_state,
63 slots,
64 applier,
65 runtime,
66 observer,
67 root: None,
68 root_key: None,
69 root_render_requested: false,
70 last_pass_stats: CompositionPassDebugStats::default(),
71 }
72 }
73
74 pub fn root_key(&self) -> Option<Key> {
77 self.root_key
78 }
79
80 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
81 self.composer_state.set_retention_policy(policy);
82 }
83
84 fn slots_host(&self) -> Rc<SlotsHost> {
85 Rc::clone(&self.slots)
86 }
87
88 fn applier_host(&self) -> Rc<dyn ApplierHost> {
89 self.applier.clone()
90 }
91
92 fn reset_last_pass_stats(&mut self) {
93 self.last_pass_stats = CompositionPassDebugStats::default();
94 }
95
96 fn maybe_dump_slot_table(&self, label: &str) {
97 if !crate::env_flag!("COMPOSE_DEBUG_SLOT_TABLE") {
98 return;
99 }
100 eprintln!(
101 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
102 self.debug_slot_snapshot()
103 );
104 }
105
106 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) {
111 self.root_render_requested = true;
112 self.runtime.handle().schedule();
113 }
114
115 fn record_pass_stats(
116 &mut self,
117 commands: &CommandQueue,
118 side_effects: &Vec<Box<dyn FnOnce()>>,
119 ) {
120 self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
121 self.last_pass_stats.commands_cap =
122 self.last_pass_stats.commands_cap.max(commands.capacity());
123 self.last_pass_stats.command_payload_len_bytes = self
124 .last_pass_stats
125 .command_payload_len_bytes
126 .max(commands.payload_len_bytes());
127 self.last_pass_stats.command_payload_cap_bytes = self
128 .last_pass_stats
129 .command_payload_cap_bytes
130 .max(commands.payload_capacity_bytes());
131 self.last_pass_stats.sync_children_len = self
132 .last_pass_stats
133 .sync_children_len
134 .max(commands.sync_children.len());
135 self.last_pass_stats.sync_children_cap = self
136 .last_pass_stats
137 .sync_children_cap
138 .max(commands.sync_children.capacity());
139 self.last_pass_stats.sync_child_ids_len = self
140 .last_pass_stats
141 .sync_child_ids_len
142 .max(commands.sync_child_ids.len());
143 self.last_pass_stats.sync_child_ids_cap = self
144 .last_pass_stats
145 .sync_child_ids_cap
146 .max(commands.sync_child_ids.capacity());
147 self.last_pass_stats.side_effects_len = self
148 .last_pass_stats
149 .side_effects_len
150 .max(side_effects.len());
151 self.last_pass_stats.side_effects_cap = self
152 .last_pass_stats
153 .side_effects_cap
154 .max(side_effects.capacity());
155 }
156
157 fn finalize_runtime_state(&mut self) {
158 let runtime_handle = self.runtime_handle();
159 self.observer.prune_dead_scopes();
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 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 if result.is_err() {
198 self.abandon_host_after_apply_failure(host);
199 }
200 result
201 }
202
203 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
204 self.root_key = Some(key);
205 self.root_render_requested = false;
206 let runtime_handle = self.runtime_handle();
207 runtime_handle.drain_ui();
208 let side_effects = {
209 let _teardown = runtime::enter_state_teardown_scope();
210 let composer = Composer::new_with_shared_state(
211 Rc::clone(&self.composer_state),
212 Rc::clone(&self.slots),
213 self.applier.clone(),
214 runtime_handle.clone(),
215 self.observer.clone(),
216 self.root,
217 );
218 self.observer.begin_frame();
219 let (root, commands, side_effects, compact_applier) = composer.install(|composer| {
220 let (_, outcome) = composer.try_with_slot_host_pass(
221 Rc::clone(&self.slots),
222 crate::slot::SlotPassMode::Compose,
223 |composer| composer.with_group(key, |_| content()),
224 )?;
225 let root = composer.root();
226 let commands = composer.take_commands();
227 let side_effects = composer.take_side_effects();
228 Ok((root, commands, side_effects, outcome.compacted))
229 })?;
230 self.record_pass_stats(&commands, &side_effects);
231 self.apply_commands_and_updates_for_host(
232 &Rc::clone(&self.slots),
233 &runtime_handle,
234 commands,
235 )?;
236 if compact_applier {
237 self.applier.compact();
238 self.applier.borrow_dyn().clear_recycled_nodes();
239 }
240
241 self.root = root;
242 side_effects
243 };
244 runtime_handle.drain_ui();
245 for effect in side_effects {
246 effect();
247 }
248 runtime_handle.drain_ui();
249 self.maybe_dump_slot_table("root_render_pass");
250 Ok(())
251 }
252
253 fn reconcile_with_content(
254 &mut self,
255 key: Key,
256 content: &mut dyn FnMut(),
257 ) -> Result<bool, NodeError> {
258 self.root_key = Some(key);
259 let mut did_work = false;
260 let mut root_render_replays = 0usize;
261 loop {
262 did_work |= self.process_invalid_scopes_until_root_request()?;
263 if !self.take_root_render_request() {
264 return Ok(did_work);
265 }
266
267 root_render_replays += 1;
268 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
269 log::error!(
270 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
271 );
272 return Err(NodeError::RecompositionLimitExceeded {
273 operation: "root render replay",
274 limit: ROOT_RENDER_REPLAY_LIMIT,
275 });
276 }
277
278 self.render_root_pass(key, content)?;
279 did_work = true;
280 }
281 }
282
283 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
284 self.reset_last_pass_stats();
285 self.render_root_pass(key, &mut content)?;
286 let _ = self.process_invalid_scopes()?;
287 Ok(())
288 }
289
290 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
293 self.reset_last_pass_stats();
294 self.render_root_pass(key, &mut content)?;
295 let _ = self.reconcile_with_content(key, &mut content)?;
296 Ok(())
297 }
298
299 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
302 self.reconcile_with_content(key, &mut content)
303 }
304
305 pub fn should_render(&self) -> bool {
314 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
315 }
316
317 pub fn should_recompose(&self) -> bool {
333 self.root_render_requested || self.runtime.has_updates()
334 }
335
336 pub fn runtime_handle(&self) -> RuntimeHandle {
337 self.runtime.handle()
338 }
339
340 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
341 ApplierGuard::new(self.applier.borrow_typed())
342 }
343
344 pub fn root(&self) -> Option<NodeId> {
345 self.root
346 }
347
348 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
349 self.slots.borrow().debug_dump_groups()
350 }
351
352 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
353 self.slots.borrow().debug_dump_slot_entries()
354 }
355
356 pub fn slot_table_heap_bytes(&self) -> usize {
357 self.slots.borrow().heap_bytes()
358 }
359
360 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
361 self.slots.debug_stats()
362 }
363
364 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
365 self.slots.debug_snapshot()
366 }
367
368 pub fn debug_observer_stats(&self) -> snapshot_state_observer::SnapshotStateObserverDebugStats {
369 self.observer.debug_stats()
370 }
371
372 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
373 self.last_pass_stats
374 }
375
376 #[cfg(test)]
377 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
378 let table = self.slots.borrow();
379 table.validate()?;
380 self.composer_state
381 .validate_host_retention(self.slots.as_ref(), &table)
382 }
383
384 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
385 let runtime_handle = self.runtime_handle();
386 let mut did_recompose = false;
387 let mut loop_count = 0;
388 loop {
389 loop_count += 1;
390 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
391 log::error!(
392 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
393 );
394 return Err(NodeError::RecompositionLimitExceeded {
395 operation: "process_invalid_scopes",
396 limit: ROOT_RENDER_REPLAY_LIMIT,
397 });
398 }
399 runtime_handle.drain_ui();
400 let pending = runtime_handle.take_invalidated_scopes();
401 if pending.is_empty() {
402 break;
403 }
404 let mut scopes = Vec::new();
405 for (id, weak) in pending {
406 if let Some(inner) = weak.upgrade() {
407 scopes.push(RecomposeScope { inner });
408 } else {
409 runtime_handle.mark_scope_recomposed(id);
410 }
411 }
412 if scopes.is_empty() {
413 continue;
414 }
415 did_recompose = true;
416 let runtime_clone = runtime_handle.clone();
417 let root_host = self.slots_host();
418 let mut scope_groups: Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> = Vec::new();
419 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
420 for scope in scopes {
421 let host = scope
422 .slots_runtime_state()
423 .and_then(|state| {
424 scope
425 .slots_storage_key()
426 .and_then(|storage_key| state.host_for_storage_key(storage_key))
427 })
428 .or_else(|| {
429 scope.slots_storage_key().and_then(|storage_key| {
430 self.composer_state.host_for_storage_key(storage_key)
431 })
432 })
433 .unwrap_or_else(|| Rc::clone(&root_host));
434 let host_key = host.storage_key();
435 if let Some(index) = scope_group_index.get(&host_key).copied() {
436 scope_groups[index].1.push(scope);
437 } else {
438 scope_group_index.insert(host_key, scope_groups.len());
439 scope_groups.push((host, vec![scope]));
440 }
441 }
442 let mut host_group_index = 0usize;
443 while host_group_index < scope_groups.len() {
444 let (host, scopes) = &scope_groups[host_group_index];
445 let scope_telemetry_threshold_ms = recompose_scope_telemetry_threshold_ms();
446 let shared_state = host
447 .runtime_state()
448 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
449 .unwrap_or_else(|| Rc::clone(&self.composer_state));
450 let side_effects = {
451 let _teardown = runtime::enter_state_teardown_scope();
452 let composer = Composer::new_with_shared_state(
453 shared_state,
454 Rc::clone(host),
455 self.applier_host(),
456 runtime_clone.clone(),
457 self.observer.clone(),
458 self.root,
459 );
460 composer.parent_stack().clear();
463 self.observer.begin_frame();
464 let (root, commands, side_effects, requested_root_render, compact_applier) =
465 composer.install(|composer| {
466 let (_, outcome) = composer.try_with_slot_host_pass(
467 Rc::clone(host),
468 crate::slot::SlotPassMode::Recompose,
469 |composer| {
470 for scope in scopes {
471 if let Some(threshold_ms) = scope_telemetry_threshold_ms {
472 let start = Instant::now();
473 composer.recompose_group(scope);
474 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
475 if elapsed_ms >= threshold_ms {
476 eprintln!(
477 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
478 scope.id(),
479 debug_scope_label(scope.id()),
480 debug_scope_invalidation_sources(scope.id())
481 );
482 }
483 } else {
484 composer.recompose_group(scope);
485 }
486 }
487 },
488 )?;
489 let root = composer.root();
490 let commands = composer.take_commands();
491 let side_effects = composer.take_side_effects();
492 let requested_root_render = composer.take_root_render_request();
493 Ok((
494 root,
495 commands,
496 side_effects,
497 requested_root_render,
498 outcome.compacted,
499 ))
500 })?;
501 self.record_pass_stats(&commands, &side_effects);
502 self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
503 if compact_applier {
504 self.applier.compact();
505 self.applier.borrow_dyn().clear_recycled_nodes();
506 }
507 if root.is_some() {
508 self.root = root;
509 }
510 if requested_root_render {
511 self.root_render_requested = true;
512 }
513 side_effects
514 };
515 runtime_handle.drain_ui();
516 for effect in side_effects {
517 effect();
518 }
519 runtime_handle.drain_ui();
520 self.maybe_dump_slot_table("recompose_pass");
521 if self.root_render_requested {
522 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
523 for scope in remaining_scopes {
524 runtime_handle.requeue_invalid_scope(scope.id(), scope.downgrade());
525 }
526 }
527 break;
528 }
529 host_group_index += 1;
530 }
531 if self.root_render_requested {
532 break;
533 }
534 }
535 self.finalize_runtime_state();
536 Ok(did_recompose)
537 }
538
539 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
540 self.process_invalid_scopes_until_root_request()
541 }
542
543 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
544 let updates = self.runtime_handle().take_updates();
545 let mut applier = self.applier.borrow_dyn();
546 for update in updates {
547 update.apply(&mut *applier)?;
548 }
549 Ok(())
550 }
551}
552
553impl<A: Applier + 'static> Drop for Composition<A> {
554 fn drop(&mut self) {
555 self.observer.stop();
556 }
557}