1use std::rc::Rc;
2
3use smallvec::SmallVec;
4use web_time::Instant;
5
6use crate::{
7 Applier, ApplierGuard, ApplierHost, CommandQueue, Composer, ConcreteApplierHost, DIAGNOSTICS,
8 DefaultScheduler, Key, NodeError, NodeId, PausableContent, 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 parked_composer: Option<Composer>,
21 pub(crate) composer_state: Rc<crate::composer::ComposerRuntimeState>,
22 pub(crate) slots: Rc<SlotsHost>,
23 pub(crate) applier: Rc<ConcreteApplierHost<A>>,
24 pub(crate) runtime: Runtime,
25 pub(crate) root: Option<NodeId>,
26 pub(crate) root_key: Option<Key>,
27 pub(crate) root_render_requested: bool,
28 pub(crate) last_pass_stats: CompositionPassDebugStats,
29 teardown: Option<runtime::StateTeardownScope>,
30}
31
32#[doc(hidden)]
35pub struct LayoutBudget {
36 state: Rc<crate::composer::ComposerRuntimeState>,
37 runtime: RuntimeHandle,
38}
39
40impl Drop for LayoutBudget {
41 fn drop(&mut self) {
42 self.state.pausing.close_layout(&self.runtime);
43 }
44}
45
46pub const ROOT_RENDER_REPLAY_LIMIT: usize = 100;
62
63impl<A: Applier + 'static> Composition<A> {
64 pub fn new(applier: A) -> Self {
67 Self::with_runtime(applier, Runtime::new(scheduler_ref(DefaultScheduler)))
68 }
69
70 pub fn with_runtime(applier: A, runtime: Runtime) -> Self {
73 let composer_state = Rc::new(crate::composer::ComposerRuntimeState::default());
74 let slots = Rc::new(SlotsHost::new(SlotTable::new()));
75 let applier = Rc::new(ConcreteApplierHost::new(applier));
76 Self {
77 parked_composer: None,
78 composer_state,
79 slots,
80 applier,
81 runtime,
82 root: None,
83 root_key: None,
84 root_render_requested: false,
85 last_pass_stats: CompositionPassDebugStats::default(),
86 teardown: None,
87 }
88 }
89
90 pub fn root_key(&self) -> Option<Key> {
93 self.root_key
94 }
95
96 pub fn set_retention_policy(&self, policy: RetentionPolicy) {
99 self.composer_state.set_retention_policy(policy);
100 }
101
102 fn slots_host(&self) -> Rc<SlotsHost> {
103 Rc::clone(&self.slots)
104 }
105
106 fn applier_host(&self) -> Rc<dyn ApplierHost> {
107 Rc::<ConcreteApplierHost<A>>::clone(&self.applier)
108 }
109
110 fn reset_last_pass_stats(&mut self) {
111 self.last_pass_stats = CompositionPassDebugStats::default();
112 }
113
114 #[expect(clippy::print_stderr, reason = "COMPOSE_DEBUG_SLOT_TABLE asks for it")]
115 fn maybe_dump_slot_table(&self, label: &str) {
116 if !DIAGNOSTICS.compose_debug_slot_table.is_set() {
117 return;
118 }
119 eprintln!(
120 "[COMPOSE_DEBUG_SLOT_TABLE] {label}\n{:#?}",
121 self.debug_slot_snapshot()
122 );
123 }
124
125 pub fn take_root_render_request(&mut self) -> bool {
127 std::mem::take(&mut self.root_render_requested)
128 }
129
130 pub fn request_root_render(&mut self) {
132 self.root_render_requested = true;
133 self.runtime.handle().schedule();
134 }
135
136 fn record_pass_stats(
137 &mut self,
138 commands: &CommandQueue,
139 side_effects: &Vec<Box<dyn FnOnce()>>,
140 ) {
141 self.last_pass_stats.commands_len = self.last_pass_stats.commands_len.max(commands.len());
142 self.last_pass_stats.commands_cap =
143 self.last_pass_stats.commands_cap.max(commands.capacity());
144 self.last_pass_stats.command_payload_len_bytes = self
145 .last_pass_stats
146 .command_payload_len_bytes
147 .max(commands.payload_len_bytes());
148 self.last_pass_stats.command_payload_cap_bytes = self
149 .last_pass_stats
150 .command_payload_cap_bytes
151 .max(commands.payload_capacity_bytes());
152 self.last_pass_stats.sync_children_len = self
153 .last_pass_stats
154 .sync_children_len
155 .max(commands.sync_children.len());
156 self.last_pass_stats.sync_children_cap = self
157 .last_pass_stats
158 .sync_children_cap
159 .max(commands.sync_children.capacity());
160 self.last_pass_stats.sync_child_ids_len = self
161 .last_pass_stats
162 .sync_child_ids_len
163 .max(commands.sync_child_ids.len());
164 self.last_pass_stats.sync_child_ids_cap = self
165 .last_pass_stats
166 .sync_child_ids_cap
167 .max(commands.sync_child_ids.capacity());
168 self.last_pass_stats.side_effects_len = self
169 .last_pass_stats
170 .side_effects_len
171 .max(side_effects.len());
172 self.last_pass_stats.side_effects_cap = self
173 .last_pass_stats
174 .side_effects_cap
175 .max(side_effects.capacity());
176 }
177
178 fn finalize_runtime_state(&mut self) {
179 if !self.runtime.has_updates()
180 && !self.runtime.has_invalid_scopes()
181 && !self.runtime.has_frame_callbacks()
182 && !self.runtime.has_pending_ui()
183 {
184 self.runtime.set_needs_frame(false);
185 }
186 }
187
188 fn abandon_host_after_apply_failure(&mut self, host: &Rc<SlotsHost>) {
189 host.abandon_after_apply_failure();
190 if Rc::ptr_eq(host, &self.slots) {
191 self.root = None;
192 }
193 self.root_render_requested = true;
194 self.finalize_runtime_state();
195 }
196
197 fn apply_pass(
200 &mut self,
201 host: &Rc<SlotsHost>,
202 runtime_handle: &RuntimeHandle,
203 composer: &Composer,
204 side_effects: &Vec<Box<dyn FnOnce()>>,
205 ) -> Result<(), NodeError> {
206 composer.with_commands(|commands| self.record_pass_stats(commands, side_effects));
207 self.apply_commands_and_updates_for_host(host, runtime_handle, composer)
208 }
209
210 fn apply_commands_and_updates_for_host(
213 &mut self,
214 host: &Rc<SlotsHost>,
215 runtime_handle: &RuntimeHandle,
216 composer: &Composer,
217 ) -> Result<(), NodeError> {
218 let result = composer.with_commands(|commands| {
219 let mut applier = self.applier.borrow_dyn();
220 let mut result = commands.apply(&mut *applier);
221 if result.is_ok() {
222 for update in runtime_handle.take_updates() {
223 if let Err(err) = update.apply(&mut *applier) {
224 result = Err(err);
225 break;
226 }
227 }
228 }
229 result
230 });
231 if result.is_err() {
232 self.abandon_host_after_apply_failure(host);
233 }
234 result
235 }
236
237 fn render_root_pass(&mut self, key: Key, content: &mut dyn FnMut()) -> Result<(), NodeError> {
238 self.root_key = Some(key);
239 self.root_render_requested = false;
240 let runtime_handle = self.runtime_handle();
241 runtime_handle.drain_ui();
242 let side_effects = {
243 let _teardown = runtime::enter_state_teardown_scope();
244 let composer = Composer::new_with_shared_state(
245 Rc::clone(&self.composer_state),
246 Rc::clone(&self.slots),
247 Rc::<ConcreteApplierHost<A>>::clone(&self.applier),
248 runtime_handle.clone(),
249 self.root,
250 );
251 let (root, side_effects, compact_applier) = composer.install(|composer| {
252 let ((), outcome) = composer.try_with_slot_host_pass(
253 Rc::clone(&self.slots),
254 crate::slot::SlotPassMode::Compose,
255 |composer| composer.with_group(key, |_| content()),
256 )?;
257 let root = composer.root();
258 let side_effects = composer.take_side_effects();
259 Ok((root, side_effects, outcome.compacted))
260 })?;
261 self.apply_pass(
262 &Rc::clone(&self.slots),
263 &runtime_handle,
264 &composer,
265 &side_effects,
266 )?;
267 if compact_applier {
268 self.applier.compact();
269 self.applier.borrow_dyn().clear_recycled_nodes();
270 }
271
272 self.root = root;
273 side_effects
274 };
275 runtime_handle.drain_ui();
276 for effect in side_effects {
277 effect();
278 }
279 runtime_handle.drain_ui();
280 self.maybe_dump_slot_table("root_render_pass");
281 Ok(())
282 }
283
284 fn reconcile_with_content(
285 &mut self,
286 key: Key,
287 content: &mut dyn FnMut(),
288 ) -> Result<bool, NodeError> {
289 self.root_key = Some(key);
290 let mut did_work = false;
291 let mut root_render_replays = 0usize;
292 loop {
293 did_work |= self.process_invalid_scopes_within_budget()?;
294 if !self.take_root_render_request() {
295 return Ok(did_work);
296 }
297
298 root_render_replays += 1;
299 if root_render_replays > ROOT_RENDER_REPLAY_LIMIT {
300 log::error!(
301 "root render replay looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
302 );
303 return Err(NodeError::RecompositionLimitExceeded {
304 operation: "root render replay",
305 limit: ROOT_RENDER_REPLAY_LIMIT,
306 });
307 }
308
309 self.render_root_pass(key, content)?;
310 did_work = true;
311 }
312 }
313
314 pub fn render(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
317 self.reset_last_pass_stats();
318 self.render_root_pass(key, &mut content)?;
319 let _ = self.process_invalid_scopes()?;
320 Ok(())
321 }
322
323 pub fn render_stable(&mut self, key: Key, mut content: impl FnMut()) -> Result<(), NodeError> {
326 self.reset_last_pass_stats();
327 self.render_root_pass(key, &mut content)?;
328 let _ = self.reconcile_with_content(key, &mut content)?;
329 Ok(())
330 }
331
332 pub fn reconcile(&mut self, key: Key, mut content: impl FnMut()) -> Result<bool, NodeError> {
335 self.reconcile_with_content(key, &mut content)
336 }
337
338 pub fn should_render(&self) -> bool {
347 self.root_render_requested || self.runtime.needs_frame() || self.runtime.has_updates()
348 }
349
350 pub fn should_recompose(&self) -> bool {
366 self.root_render_requested || self.runtime.has_updates()
367 }
368
369 pub fn set_pausable_budget(&self, budget: Option<std::time::Duration>) {
374 self.composer_state.pausing.set_budget(budget);
375 }
376
377 #[doc(hidden)]
382 pub fn open_layout_budget(&self) -> LayoutBudget {
383 self.composer_state.pausing.open_layout();
384 LayoutBudget {
385 state: Rc::clone(&self.composer_state),
386 runtime: self.runtime.handle(),
387 }
388 }
389
390 pub fn runtime_handle(&self) -> RuntimeHandle {
392 self.runtime.handle()
393 }
394
395 pub fn runtime(&self) -> &Runtime {
397 &self.runtime
398 }
399
400 pub fn applier_mut(&mut self) -> ApplierGuard<'_, A> {
402 self.applier.borrow_typed()
403 }
404
405 pub fn root(&self) -> Option<NodeId> {
407 self.root
408 }
409
410 pub fn debug_dump_slot_table_groups(&self) -> Vec<(usize, Key, Option<ScopeId>, usize)> {
413 self.slots.borrow().debug_dump_groups()
414 }
415
416 pub fn debug_dump_slot_entries(&self) -> Vec<crate::SlotDebugEntry> {
418 self.slots.borrow().debug_dump_slot_entries()
419 }
420
421 pub fn slot_table_heap_bytes(&self) -> usize {
423 self.slots.borrow().heap_bytes()
424 }
425
426 pub fn debug_slot_table_stats(&self) -> SlotTableDebugStats {
428 self.slots.debug_stats()
429 }
430
431 pub fn debug_slot_snapshot(&self) -> SlotDebugSnapshot {
433 self.slots.debug_snapshot()
434 }
435
436 pub fn debug_last_pass_stats(&self) -> CompositionPassDebugStats {
438 self.last_pass_stats
439 }
440
441 #[cfg(test)]
442 pub(crate) fn debug_validate_slots(&self) -> Result<(), crate::slot::SlotInvariantError> {
443 let table = self.slots.borrow();
444 table.validate()?;
445 self.composer_state
446 .validate_host_retention(self.slots.as_ref(), &table)
447 }
448
449 fn host_for_scope(&self, scope: &RecomposeScope, root_host: &Rc<SlotsHost>) -> Rc<SlotsHost> {
453 scope
454 .slots_runtime_state()
455 .and_then(|state| {
456 scope
457 .slots_storage_key()
458 .and_then(|storage_key| state.host_for_storage_key(storage_key))
459 })
460 .or_else(|| {
461 scope
462 .slots_storage_key()
463 .and_then(|storage_key| self.composer_state.host_for_storage_key(storage_key))
464 })
465 .unwrap_or_else(|| Rc::clone(root_host))
466 }
467
468 fn group_scopes_by_host(
470 &self,
471 scopes: Vec<RecomposeScope>,
472 ) -> SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> {
473 let root_host = self.slots_host();
474 let first_identity = scopes.first().map(RecomposeScope::slots_host_identity);
477 if let Some(first) = scopes.first()
478 && scopes
479 .iter()
480 .all(|scope| Some(scope.slots_host_identity()) == first_identity)
481 {
482 let host = self.host_for_scope(first, &root_host);
483 return smallvec::smallvec![(host, scopes)];
484 }
485 let mut scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]> = SmallVec::new();
486 let mut scope_group_index: HashMap<usize, usize> = HashMap::default();
487 let mut previous_host: Option<(_, usize)> = None;
488 for scope in scopes {
489 let identity = scope.slots_host_identity();
490 if let Some((previous_identity, index)) = previous_host
491 && previous_identity == identity
492 {
493 scope_groups[index].1.push(scope);
494 continue;
495 }
496 let host = self.host_for_scope(&scope, &root_host);
497 let host_key = host.storage_key();
498 let index = if let Some(index) = scope_group_index.get(&host_key).copied() {
499 scope_groups[index].1.push(scope);
500 index
501 } else {
502 scope_group_index.insert(host_key, scope_groups.len());
503 scope_groups.push((host, vec![scope]));
504 scope_groups.len() - 1
505 };
506 previous_host = Some((identity, index));
507 }
508 scope_groups
509 }
510
511 fn process_invalid_scopes_within_budget(&mut self) -> Result<bool, NodeError> {
514 self.composer_state.pausing.start_budget();
515 let result = self.process_invalid_scopes_until_root_request();
516 self.composer_state.pausing.end_budget(&self.runtime);
517 result
518 }
519
520 fn process_invalid_scopes_until_root_request(&mut self) -> Result<bool, NodeError> {
521 let runtime_handle = self.runtime_handle();
522 let mut did_recompose = false;
523 let mut loop_count = 0;
524 loop {
525 loop_count += 1;
526 if loop_count > ROOT_RENDER_REPLAY_LIMIT {
527 log::error!(
528 "process_invalid_scopes looped past {ROOT_RENDER_REPLAY_LIMIT} iterations; breaking to keep UI responsive"
529 );
530 return Err(NodeError::RecompositionLimitExceeded {
531 operation: "process_invalid_scopes",
532 limit: ROOT_RENDER_REPLAY_LIMIT,
533 });
534 }
535 runtime_handle.drain_ui();
536 self.dispose_forgotten_movables()?;
537 let (scopes, batch) = match runtime_handle.take_invalidated_scopes() {
538 Some(scopes) => (scopes, Batch::Invalid),
539 None => match self.composer_state.pausing.take_resumable() {
540 Some((content, scopes)) => (scopes, Batch::Waiting(content)),
541 None => break,
542 },
543 };
544 let Some(scopes) = self.rerun_derivations(&runtime_handle, scopes) else {
545 continue;
546 };
547 did_recompose = true;
548 let scope_groups = self.group_scopes_by_host(scopes);
549 for (index, (host, scopes)) in scope_groups.iter().enumerate() {
550 let side_effects =
551 self.recompose_host_scopes(host, scopes, &batch, &runtime_handle)?;
552 runtime_handle.drain_ui();
553 for effect in side_effects {
554 effect();
555 }
556 runtime_handle.drain_ui();
557 self.maybe_dump_slot_table("recompose_pass");
558 if self.root_render_requested {
559 requeue_scopes(&runtime_handle, &batch, &scope_groups[index + 1..]);
560 break;
561 }
562 }
563 recycle_scope_groups(&runtime_handle, scope_groups);
564 if let Batch::Waiting(content) = batch {
565 self.composer_state.pausing.finish_resumed(content);
566 }
567 if self.root_render_requested {
568 break;
569 }
570 }
571 self.finalize_runtime_state();
572 Ok(did_recompose)
573 }
574
575 fn recompose_host_scopes(
578 &mut self,
579 host: &Rc<SlotsHost>,
580 scopes: &[RecomposeScope],
581 batch: &Batch,
582 runtime_handle: &RuntimeHandle,
583 ) -> Result<Vec<Box<dyn FnOnce()>>, NodeError> {
584 let shared_state = host
585 .runtime_state()
586 .or_else(|| scopes.first().and_then(RecomposeScope::slots_runtime_state))
587 .unwrap_or_else(|| Rc::clone(&self.composer_state));
588 let threshold_ms = DIAGNOSTICS.recompose_scope_telemetry_ms.threshold_ms();
589 let _teardown = runtime::enter_state_teardown_scope();
590 let composer = Composer::for_pass(
591 self.parked_composer.take(),
592 shared_state,
593 Rc::clone(host),
594 self.applier_host(),
595 runtime_handle.clone(),
596 self.root,
597 );
598 composer.parent_stack().clear();
599 let (root, side_effects, requested_root_render, compact_applier) =
600 composer.install(|composer| {
601 let ((), outcome) = composer.try_with_slot_host_pass(
602 Rc::clone(host),
603 crate::slot::SlotPassMode::Recompose,
604 |composer| match (batch, threshold_ms) {
605 (Batch::Invalid, None) => {
606 for scope in scopes {
607 composer.recompose_group(scope);
608 }
609 }
610 (Batch::Invalid, Some(threshold_ms)) => {
611 recompose_timed(composer, scopes, threshold_ms);
612 }
613 (Batch::Waiting(content), _) => {
614 composer.resume_waiting_bodies(content, scopes);
615 }
616 },
617 )?;
618 let root = composer.root();
619 let side_effects = composer.take_side_effects();
620 let requested_root_render = composer.take_root_render_request();
621 Ok((root, side_effects, requested_root_render, outcome.compacted))
622 })?;
623 self.apply_pass(host, runtime_handle, &composer, &side_effects)?;
624 if compact_applier {
625 self.applier.compact();
626 self.applier.borrow_dyn().clear_recycled_nodes();
627 }
628 if root.is_some() {
629 self.root = root;
630 }
631 if requested_root_render {
632 self.root_render_requested = true;
633 }
634 self.park_composer(host, composer);
635 Ok(side_effects)
636 }
637
638 pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
641 self.process_invalid_scopes_within_budget()
642 }
643
644 fn rerun_derivations(
650 &mut self,
651 runtime: &RuntimeHandle,
652 mut scopes: Vec<RecomposeScope>,
653 ) -> Option<Vec<RecomposeScope>> {
654 if scopes.iter().any(RecomposeScope::is_derivation) {
655 self.rerun_derivations_of(runtime, &mut scopes);
656 }
657 if scopes.is_empty() {
658 runtime.recycle_scopes(scopes);
659 return None;
660 }
661 Some(scopes)
662 }
663
664 fn rerun_derivations_of(&mut self, runtime: &RuntimeHandle, scopes: &mut Vec<RecomposeScope>) {
665 let _teardown = runtime::enter_state_teardown_scope();
666 let host = self.slots_host();
667 let composer = Composer::for_pass(
668 self.parked_composer.take(),
669 Rc::clone(&self.composer_state),
670 Rc::clone(&host),
671 self.applier_host(),
672 runtime.clone(),
673 self.root,
674 );
675 composer.install(|composer| {
676 for scope in scopes.iter().filter(|scope| scope.is_derivation()) {
677 composer.recompose_group(scope);
678 }
679 });
680 self.park_composer(&host, composer);
681 scopes.retain(|scope| !scope.is_derivation());
682 }
683
684 fn park_composer(&mut self, host: &Rc<SlotsHost>, composer: Composer) {
686 if Rc::ptr_eq(host, &self.slots) {
687 self.parked_composer = composer.park();
688 }
689 }
690
691 fn dispose_forgotten_movables(&mut self) -> Result<(), NodeError> {
692 let ids = self.runtime.take_forgotten_movables();
693 if ids.is_empty() {
694 return Ok(());
695 }
696 self.forget_movables(&ids)
697 }
698
699 #[inline(never)]
702 fn forget_movables(&mut self, ids: &[Key]) -> Result<(), NodeError> {
703 let runtime_handle = self.runtime_handle();
704 let _teardown = runtime::enter_state_teardown_scope();
705 let host = self.slots_host();
706 let composer = Composer::new_with_shared_state(
707 Rc::clone(&self.composer_state),
708 Rc::clone(&host),
709 self.applier_host(),
710 runtime_handle.clone(),
711 self.root,
712 );
713 composer.install(|composer| composer.forget_movables(ids))?;
714 self.apply_commands_and_updates_for_host(&host, &runtime_handle, &composer)
715 }
716
717 pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
719 let updates = self.runtime.take_updates();
720 let mut applier = self.applier.borrow_dyn();
721 for update in updates {
722 update.apply(&mut *applier)?;
723 }
724 Ok(())
725 }
726}
727
728#[expect(clippy::print_stderr, reason = "a scope telemetry threshold asks")]
731fn recompose_timed(composer: &Composer, scopes: &[RecomposeScope], threshold_ms: f64) {
732 for scope in scopes {
733 let start = Instant::now();
734 composer.recompose_group(scope);
735 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
736 if elapsed_ms >= threshold_ms {
737 eprintln!(
738 "[recompose-scope-telemetry] scope_id={} label={:?} elapsed_ms={elapsed_ms:.3} invalidation_sources={:?}",
739 scope.id(),
740 debug_scope_label(scope.id()),
741 debug_scope_invalidation_sources(scope.id())
742 );
743 }
744 }
745}
746
747enum Batch {
749 Invalid,
751 Waiting(PausableContent),
753}
754
755fn requeue_scopes(
758 runtime_handle: &RuntimeHandle,
759 batch: &Batch,
760 scope_groups: &[(Rc<SlotsHost>, Vec<RecomposeScope>)],
761) {
762 for scope in scope_groups.iter().flat_map(|(_, scopes)| scopes) {
763 match batch {
764 Batch::Invalid => runtime_handle.requeue_invalid_scope(scope),
765 Batch::Waiting(content) => content.keep_waiting(scope),
766 }
767 }
768}
769
770fn recycle_scope_groups(
773 runtime: &RuntimeHandle,
774 scope_groups: SmallVec<[(Rc<SlotsHost>, Vec<RecomposeScope>); 1]>,
775) {
776 for (_, scopes) in scope_groups {
777 runtime.recycle_scopes(scopes);
778 }
779}
780
781impl<A: Applier + 'static> Drop for Composition<A> {
782 fn drop(&mut self) {
783 #[cfg(target_os = "android")]
784 self.runtime.restore_thread_state();
785 self.teardown = Some(runtime::enter_state_teardown_scope());
786 }
787}
788
789#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
792pub struct CompositionPassDebugStats {
793 pub commands_len: usize,
795 pub commands_cap: usize,
797 pub command_payload_len_bytes: usize,
799 pub command_payload_cap_bytes: usize,
801 pub sync_children_len: usize,
803 pub sync_children_cap: usize,
805 pub sync_child_ids_len: usize,
807 pub sync_child_ids_cap: usize,
809 pub side_effects_len: usize,
811 pub side_effects_cap: usize,
813}