1use crate::{
2 collections::map::HashMap, runtime, snapshot_state_observer, Applier, ApplierGuard,
3 ApplierHost, CommandQueue, Composer, CompositionPassDebugStats, ConcreteApplierHost,
4 DefaultScheduler, Key, NodeError, NodeId, RecomposeScope, RetentionPolicy, Runtime,
5 RuntimeHandle, ScopeId, SlotDebugSnapshot, SlotTable, SlotTableDebugStats, SlotsHost,
6 SnapshotStateObserver,
7};
8use std::rc::Rc;
9use std::sync::Arc;
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
40impl<A: Applier + 'static> Composition<A> {
41 pub fn new(applier: A) -> Self {
42 Self::with_runtime(applier, Runtime::new(Arc::new(DefaultScheduler)))
43 }
44
45 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
46 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
47 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
48 let applier = Rc::new(ConcreteApplierHost::new(applier));
49 let observer_handle = runtime.handle();
50 let observer = SnapshotStateObserver::new(move |callback| {
51 observer_handle.enqueue_ui_task(callback);
52 });
53 observer.start();
54 Self {
55 composer_state,
56 slots,
57 applier,
58 runtime,
59 observer,
60 root: None,
61 root_key: None,
62 root_render_requested: false,
63 last_pass_stats: CompositionPassDebugStats::default(),
64 }
65 }
66
67 pub fn root_key(&self) -> Option<Key> {
70 self.root_key
71 }
72
73 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
74 self.composer_state.set_retention_policy(policy);
75 }
76
77 fn slots_host(&self) -> Rc<SlotsHost> {
78 Rc::clone(&self.slots)
79 }
80
81 fn applier_host(&self) -> Rc<dyn ApplierHost> {
82 self.applier.clone()
83 }
84
85 fn reset_last_pass_stats(&mut self) {
86 self.last_pass_stats = CompositionPassDebugStats::default();
87 }
88
89 fn maybe_dump_slot_table(&self, label: &str) {
90 if std::env::var_os("COMPOSE_DEBUG_SLOT_TABLE").is_none() {
91 return;
92 }
93 eprintln!(
94 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
95 self.debug_slot_snapshot()
96 );
97 }
98
99 pub fn take_root_render_request(&mut self) -> bool {
100 std::mem::take(&mut self.root_render_requested)
101 }
102
103 fn record_pass_stats(
104 &mut self,
105 commands: &CommandQueue,
106 side_effects: &Vec<Box<dyn FnOnce()>>,
107 ) {
108 self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
109 self.last_pass_stats.commands_cap =
110 self.last_pass_stats.commands_cap.max(commands.capacity());
111 self.last_pass_stats.command_payload_len_bytes = self
112 .last_pass_stats
113 .command_payload_len_bytes
114 .max(commands.payload_len_bytes());
115 self.last_pass_stats.command_payload_cap_bytes = self
116 .last_pass_stats
117 .command_payload_cap_bytes
118 .max(commands.payload_capacity_bytes());
119 self.last_pass_stats.sync_children_len = self
120 .last_pass_stats
121 .sync_children_len
122 .max(commands.sync_children.len());
123 self.last_pass_stats.sync_children_cap = self
124 .last_pass_stats
125 .sync_children_cap
126 .max(commands.sync_children.capacity());
127 self.last_pass_stats.sync_child_ids_len = self
128 .last_pass_stats
129 .sync_child_ids_len
130 .max(commands.sync_child_ids.len());
131 self.last_pass_stats.sync_child_ids_cap = self
132 .last_pass_stats
133 .sync_child_ids_cap
134 .max(commands.sync_child_ids.capacity());
135 self.last_pass_stats.side_effects_len = self
136 .last_pass_stats
137 .side_effects_len
138 .max(side_effects.len());
139 self.last_pass_stats.side_effects_cap = self
140 .last_pass_stats
141 .side_effects_cap
142 .max(side_effects.capacity());
143 }
144
145 fn finalize_runtime_state(&mut self) {
146 let runtime_handle = self.runtime_handle();
147 self.observer.prune_dead_scopes();
148 if !self.runtime.has_updates()
149 && !runtime_handle.has_invalid_scopes()
150 && !runtime_handle.has_frame_callbacks()
151 && !runtime_handle.has_pending_ui()
152 {
153 self.runtime.set_needs_frame(false);
154 }
155 }
156
157 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
158 host.abandon_after_apply_failure();
159 if Rc::ptr_eq(host, &self.slots) {
160 self.root = None;
161 }
162 self.root_render_requested = true;
163 self.finalize_runtime_state();
164 }
165
166 fn apply_commands_and_updates_for_host(
167 &mut self,
168 host: &Rc<SlotsHost>,
169 runtime_handle: &RuntimeHandle,
170 commands: CommandQueue,
171 ) -> Result<(), NodeError> {
172 let result = {
173 let mut applier = self.applier.borrow_dyn();
174 let mut result = commands.apply(&mut *applier);
175 if result.is_ok() {
176 for update in runtime_handle.take_updates() {
177 if let Err(err) = update.apply(&mut *applier) {
178 result = Err(err);
179 break;
180 }
181 }
182 }
183 result
184 };
185 if result.is_err() {
186 self.abandon_host_after_apply_failure(host);
187 }
188 result
189 }
190
191 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
192 self.root_key = Some(key);
193 self.root_render_requested = false;
194 let runtime_handle = self.runtime_handle();
195 runtime_handle.drain_ui();
196 let side_effects = {
197 let _teardown = runtime::enter_state_teardown_scope();
198 let composer = Composer::new_with_shared_state(
199 Rc::clone(&self.composer_state),
200 Rc::clone(&self.slots),
201 self.applier.clone(),
202 runtime_handle.clone(),
203 self.observer.clone(),
204 self.root,
205 );
206 self.observer.begin_frame();
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 debug_assert!(
258 false,
259 "root render replay exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant render bug"
260 );
261 log::error!(
262 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
263 );
264 return Ok(true);
265 }
266
267 self.render_root_pass(key, content)?;
268 did_work = true;
269 }
270 }
271
272 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
273 self.reset_last_pass_stats();
274 self.render_root_pass(key, &mut content)?;
275 let _ = self.process_invalid_scopes()?;
276 Ok(())
277 }
278
279 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
282 self.reset_last_pass_stats();
283 self.render_root_pass(key, &mut content)?;
284 let _ = self.reconcile_with_content(key, &mut content)?;
285 Ok(())
286 }
287
288 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
291 self.reconcile_with_content(key, &mut content)
292 }
293
294 pub fn should_render(&self) -> bool {
303 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
304 }
305
306 pub fn runtime_handle(&self) -> RuntimeHandle {
307 self.runtime.handle()
308 }
309
310 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
311 ApplierGuard::new(self.applier.borrow_typed())
312 }
313
314 pub fn root(&self) -> Option<NodeId> {
315 self.root
316 }
317
318 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
319 self.slots.borrow().debug_dump_groups()
320 }
321
322 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
323 self.slots.borrow().debug_dump_slot_entries()
324 }
325
326 pub fn slot_table_heap_bytes(&self) -> usize {
327 self.slots.borrow().heap_bytes()
328 }
329
330 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
331 self.slots.debug_stats()
332 }
333
334 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
335 self.slots.debug_snapshot()
336 }
337
338 pub fn debug_observer_stats(&self) -> snapshot_state_observer::SnapshotStateObserverDebugStats {
339 self.observer.debug_stats()
340 }
341
342 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
343 self.last_pass_stats
344 }
345
346 #[cfg(test)]
347 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
348 let table = self.slots.borrow();
349 table.validate()?;
350 self.composer_state
351 .validate_host_retention(self.slots.as_ref(), &table)
352 }
353
354 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
355 let runtime_handle = self.runtime_handle();
356 let mut did_recompose = false;
357 let mut loop_count = 0;
358 loop {
359 loop_count += 1;
360 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
361 debug_assert!(
362 false,
363 "process_invalid_scopes exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant recomposition bug (a scope keeps re-invalidating)"
364 );
365 log::error!(
366 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
367 );
368 break;
369 }
370 runtime_handle.drain_ui();
371 let pending = runtime_handle.take_invalidated_scopes();
372 if pending.is_empty() {
373 break;
374 }
375 let mut scopes = Vec::new();
376 for (id, weak) in pending {
377 if let Some(inner) = weak.upgrade() {
378 scopes.push(RecomposeScope { inner });
379 } else {
380 runtime_handle.mark_scope_recomposed(id);
381 }
382 }
383 if scopes.is_empty() {
384 continue;
385 }
386 did_recompose = true;
387 let runtime_clone = runtime_handle.clone();
388 let root_host = self.slots_host();
389 let mut scope_groups: Vec<(Rc<SlotsHost>, Vec<RecomposeScope>)> = Vec::new();
390 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
391 for scope in scopes {
392 let host = scope
393 .slots_runtime_state()
394 .and_then(|state| {
395 scope
396 .slots_storage_key()
397 .and_then(|storage_key| state.host_for_storage_key(storage_key))
398 })
399 .or_else(|| {
400 scope.slots_storage_key().and_then(|storage_key| {
401 self.composer_state.host_for_storage_key(storage_key)
402 })
403 })
404 .unwrap_or_else(|| Rc::clone(&root_host));
405 let host_key = host.storage_key();
406 if let Some(index) = scope_group_index.get(&host_key).copied() {
407 scope_groups[index].1.push(scope);
408 } else {
409 scope_group_index.insert(host_key, scope_groups.len());
410 scope_groups.push((host, vec![scope]));
411 }
412 }
413 let mut host_group_index = 0usize;
414 while host_group_index < scope_groups.len() {
415 let (host, scopes) = &scope_groups[host_group_index];
416 let shared_state = host
417 .runtime_state()
418 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
419 .unwrap_or_else(|| Rc::clone(&self.composer_state));
420 let side_effects = {
421 let _teardown = runtime::enter_state_teardown_scope();
422 let composer = Composer::new_with_shared_state(
423 shared_state,
424 Rc::clone(host),
425 self.applier_host(),
426 runtime_clone.clone(),
427 self.observer.clone(),
428 self.root,
429 );
430 self.observer.begin_frame();
431 let (root, commands, side_effects, requested_root_render, compact_applier) =
432 composer.install(|composer| {
433 let (_, outcome) = composer.try_with_slot_host_pass(
434 Rc::clone(host),
435 crate::slot::SlotPassMode::Recompose,
436 |composer| {
437 for scope in scopes {
438 composer.recranpose_group(scope);
439 }
440 },
441 )?;
442 let root = composer.root();
443 let commands = composer.take_commands();
444 let side_effects = composer.take_side_effects();
445 let requested_root_render = composer.take_root_render_request();
446 Ok((
447 root,
448 commands,
449 side_effects,
450 requested_root_render,
451 outcome.compacted,
452 ))
453 })?;
454 self.record_pass_stats(&commands, &side_effects);
455 self.apply_commands_and_updates_for_host(host, &runtime_handle, commands)?;
456 if compact_applier {
457 self.applier.compact();
458 self.applier.borrow_dyn().clear_recycled_nodes();
459 }
460 if root.is_some() {
461 self.root = root;
462 }
463 if requested_root_render {
464 self.root_render_requested = true;
465 }
466 side_effects
467 };
468 runtime_handle.drain_ui();
469 for effect in side_effects {
470 effect();
471 }
472 runtime_handle.drain_ui();
473 self.maybe_dump_slot_table("recompose_pass");
474 if self.root_render_requested {
475 for (_, remaining_scopes) in scope_groups.iter().skip(host_group_index + 1) {
476 for scope in remaining_scopes {
477 runtime_handle.requeue_invalid_scope(scope.id(), scope.downgrade());
478 }
479 }
480 break;
481 }
482 host_group_index += 1;
483 }
484 if self.root_render_requested {
485 break;
486 }
487 }
488 self.finalize_runtime_state();
489 Ok(did_recompose)
490 }
491
492 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
493 self.process_invalid_scopes_until_root_request()
494 }
495
496 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
497 let updates = self.runtime_handle().take_updates();
498 let mut applier = self.applier.borrow_dyn();
499 for update in updates {
500 update.apply(&mut *applier)?;
501 }
502 Ok(())
503 }
504}
505
506impl<A: Applier + 'static> Drop for Composition<A> {
507 fn drop(&mut self) {
508 self.observer.stop();
509 }
510}