1use std::{any::Any, collections::VecDeque, fmt, rc::Rc};
10
11use smallvec::SmallVec;
12
13use crate::{
14 CallbackHolder, NodeId, RecomposeScope, SlotTable, SlotsHost,
15 collections::map::{HashMap, HashSet},
16};
17
18pub type DebugSlotGroup = (usize, crate::Key, Option<usize>, usize);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct SlotId(pub u64);
28
29impl SlotId {
30 #[inline]
32 pub fn new(raw: u64) -> Self {
33 Self(raw)
34 }
35
36 #[inline]
38 pub fn raw(self) -> u64 {
39 self.0
40 }
41}
42
43pub trait SlotReusePolicy: 'static {
49 fn get_slots_to_retain(&self, active: &[SlotId]) -> HashSet<SlotId> {
53 let _ = active;
54 HashSet::default()
55 }
56
57 fn are_compatible(&self, existing: TypedSlot, requested: TypedSlot) -> bool;
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct TypedSlot {
72 pub id: SlotId,
74 pub content_type: Option<u64>,
76}
77
78#[derive(Debug, Default)]
82pub struct DefaultSlotReusePolicy;
83
84impl SlotReusePolicy for DefaultSlotReusePolicy {
85 fn are_compatible(&self, existing: TypedSlot, requested: TypedSlot) -> bool {
86 existing.id == requested.id
87 }
88}
89
90#[derive(Debug, Default)]
114pub struct ContentTypeReusePolicy;
115
116impl SlotReusePolicy for ContentTypeReusePolicy {
117 fn are_compatible(&self, existing: TypedSlot, requested: TypedSlot) -> bool {
118 existing.id == requested.id || existing.content_type == requested.content_type
119 }
120}
121
122#[doc(hidden)]
123pub struct ExactSlotActivation<'a> {
124 pub nodes: &'a [NodeId],
125 pub was_recycled: bool,
126}
127
128#[derive(Default)]
131pub struct SubcomposeDisposal {
132 pub(crate) nodes: Vec<NodeId>,
133 pub(crate) slot_hosts: Vec<Rc<SlotsHost>>,
134}
135
136impl SubcomposeDisposal {
137 pub fn nodes(&self) -> &[NodeId] {
146 &self.nodes
147 }
148
149 fn append(&mut self, mut other: Self) {
150 self.nodes.append(&mut other.nodes);
151 self.slot_hosts.append(&mut other.slot_hosts);
152 }
153}
154
155#[derive(Default)]
158struct SlotRecord {
159 nodes: Option<Vec<NodeId>>,
161 scopes: Option<Vec<RecomposeScope>>,
163 live: bool,
165 active_pass: u64,
167 reusable_nodes: usize,
169 exact_reactivation: bool,
171 content_type: Option<u64>,
172 composition: Option<Rc<SlotsHost>>,
173 callback: Option<CallbackHolder>,
174 capture_key: Option<RetainedCaptureKey>,
175 composed_generation: Option<(u64, u64)>,
177}
178
179impl SlotRecord {
180 fn has_stale_scopes(&self) -> bool {
183 self.scopes
184 .iter()
185 .flatten()
186 .any(|scope| scope.is_invalid() || !scope.is_active())
187 }
188
189 fn has_invalid_scopes(&self) -> bool {
190 self.scopes.iter().flatten().any(RecomposeScope::is_invalid)
191 }
192
193 fn is_empty(&self) -> bool {
195 self.nodes.is_none()
196 && self.scopes.is_none()
197 && !self.live
198 && self.reusable_nodes == 0
199 && !self.exact_reactivation
200 && self.content_type.is_none()
201 && self.composition.is_none()
202 && self.callback.is_none()
203 && self.capture_key.is_none()
204 && self.composed_generation.is_none()
205 }
206}
207
208pub struct SubcomposeState {
211 slots: HashMap<SlotId, SlotRecord>,
212 node_to_slot: HashMap<NodeId, SlotId>,
213 active_order: Vec<SlotId>,
214 pass: u64,
217 reusable_by_type: HashMap<u64, VecDeque<(SlotId, NodeId)>>,
218 reusable_nodes_untyped: VecDeque<(SlotId, NodeId)>,
219 precomposed_nodes: HashMap<SlotId, Vec<NodeId>>,
220 policy: Box<dyn SlotReusePolicy>,
221 pub(crate) current_index: usize,
222 pub(crate) reusable_count: usize,
223 pub(crate) precomposed_count: usize,
224 max_reusable_per_type: usize,
225 max_reusable_untyped: usize,
226 last_slot_reused: Option<bool>,
227 content_generation: std::cell::Cell<u64>,
228 locals_generation: std::cell::Cell<u64>,
231}
232
233struct RetainedCaptureKey {
234 value: Box<dyn Any>,
235 eq: fn(&dyn Any, &dyn Any) -> bool,
236}
237
238fn retained_capture_key_eq<K: PartialEq + 'static>(stored: &dyn Any, candidate: &dyn Any) -> bool {
239 match (stored.downcast_ref::<K>(), candidate.downcast_ref::<K>()) {
240 (Some(stored), Some(candidate)) => stored == candidate,
241 _ => false,
242 }
243}
244
245impl fmt::Debug for SubcomposeState {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.debug_struct("SubcomposeState")
248 .field("slots_count", &self.slots.len())
249 .field("node_to_slot", &self.node_to_slot)
250 .field("active_order", &self.active_order)
251 .field("reusable_by_type_count", &self.reusable_by_type.len())
252 .field("reusable_untyped_count", &self.reusable_nodes_untyped.len())
253 .field("precomposed_nodes", &self.precomposed_nodes)
254 .field("current_index", &self.current_index)
255 .field("reusable_count", &self.reusable_count)
256 .field("precomposed_count", &self.precomposed_count)
257 .finish()
258 }
259}
260
261impl Default for SubcomposeState {
262 fn default() -> Self {
263 Self::new(Box::new(DefaultSlotReusePolicy))
264 }
265}
266
267const DEFAULT_MAX_REUSABLE_PER_TYPE: usize = 5;
268
269const DEFAULT_MAX_REUSABLE_UNTYPED: usize = 10;
270
271impl SubcomposeState {
272 pub fn new(policy: Box<dyn SlotReusePolicy>) -> Self {
274 Self {
275 slots: HashMap::default(),
276 node_to_slot: HashMap::default(),
277 active_order: Vec::new(),
278 pass: 1,
279 reusable_by_type: HashMap::default(),
280 reusable_nodes_untyped: VecDeque::new(),
281 precomposed_nodes: HashMap::default(),
282 policy,
283 current_index: 0,
284 reusable_count: 0,
285 precomposed_count: 0,
286 max_reusable_per_type: DEFAULT_MAX_REUSABLE_PER_TYPE,
287 max_reusable_untyped: DEFAULT_MAX_REUSABLE_UNTYPED,
288 last_slot_reused: None,
289 content_generation: std::cell::Cell::new(0),
290 locals_generation: std::cell::Cell::new(0),
291 }
292 }
293
294 pub fn set_policy(&mut self, policy: Box<dyn SlotReusePolicy>) {
296 self.policy = policy;
297 }
298
299 pub fn set_reusable_pool_limits(&mut self, per_type: usize, untyped: usize) {
302 self.max_reusable_per_type = per_type;
303 self.max_reusable_untyped = untyped;
304 }
305
306 fn record(&mut self, slot_id: SlotId) -> &mut SlotRecord {
307 self.slots.entry(slot_id).or_default()
308 }
309
310 fn slot_nodes(&self, slot_id: SlotId) -> Option<&[NodeId]> {
311 self.slots.get(&slot_id)?.nodes.as_deref()
312 }
313
314 fn typed_slot(&self, slot_id: SlotId) -> TypedSlot {
315 TypedSlot {
316 id: slot_id,
317 content_type: self.get_content_type(slot_id),
318 }
319 }
320
321 fn slot_active_in_pass(&self, slot_id: SlotId) -> bool {
322 self.slots
323 .get(&slot_id)
324 .is_some_and(|record| record.active_pass == self.pass)
325 }
326
327 fn clear_exact_reactivation(&mut self, slot_id: SlotId) {
328 if let Some(record) = self.slots.get_mut(&slot_id) {
329 record.exact_reactivation = false;
330 }
331 }
332
333 pub fn register_content_type(&mut self, slot_id: SlotId, content_type: u64) {
337 self.record(slot_id).content_type = Some(content_type);
338 }
339
340 pub fn update_content_type(&mut self, slot_id: SlotId, content_type: Option<u64>) {
346 match content_type {
347 Some(content_type) => self.register_content_type(slot_id, content_type),
348 None => {
349 if let Some(record) = self.slots.get_mut(&slot_id) {
350 record.content_type = None;
351 }
352 }
353 }
354 }
355
356 pub fn get_content_type(&self, slot_id: SlotId) -> Option<u64> {
358 self.slots.get(&slot_id)?.content_type
359 }
360
361 pub fn active_slot_for_node(&self, node_id: NodeId) -> Option<SlotId> {
366 let slot = *self.node_to_slot.get(&node_id)?;
367 self.slot_is_active(slot).then_some(slot)
368 }
369
370 pub fn begin_pass(&mut self) {
375 self.current_index = 0;
376 self.pass += 1;
377 }
378
379 pub fn active_slot_cursor(&self) -> usize {
381 self.current_index
382 }
383
384 pub fn restore_active_slot_cursor(&mut self, cursor: usize) {
387 self.current_index = cursor.min(self.active_order.len());
388 }
389
390 pub fn recycle_active_slot(&mut self, slot_id: SlotId) -> SubcomposeDisposal {
392 self.recycle_active_slot_internal(slot_id, false)
393 }
394
395 pub fn recycle_prefetched_active_slot(&mut self, slot_id: SlotId) -> SubcomposeDisposal {
398 self.recycle_active_slot_internal(slot_id, true)
399 }
400
401 pub fn recycle_active_slots_where(
404 &mut self,
405 mut predicate: impl FnMut(SlotId) -> bool,
406 ) -> SubcomposeDisposal {
407 let slots: Vec<_> = self
408 .active_order
409 .iter()
410 .copied()
411 .filter(|slot| !self.slot_active_in_pass(*slot))
412 .filter(|slot| predicate(*slot))
413 .collect();
414 let mut disposed = SubcomposeDisposal::default();
415 for slot in slots {
416 disposed.append(self.recycle_active_slot(slot));
417 }
418 disposed
419 }
420
421 fn recycle_active_slot_internal(
422 &mut self,
423 slot_id: SlotId,
424 allow_exact_reactivation: bool,
425 ) -> SubcomposeDisposal {
426 let Some(position) = self
427 .active_order
428 .iter()
429 .position(|candidate| *candidate == slot_id)
430 else {
431 return SubcomposeDisposal::default();
432 };
433 self.active_order.remove(position);
434 if position < self.current_index {
435 self.current_index = self.current_index.saturating_sub(1);
436 }
437 self.move_slot_to_reusable(slot_id, allow_exact_reactivation);
438 self.enforce_reusable_pool_limits()
439 }
440
441 pub fn finish_pass(&mut self) -> SubcomposeDisposal {
443 self.dispose_or_reuse_starting_from_index(self.current_index)
444 }
445
446 pub fn get_or_create_slots(&mut self, slot_id: SlotId) -> Rc<SlotsHost> {
450 Rc::clone(
451 self.record(slot_id)
452 .composition
453 .get_or_insert_with(|| Rc::new(SlotsHost::new(SlotTable::new()))),
454 )
455 }
456
457 pub fn callback_holder(&mut self, slot_id: SlotId) -> CallbackHolder {
459 self.record(slot_id)
460 .callback
461 .get_or_insert_with(CallbackHolder::default)
462 .clone()
463 }
464
465 #[doc(hidden)]
466 pub fn activate_current_active_slot(&mut self, slot_id: SlotId) -> Option<&[NodeId]> {
467 if !self.activate_current_active_slot_at_cursor(slot_id) {
468 return None;
469 }
470 self.slot_nodes(slot_id)
471 }
472
473 fn activate_current_active_slot_at_cursor(&mut self, slot_id: SlotId) -> bool {
474 if self.active_order.get(self.current_index) != Some(&slot_id) {
475 return false;
476 }
477 let pass = self.pass;
478 let Some(record) = self.slots.get_mut(&slot_id) else {
479 return false;
480 };
481 if record.active_pass == pass
482 || record.exact_reactivation
483 || record.reusable_nodes > 0
484 || record.nodes.is_none()
485 || record.has_stale_scopes()
486 {
487 return false;
488 }
489 record.live = true;
490 record.active_pass = pass;
491 self.last_slot_reused = Some(true);
492 self.current_index += 1;
493 true
494 }
495
496 pub fn has_pending_precompositions(&self, slot_id: SlotId) -> bool {
500 self.precomposed_nodes.contains_key(&slot_id)
501 }
502
503 pub fn retained_capture_key_matches<K: PartialEq + 'static>(
507 &self,
508 slot_id: SlotId,
509 key: &K,
510 ) -> bool {
511 self.slots
512 .get(&slot_id)
513 .and_then(|record| record.capture_key.as_ref())
514 .is_some_and(|retained| (retained.eq)(retained.value.as_ref(), key))
515 }
516
517 pub fn store_retained_capture_key<K: PartialEq + 'static>(&mut self, slot_id: SlotId, key: K) {
519 self.record(slot_id).capture_key = Some(RetainedCaptureKey {
520 value: Box::new(key),
521 eq: retained_capture_key_eq::<K>,
522 });
523 }
524
525 #[doc(hidden)]
526 pub fn activate_exact_slot(&mut self, slot_id: SlotId) -> Option<ExactSlotActivation<'_>> {
527 let was_recycled = if self.activate_current_active_slot_at_cursor(slot_id) {
528 false
529 } else {
530 self.reactivate_exact_slot(slot_id)?
531 };
532 Some(ExactSlotActivation {
533 nodes: self.slot_nodes(slot_id)?,
534 was_recycled,
535 })
536 }
537
538 fn reactivate_exact_slot(&mut self, slot_id: SlotId) -> Option<bool> {
542 let active_position = self
543 .active_order
544 .iter()
545 .position(|candidate| *candidate == slot_id);
546 let record = self.slots.get(&slot_id)?;
547 if active_position.is_none() && !record.exact_reactivation || record.has_invalid_scopes() {
548 return None;
549 }
550 let nodes: SmallVec<[NodeId; 4]> = record.nodes.as_deref()?.iter().copied().collect();
551 if active_position.is_none() {
552 for scope in record.scopes.iter().flatten() {
553 scope.reactivate();
554 }
555 for &node in &nodes {
556 let _ = self.remove_from_reusable_pools(node);
557 }
558 }
559 self.mark_slot_active(slot_id, true);
560 Self::consume_precomposed_nodes(
561 &mut self.precomposed_nodes,
562 &mut self.precomposed_count,
563 slot_id,
564 &nodes,
565 );
566 Some(active_position.is_none())
567 }
568
569 pub fn register_active(
572 &mut self,
573 slot_id: SlotId,
574 node_ids: &[NodeId],
575 scopes: &[RecomposeScope],
576 ) {
577 for scope in scopes {
578 scope.reactivate();
579 }
580 let record = self.slots.entry(slot_id).or_default();
581 let was_reused = record.nodes.is_some();
582 let nodes = record.nodes.get_or_insert_default();
583 nodes.clear();
584 nodes.extend_from_slice(node_ids);
585 let held_scopes = record.scopes.get_or_insert_default();
586 held_scopes.clear();
587 held_scopes.extend_from_slice(scopes);
588 for &node in node_ids {
589 self.node_to_slot.insert(node, slot_id);
590 }
591 Self::consume_precomposed_nodes(
592 &mut self.precomposed_nodes,
593 &mut self.precomposed_count,
594 slot_id,
595 node_ids,
596 );
597 self.mark_slot_active(slot_id, was_reused);
598 }
599
600 fn mark_slot_active(&mut self, slot_id: SlotId, was_reused: bool) {
601 self.last_slot_reused = Some(was_reused);
602 let pass = self.pass;
603 let record = self.record(slot_id);
604 record.live = true;
605 record.active_pass = pass;
606 record.exact_reactivation = false;
607
608 if let Some(position) = self.active_order.iter().position(|slot| *slot == slot_id) {
609 if position < self.current_index {
610 return;
611 }
612 self.active_order.remove(position);
613 }
614 let insert_at = self.current_index.min(self.active_order.len());
615 self.active_order.insert(insert_at, slot_id);
616 self.current_index += 1;
617 }
618
619 fn consume_precomposed_nodes(
620 precomposed_nodes: &mut HashMap<SlotId, Vec<NodeId>>,
621 precomposed_count: &mut usize,
622 slot_id: SlotId,
623 node_ids: &[NodeId],
624 ) {
625 if let Some(nodes) = precomposed_nodes.get_mut(&slot_id) {
626 let before_len = nodes.len();
627 nodes.retain(|node| !node_ids.contains(node));
628 let removed = before_len - nodes.len();
629 *precomposed_count = precomposed_count.saturating_sub(removed);
630 if nodes.is_empty() {
631 precomposed_nodes.remove(&slot_id);
632 }
633 }
634 }
635
636 pub fn register_precomposed(&mut self, slot_id: SlotId, node_id: NodeId) {
639 self.precomposed_nodes
640 .entry(slot_id)
641 .or_default()
642 .push(node_id);
643 self.precomposed_count += 1;
644 }
645
646 fn decrement_reusable_slot(&mut self, slot_id: SlotId) {
647 if let Some(record) = self.slots.get_mut(&slot_id) {
648 record.reusable_nodes = record.reusable_nodes.saturating_sub(1);
649 }
650 self.reusable_count = self.reusable_count.saturating_sub(1);
651 }
652
653 fn remove_node_from_slot(&mut self, node: NodeId) -> Option<SlotId> {
656 let slot = self.node_to_slot.remove(&node)?;
657 if let Some(record) = self.slots.get_mut(&slot)
658 && let Some(nodes) = record.nodes.as_mut()
659 {
660 if let Some(index) = nodes.iter().position(|candidate| *candidate == node) {
661 nodes.remove(index);
662 }
663 if nodes.is_empty() {
664 record.nodes = None;
665 record.scopes = None;
666 }
667 }
668 Some(slot)
669 }
670
671 pub fn slot_is_active(&self, slot_id: SlotId) -> bool {
674 self.slots.get(&slot_id).is_some_and(|record| record.live)
675 }
676
677 pub fn slot_is_retained(&self, slot_id: SlotId) -> bool {
681 self.slots
682 .get(&slot_id)
683 .is_some_and(|record| record.live || record.reusable_nodes > 0)
684 }
685
686 fn prune_slot_if_unused(&mut self, slot_id: SlotId) -> Option<Rc<SlotsHost>> {
687 if self.slot_is_retained(slot_id) {
688 return None;
689 }
690
691 debug_assert!(
692 self.slot_nodes(slot_id).is_none(),
693 "inactive slot {slot_id:?} still has mapped nodes",
694 );
695
696 let host = self
697 .slots
698 .remove(&slot_id)
699 .and_then(|record| record.composition);
700 if let Some(nodes) = self.precomposed_nodes.remove(&slot_id) {
701 self.precomposed_count = self.precomposed_count.saturating_sub(nodes.len());
702 }
703 host
704 }
705
706 pub fn take_node_from_reusables(&mut self, slot_id: SlotId) -> Option<(NodeId, bool)> {
717 if let Some(node_id) = self
718 .slot_nodes(slot_id)
719 .and_then(|nodes| nodes.first().copied())
720 {
721 let _ = self.remove_from_reusable_pools(node_id);
722 self.clear_exact_reactivation(slot_id);
723 return Some((node_id, false));
724 }
725
726 let requested = self.typed_slot(slot_id);
727
728 if let Some(content_type) = requested.content_type
729 && let Some((old_slot, node_id)) =
730 self.take_compatible_typed_reusable(content_type, requested)
731 {
732 self.decrement_reusable_slot(old_slot);
733 self.move_node_to_slot(node_id, old_slot, slot_id);
734 return Some((node_id, true));
735 }
736
737 let slots = &self.slots;
738 let policy = &self.policy;
739 let position = self
740 .reusable_nodes_untyped
741 .iter()
742 .position(|(existing_slot, _)| {
743 reusable_for(slots, policy.as_ref(), *existing_slot, requested)
744 });
745
746 if let Some(index) = position
747 && let Some((old_slot, node_id)) = self.reusable_nodes_untyped.remove(index)
748 {
749 self.decrement_reusable_slot(old_slot);
750 self.move_node_to_slot(node_id, old_slot, slot_id);
751 return Some((node_id, true));
752 }
753
754 None
755 }
756
757 fn take_compatible_typed_reusable(
758 &mut self,
759 content_type: u64,
760 requested: TypedSlot,
761 ) -> Option<(SlotId, NodeId)> {
762 let reused = {
763 let slots = &self.slots;
764 let policy = &self.policy;
765 let pool = self.reusable_by_type.get_mut(&content_type)?;
766 let index = pool.iter().position(|(existing_slot, _)| {
767 reusable_for(slots, policy.as_ref(), *existing_slot, requested)
768 })?;
769 pool.remove(index)
770 };
771
772 if self
773 .reusable_by_type
774 .get(&content_type)
775 .is_some_and(std::collections::VecDeque::is_empty)
776 {
777 self.reusable_by_type.remove(&content_type);
778 }
779
780 reused
781 }
782
783 fn remove_from_reusable_pools(&mut self, node_id: NodeId) -> Option<SlotId> {
784 let mut typed_match = None;
785 for (&content_type, pool) in &self.reusable_by_type {
786 if let Some(position) = pool
787 .iter()
788 .position(|(_, pooled_node)| *pooled_node == node_id)
789 {
790 typed_match = Some((content_type, position));
791 break;
792 }
793 }
794 if let Some((content_type, position)) = typed_match {
795 let pool = self.reusable_by_type.get_mut(&content_type)?;
796 let (slot, _) = pool.remove(position)?;
797 if pool.is_empty() {
798 self.reusable_by_type.remove(&content_type);
799 }
800 self.decrement_reusable_slot(slot);
801 return Some(slot);
802 }
803 if let Some(position) = self
804 .reusable_nodes_untyped
805 .iter()
806 .position(|(_, n)| *n == node_id)
807 && let Some((slot, _)) = self.reusable_nodes_untyped.remove(position)
808 {
809 self.decrement_reusable_slot(slot);
810 return Some(slot);
811 }
812 None
813 }
814
815 fn move_node_to_slot(&mut self, node_id: NodeId, old_slot: SlotId, new_slot: SlotId) {
816 if old_slot == new_slot {
817 return;
818 }
819
820 self.remove_node_from_slot(node_id);
821 let (composition, callback) = match self.slots.get_mut(&old_slot) {
822 Some(old) => {
823 old.exact_reactivation = false;
824 old.content_type = None;
825 old.capture_key = None;
826 old.composed_generation = None;
827 (old.composition.take(), old.callback.take())
828 }
829 None => (None, None),
830 };
831 if self.slots.get(&old_slot).is_some_and(SlotRecord::is_empty) {
832 self.slots.remove(&old_slot);
833 }
834 self.node_to_slot.insert(node_id, new_slot);
835 let new = self.record(new_slot);
836 new.nodes.get_or_insert_default().push(node_id);
837 new.capture_key = None;
838 new.composed_generation = None;
839 if composition.is_some() {
840 new.composition = composition;
841 }
842 if callback.is_some() {
843 new.callback = callback;
844 }
845 if let Some(nodes) = self.precomposed_nodes.get_mut(&old_slot) {
846 let before_len = nodes.len();
847 nodes.retain(|candidate| *candidate != node_id);
848 let removed = before_len - nodes.len();
849 self.precomposed_count = self.precomposed_count.saturating_sub(removed);
850 if nodes.is_empty() {
851 self.precomposed_nodes.remove(&old_slot);
852 }
853 }
854 }
855
856 pub fn dispose_or_reuse_starting_from_index(
860 &mut self,
861 start_index: usize,
862 ) -> SubcomposeDisposal {
863 if start_index >= self.active_order.len() {
864 return SubcomposeDisposal::default();
865 }
866
867 let retain = self
868 .policy
869 .get_slots_to_retain(&self.active_order[start_index..]);
870 let mut retained = Vec::new();
871 while self.active_order.len() > start_index {
872 let Some(slot) = self.active_order.pop() else {
873 break;
874 };
875 if retain.contains(&slot) {
876 retained.push(slot);
877 continue;
878 }
879 self.move_slot_to_reusable(slot, false);
880 }
881 retained.reverse();
882 self.active_order.extend(retained);
883
884 self.enforce_reusable_pool_limits()
885 }
886
887 fn move_slot_to_reusable(&mut self, slot: SlotId, allow_exact_reactivation: bool) {
888 let record = self.slots.entry(slot).or_default();
889 record.live = false;
890 record.active_pass = 0;
891 for scope in record.scopes.iter().flatten() {
892 scope.deactivate();
893 }
894 let forgot_effects = record
895 .composition
896 .as_ref()
897 .is_some_and(|host| host.forget_effects());
898 record.exact_reactivation = allow_exact_reactivation && !forgot_effects;
899
900 let content_type = record.content_type;
901 let nodes = record.nodes.as_deref().unwrap_or_default();
902 record.reusable_nodes += nodes.len();
903 self.reusable_count += nodes.len();
904 let pool = match content_type {
905 Some(content_type) => self.reusable_by_type.entry(content_type).or_default(),
906 None => &mut self.reusable_nodes_untyped,
907 };
908 pool.extend(nodes.iter().map(|&node| (slot, node)));
909 }
910
911 fn enforce_reusable_pool_limits(&mut self) -> SubcomposeDisposal {
912 let mut disposed = SubcomposeDisposal::default();
913 let mut typed_disposals = Vec::new();
914 for pool in self.reusable_by_type.values_mut() {
915 while pool.len() > self.max_reusable_per_type {
916 if let Some((slot, node_id)) = pool.pop_front() {
917 typed_disposals.push((slot, node_id));
918 }
919 }
920 }
921 for (slot, node_id) in typed_disposals {
922 self.dispose_reusable_node(slot, node_id, &mut disposed);
923 }
924
925 while self.reusable_nodes_untyped.len() > self.max_reusable_untyped {
926 if let Some((slot, node_id)) = self.reusable_nodes_untyped.pop_front() {
927 self.dispose_reusable_node(slot, node_id, &mut disposed);
928 }
929 }
930
931 self.reusable_by_type.retain(|_, pool| !pool.is_empty());
932 disposed
933 }
934
935 fn dispose_reusable_node(
937 &mut self,
938 slot: SlotId,
939 node_id: NodeId,
940 disposed: &mut SubcomposeDisposal,
941 ) {
942 self.decrement_reusable_slot(slot);
943 self.remove_node_from_slot(node_id);
944 self.clear_exact_reactivation(slot);
945 disposed.slot_hosts.extend(self.prune_slot_if_unused(slot));
946 disposed.nodes.push(node_id);
947 }
948
949 pub fn reusable(&self) -> Vec<NodeId> {
951 let mut nodes: Vec<NodeId> = self
952 .reusable_by_type
953 .values()
954 .flat_map(|pool| pool.iter().map(|(_, n)| *n))
955 .collect();
956 nodes.extend(self.reusable_nodes_untyped.iter().map(|(_, n)| *n));
957 nodes
958 }
959
960 pub fn active_slots_count(&self) -> usize {
965 self.active_order.len()
966 }
967
968 pub fn reusable_slots_count(&self) -> usize {
973 self.reusable_count
974 }
975
976 pub fn invalidate_scopes(&self) {
989 for scope in self
990 .slots
991 .values()
992 .flat_map(|record| record.scopes.iter().flatten())
993 {
994 scope.invalidate();
995 }
996 self.content_generation
997 .set(self.content_generation.get() + 1);
998 }
999
1000 pub fn invalidate_locals(&self) {
1004 self.bump_content_generation();
1005 self.locals_generation.set(self.content_generation.get());
1006 }
1007
1008 pub fn slot_locals_stale(&self, slot_id: SlotId) -> bool {
1013 let changed_at = self.locals_generation.get();
1014 self.slots
1015 .get(&slot_id)
1016 .and_then(|record| record.composed_generation)
1017 .is_some_and(|(generation, _)| generation < changed_at)
1018 }
1019
1020 pub fn bump_content_generation(&self) {
1027 self.content_generation
1028 .set(self.content_generation.get() + 1);
1029 }
1030
1031 pub fn slot_content_generation_current(&self, slot_id: SlotId, owner_epoch: u64) -> bool {
1037 self.slots
1038 .get(&slot_id)
1039 .and_then(|record| record.composed_generation)
1040 == Some((self.content_generation.get(), owner_epoch))
1041 }
1042
1043 pub fn mark_slot_composed_current(&mut self, slot_id: SlotId, owner_epoch: u64) {
1046 let generation = self.content_generation.get();
1047 self.record(slot_id).composed_generation = Some((generation, owner_epoch));
1048 }
1049
1050 pub fn was_last_slot_reused(&self) -> Option<bool> {
1058 self.last_slot_reused
1059 }
1060
1061 #[doc(hidden)]
1062 pub fn debug_scope_ids_by_slot(&self) -> Vec<(u64, Vec<usize>)> {
1063 self.slots
1064 .iter()
1065 .filter_map(|(slot, record)| {
1066 let scopes = record.scopes.as_ref()?;
1067 Some((slot.raw(), scopes.iter().map(RecomposeScope::id).collect()))
1068 })
1069 .collect()
1070 }
1071
1072 #[doc(hidden)]
1073 pub fn debug_slot_table_for_slot(&self, slot_id: SlotId) -> Option<Vec<crate::SlotDebugEntry>> {
1074 let slots = self.slots.get(&slot_id)?.composition.as_ref()?;
1075 Some(slots.borrow().debug_dump_slot_entries())
1076 }
1077
1078 #[doc(hidden)]
1079 pub fn debug_slot_table_groups_for_slot(&self, slot_id: SlotId) -> Option<Vec<DebugSlotGroup>> {
1080 let slots = self.slots.get(&slot_id)?.composition.as_ref()?;
1081 Some(slots.borrow().debug_dump_groups())
1082 }
1083
1084 pub fn precomposed(&self) -> &HashMap<SlotId, Vec<NodeId>> {
1086 &self.precomposed_nodes
1087 }
1088
1089 pub fn drain_inactive_precomposed(&mut self) -> SubcomposeDisposal {
1092 let mut disposed = SubcomposeDisposal::default();
1093 let mut empty_slots = Vec::new();
1094 for (slot, nodes) in &self.precomposed_nodes {
1095 if !self.slot_active_in_pass(*slot) {
1096 disposed.nodes.extend(nodes.iter().copied());
1097 empty_slots.push(*slot);
1098 }
1099 }
1100 for slot in empty_slots {
1101 self.precomposed_nodes.remove(&slot);
1102 disposed.slot_hosts.extend(self.prune_slot_if_unused(slot));
1103 }
1104 self.precomposed_count = self.precomposed_count.saturating_sub(disposed.nodes.len());
1105 disposed
1106 }
1107}
1108
1109fn reusable_for(
1112 slots: &HashMap<SlotId, SlotRecord>,
1113 policy: &dyn SlotReusePolicy,
1114 existing: SlotId,
1115 requested: TypedSlot,
1116) -> bool {
1117 let record = slots.get(&existing);
1118 !record.is_some_and(|record| record.exact_reactivation)
1119 && policy.are_compatible(
1120 TypedSlot {
1121 id: existing,
1122 content_type: record.and_then(|record| record.content_type),
1123 },
1124 requested,
1125 )
1126}
1127
1128#[cfg(test)]
1129#[path = "tests/subcompose_tests.rs"]
1130mod tests;