1use crate::{
2 collections::map::HashMap, debug_scope_invalidation_sources, debug_scope_label, runtime,
3 snapshot_state_observer, Applier, ApplierGuard, ApplierHost, CommandQueue, Composer,
4 CompositionPassDebugStats, ConcreteApplierHost, DefaultScheduler, Key, NodeError, NodeId,
5 RecomposeScope, RetentionPolicy, Runtime, RuntimeHandle, ScopeId, SlotDebugSnapshot, SlotTable,
6 SlotTableDebugStats, SlotsHost, SnapshotStateObserver,
7};
8use std::rc::Rc;
9use std::sync::Arc;
10use web_time::Instant;
11
12pub struct Composition<A: Applier + 'static> {
13 pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
14 pub(crate) slots: Rc<SlotsHost>,
15 pub(crate) applier: Rc<ConcreteApplierHost<A>>,
16 pub(crate) runtime: Runtime,
17 pub(crate) observer: SnapshotStateObserver,
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}
23
24pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
40
41fn recompose_scope_telemetry_threshold_ms() -> Option<f64> {
42 std::env::var("CRANPOSE_RECOMPOSE_SCOPE_TELEMETRY_MS")
43 .ok()
44 .and_then(|value| value.parse::<f64>().ok())
45 .filter(|value| value.is_finite() && *value >= 0.0)
46}
47
48impl<A: Applier + 'static> Composition<A> {
49 pub fn new(applier: A) -> Self {
50 Self::with_runtime(applier, Runtime::new(Arc::new(DefaultScheduler)))
51 }
52
53 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
54 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
55 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
56 let applier = Rc::new(ConcreteApplierHost::new(applier));
57 let observer_handle = runtime.handle();
58 let observer = SnapshotStateObserver::new(move |callback| {
59 observer_handle.enqueue_ui_task(callback);
60 });
61 observer.start();
62 Self {
63 composer_state,
64 slots,
65 applier,
66 runtime,
67 observer,
68 root: None,
69 root_key: None,
70 root_render_requested: false,
71 last_pass_stats: CompositionPassDebugStats::default(),
72 }
73 }
74
75 pub fn root_key(&self) -> Option<Key> {
78 self.root_key
79 }
80
81 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
82 self.composer_state.set_retention_policy(policy);
83 }
84
85 fn slots_host(&self) -> Rc<SlotsHost> {
86 Rc::clone(&self.slots)
87 }
88
89 fn applier_host(&self) -> Rc<dyn ApplierHost> {
90 self.applier.clone()
91 }
92
93 fn reset_last_pass_stats(&mut self) {
94 self.last_pass_stats = CompositionPassDebugStats::default();
95 }
96
97 fn maybe_dump_slot_table(&self, label: &str) {
98 if !crate::env_flag!("COMPOSE_DEBUG_SLOT_TABLE") {
99 return;
100 }
101 eprintln!(
102 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
103 self.debug_slot_snapshot()
104 );
105 }
106
107 pub fn take_root_render_request(&mut self) -> bool {
108 std::mem::take(&mut self.root_render_requested)
109 }
110
111 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 self.observer.prune_dead_scopes();
161 if !self.runtime.has_updates()
162 && !runtime_handle.has_invalid_scopes()
163 && !runtime_handle.has_frame_callbacks()
164 && !runtime_handle.has_pending_ui()
165 {
166 self.runtime.set_needs_frame(false);
167 }
168 }
169
170 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
171 host.abandon_after_apply_failure();
172 if Rc::ptr_eq(host, &self.slots) {
173 self.root = None;
174 }
175 self.root_render_requested = true;
176 self.finalize_runtime_state();
177 }
178
179 fn apply_commands_and_updates_for_host(
180 &mut self,
181 host: &Rc<SlotsHost>,
182 runtime_handle: &RuntimeHandle,
183 commands: CommandQueue,
184 ) -> Result<(), NodeError> {
185 let result = {
186 let mut applier = self.applier.borrow_dyn();
187 let mut result = commands.apply(&mut *applier);
188 if result.is_ok() {
189 for update in runtime_handle.take_updates() {
190 if let Err(err) = update.apply(&mut *applier) {
191 result = Err(err);
192 break;
193 }
194 }
195 }
196 result
197 };
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.observer.clone(),
217 self.root,
218 );
219 self.observer.begin_frame();
220 let (root, commands, side_effects, compact_applier) = composer.install(|composer| {
221 let (_, outcome) = composer.try_with_slot_host_pass(
222 Rc::clone(&self.slots),
223 crate::slot::SlotPassMode::Compose,
224 |composer| composer.with_group(key, |_| content()),
225 )?;
226 let root = composer.root();
227 let commands = composer.take_commands();
228 let side_effects = composer.take_side_effects();
229 Ok((root, commands, side_effects, outcome.compacted))
230 })?;
231 self.record_pass_stats(&commands, &side_effects);
232 self.apply_commands_and_updates_for_host(
233 &Rc::clone(&self.slots),
234 &runtime_handle,
235 commands,
236 )?;
237 if compact_applier {
238 self.applier.compact();
239 self.applier.borrow_dyn().clear_recycled_nodes();
240 }
241
242 self.root = root;
243 side_effects
244 };
245 runtime_handle.drain_ui();
246 for effect in side_effects {
247 effect();
248 }
249 runtime_handle.drain_ui();
250 self.maybe_dump_slot_table("root_render_pass");
251 Ok(())
252 }
253
254 fn reconcile_with_content(
255 &mut self,
256 key: Key,
257 content: &mut dyn FnMut(),
258 ) -> Result<bool, NodeError> {
259 self.root_key = Some(key);
260 let mut did_work = false;
261 let mut root_render_replays = 0usize;
262 loop {
263 did_work |= self.process_invalid_scopes_until_root_request()?;
264 if !self.take_root_render_request() {
265 return Ok(did_work);
266 }
267
268 root_render_replays += 1;
269 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
270 log::error!(
271 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
272 );
273 return Err(NodeError::RecompositionLimitExceeded {
274 operation: "root render replay",
275 limit: ROOT_RENDER_REPLAY_LIMIT,
276 });
277 }
278
279 self.render_root_pass(key, content)?;
280 did_work = true;
281 }
282 }
283
284 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 {
338 self.runtime.handle()
339 }
340
341 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
342 ApplierGuard::new(self.applier.borrow_typed())
343 }
344
345 pub fn root(&self) -> Option<NodeId> {
346 self.root
347 }
348
349 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
350 self.slots.borrow().debug_dump_groups()
351 }
352
353 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
354 self.slots.borrow().debug_dump_slot_entries()
355 }
356
357 pub fn slot_table_heap_bytes(&self) -> usize {
358 self.slots.borrow().heap_bytes()
359 }
360
361 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
362 self.slots.debug_stats()
363 }
364
365 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
366 self.slots.debug_snapshot()
367 }
368
369 pub fn debug_observer_stats(&self) -> snapshot_state_observer::SnapshotStateObserverDebugStats {
370 self.observer.debug_stats()
371 }
372
373 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
374 self.last_pass_stats
375 }
376
377 #[cfg(test)]
378 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
379 let table = self.slots.borrow();
380 table.validate()?;
381 self.composer_state
382 .validate_host_retention(self.slots.as_ref(), &table)
383 }
384
385 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
386 let runtime_handle = self.runtime_handle();
387 let mut did_recompose = false;
388 let mut loop_count = 0;
389 loop {
390 loop_count += 1;
391 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
392 log::error!(
393 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
394 );
395 return Err(NodeError::RecompositionLimitExceeded {
396 operation: "process_invalid_scopes",
397 limit: ROOT_RENDER_REPLAY_LIMIT,
398 });
399 }
400 runtime_handle.drain_ui();
401 let pending = runtime_handle.take_invalidated_scopes();
402 if pending.is_empty() {
403 break;
404 }
405 let mut scopes = Vec::new();
406 for (id, weak) in pending {
407 if let Some(inner) = weak.upgrade() {
408 scopes.push(RecomposeScope { inner });
409 } else {
410 runtime_handle.mark_scope_recomposed(id);
411 }
412 }
413 if scopes.is_empty() {
414 continue;
415 }
416 did_recompose = true;
417 let runtime_clone = runtime_handle.clone();
418 let root_host = self.slots_host();
419 let mut scope_groups: Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> = Vec::new();
420 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
421 for scope in scopes {
422 let host = scope
423 .slots_runtime_state()
424 .and_then(|state| {
425 scope
426 .slots_storage_key()
427 .and_then(|storage_key| state.host_for_storage_key(storage_key))
428 })
429 .or_else(|| {
430 scope.slots_storage_key().and_then(|storage_key| {
431 self.composer_state.host_for_storage_key(storage_key)
432 })
433 })
434 .unwrap_or_else(|| Rc::clone(&root_host));
435 let host_key = host.storage_key();
436 if let Some(index) = scope_group_index.get(&host_key).copied() {
437 scope_groups[index].1.push(scope);
438 } else {
439 scope_group_index.insert(host_key, scope_groups.len());
440 scope_groups.push((host, vec![scope]));
441 }
442 }
443 let mut host_group_index = 0usize;
444 while host_group_index < scope_groups.len() {
445 let (host, scopes) = &scope_groups[host_group_index];
446 let scope_telemetry_threshold_ms = recompose_scope_telemetry_threshold_ms();
447 let shared_state = host
448 .runtime_state()
449 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
450 .unwrap_or_else(|| Rc::clone(&self.composer_state));
451 let side_effects = {
452 let _teardown = runtime::enter_state_teardown_scope();
453 let composer = Composer::new_with_shared_state(
454 shared_state,
455 Rc::clone(host),
456 self.applier_host(),
457 runtime_clone.clone(),
458 self.observer.clone(),
459 self.root,
460 );
461 composer.parent_stack().clear();
464 self.observer.begin_frame();
465 let (root, commands, side_effects, requested_root_render, compact_applier) =
466 composer.install(|composer| {
467 let (_, outcome) = composer.try_with_slot_host_pass(
468 Rc::clone(host),
469 crate::slot::SlotPassMode::Recompose,
470 |composer| {
471 for scope in scopes {
472 if let Some(threshold_ms) = scope_telemetry_threshold_ms {
473 let start = Instant::now();
474 composer.recranpose_group(scope);
475 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
476 if elapsed_ms >= threshold_ms {
477 eprintln!(
478 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
479 scope.id(),
480 debug_scope_label(scope.id()),
481 debug_scope_invalidation_sources(scope.id())
482 );
483 }
484 } else {
485 composer.recranpose_group(scope);
486 }
487 }
488 },
489 )?;
490 let root = composer.root();
491 let commands = composer.take_commands();
492 let side_effects = composer.take_side_effects();
493 let requested_root_render = composer.take_root_render_request();
494 Ok((
495 root,
496 commands,
497 side_effects,
498 requested_root_render,
499 outcome.compacted,
500 ))
501 })?;
502 self.record_pass_stats(&commands, &side_effects);
503 self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
504 if compact_applier {
505 self.applier.compact();
506 self.applier.borrow_dyn().clear_recycled_nodes();
507 }
508 if root.is_some() {
509 self.root = root;
510 }
511 if requested_root_render {
512 self.root_render_requested = true;
513 }
514 side_effects
515 };
516 runtime_handle.drain_ui();
517 for effect in side_effects {
518 effect();
519 }
520 runtime_handle.drain_ui();
521 self.maybe_dump_slot_table("recompose_pass");
522 if self.root_render_requested {
523 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
524 for scope in remaining_scopes {
525 runtime_handle.requeue_invalid_scope(scope.id(), scope.downgrade());
526 }
527 }
528 break;
529 }
530 host_group_index += 1;
531 }
532 if self.root_render_requested {
533 break;
534 }
535 }
536 self.finalize_runtime_state();
537 Ok(did_recompose)
538 }
539
540 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
541 self.process_invalid_scopes_until_root_request()
542 }
543
544 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
545 let updates = self.runtime_handle().take_updates();
546 let mut applier = self.applier.borrow_dyn();
547 for update in updates {
548 update.apply(&mut *applier)?;
549 }
550 Ok(())
551 }
552}
553
554impl<A: Applier + 'static> Drop for Composition<A> {
555 fn drop(&mut self) {
556 self.observer.stop();
557 }
558}