Skip to main content

gc/
gc_stats.rs

1//! Diagnostics side of the collector: `run_gc_with_stats_bundle` plus the
2//! helpers that capture edges, object decisions, and restoration witnesses.
3//! The core collector and the thin `run_gc_with_stats` entry stay in
4//! gc_runtime.rs; this is a child module of `runtime` so it can read the
5//! runtime's private object table and GC lists.
6
7use crate::header::{GcId, GcListKind};
8use crate::list::GcListIter;
9use crate::report::{
10    summarize_gc_object, summarize_gc_objects, FinalFate, FreeCycleStats, GcPhaseStats,
11    GcStatsBundle, ObjectDecision, RestorationWitness, ScanStats, TrialDecision,
12    TrialDeletionStats, VisitedEdge, MAX_EDGE_DETAILS, MAX_OBJECT_DECISIONS,
13    MAX_RESTORATION_WITNESSES,
14};
15use crate::runtime::GcRuntime;
16use crate::value::{EdgeRelation, ValueCell};
17use std::collections::{HashMap, HashSet, VecDeque};
18
19impl GcRuntime {
20    /// Run all collector phases atomically and return diagnostics plus the object catalog.
21    pub fn run_gc_with_stats_bundle(&mut self) -> GcStatsBundle {
22        // 1. Capture immutable graph + RC before trial deletion.
23        let live_ids = self.sorted_list_ids(GcListKind::GcObj);
24        let mut ref_count_before = HashMap::with_capacity(live_ids.len());
25        for &id in &live_ids {
26            ref_count_before.insert(id, self.header(id).ref_count);
27        }
28        let all_edges = self.capture_visited_edges(&live_ids);
29        let edges_visited = all_edges.len();
30        debug_assert_eq!(
31            edges_visited,
32            self.gc_edge_count(),
33            "semantic edge capture must match collector trace edge count"
34        );
35        let mut heap_incoming = HashMap::<GcId, usize>::with_capacity(live_ids.len());
36        for edge in &all_edges {
37            *heap_incoming.entry(edge.to_id).or_default() += 1;
38        }
39        let incoming_sum: usize = heap_incoming.values().sum();
40        debug_assert_eq!(
41            incoming_sum, edges_visited,
42            "Σ heapIncomingEdges must equal edgesVisited"
43        );
44
45        // 2. Trial deletion.
46        self.gc_decref();
47        let candidate_ids = self.sorted_list_ids(GcListKind::Tmp);
48        let candidate_set: HashSet<GcId> = candidate_ids.iter().copied().collect();
49        let mut trial_ref_counts = HashMap::with_capacity(live_ids.len());
50        for &id in &live_ids {
51            let trial_rc = self.header(id).ref_count;
52            let before = ref_count_before[&id];
53            let incoming = heap_incoming.get(&id).copied().unwrap_or(0);
54            debug_assert_eq!(
55                trial_rc,
56                before - incoming as i32,
57                "trialRefCount must equal refCountBefore − heapIncomingEdges"
58            );
59            debug_assert_eq!(
60                candidate_set.contains(&id),
61                trial_rc == 0,
62                "Candidate ⇔ trialRefCount == 0"
63            );
64            trial_ref_counts.insert(id, trial_rc);
65        }
66
67        // 3. Scan + witness forest on the immutable captured graph.
68        self.gc_scan();
69        let garbage_candidate_ids = self.sorted_list_ids(GcListKind::Tmp);
70        let garbage_set: HashSet<GcId> = garbage_candidate_ids.iter().copied().collect();
71        let restored_ids = candidate_ids
72            .iter()
73            .copied()
74            .filter(|id| !garbage_set.contains(id))
75            .collect::<Vec<_>>();
76        debug_assert_eq!(
77            restored_ids.len() + garbage_candidate_ids.len(),
78            candidate_ids.len(),
79            "Candidates = Restored + Garbage candidates"
80        );
81        let restored_objects = summarize_gc_objects(self, &restored_ids);
82        let garbage_candidate_objects = summarize_gc_objects(self, &garbage_candidate_ids);
83        let all_witnesses =
84            self.build_restoration_witnesses(&all_edges, &trial_ref_counts, &restored_ids);
85
86        // Capture labels for every live object before free cycles reclaim garbage.
87        let label_by_id: HashMap<GcId, crate::report::GcObjectSummary> = live_ids
88            .iter()
89            .copied()
90            .map(|id| (id, summarize_gc_object(self, id)))
91            .collect();
92
93        // 4. Free cycles + final fate.
94        let before_free = self.object_ids().len();
95        self.gc_free_cycles();
96        let freed = before_free.saturating_sub(self.object_ids().len());
97        debug_assert_eq!(freed, garbage_candidate_ids.len(), "Objects freed = Garbage candidates");
98
99        let survivor_ids: Vec<GcId> = live_ids
100            .iter()
101            .copied()
102            .filter(|id| !candidate_set.contains(id))
103            .collect();
104
105        let (object_decisions, omitted_object_decisions, selected_decision_ids) =
106            select_object_decisions(
107                &live_ids,
108                &candidate_set,
109                &survivor_ids,
110                &all_witnesses,
111                &ref_count_before,
112                &heap_incoming,
113                &trial_ref_counts,
114                &garbage_set,
115            );
116
117        let (visited_edges, omitted_edge_details) =
118            select_visited_edges(&all_edges, &candidate_set, &all_witnesses);
119
120        let (restoration_witnesses, omitted_witnesses) =
121            select_restoration_witnesses(&all_witnesses, &selected_decision_ids);
122
123        let mut catalog_ids: HashSet<GcId> = HashSet::new();
124        for decision in &object_decisions {
125            catalog_ids.insert(decision.object_id);
126        }
127        for edge in &visited_edges {
128            catalog_ids.insert(edge.from_id);
129            catalog_ids.insert(edge.to_id);
130        }
131        for witness in &restoration_witnesses {
132            catalog_ids.insert(witness.object_id);
133            catalog_ids.insert(witness.root_id);
134            catalog_ids.insert(witness.predecessor_id);
135        }
136        for object in restored_objects
137            .iter()
138            .chain(garbage_candidate_objects.iter())
139        {
140            catalog_ids.insert(object.id);
141        }
142        let mut catalog_ids = catalog_ids.into_iter().collect::<Vec<_>>();
143        catalog_ids.sort_unstable();
144        let objects = catalog_ids
145            .iter()
146            .map(|&id| {
147                label_by_id
148                    .get(&id)
149                    .cloned()
150                    .unwrap_or_else(|| crate::report::GcObjectSummary {
151                        id,
152                        kind: crate::value::ValueKind::Other,
153                        label: format!("Object#{}", id),
154                    })
155            })
156            .collect();
157
158        GcStatsBundle {
159            objects,
160            phases: GcPhaseStats {
161                trial_deletion: TrialDeletionStats {
162                    edges_visited,
163                    candidates: candidate_ids.len(),
164                    object_decisions,
165                    visited_edges,
166                    omitted_object_decisions,
167                    omitted_edge_details,
168                },
169                scan: ScanStats {
170                    restored: restored_objects.len(),
171                    garbage_candidates: garbage_candidate_objects.len(),
172                    restored_objects,
173                    garbage_candidate_objects,
174                    restoration_witnesses,
175                    omitted_witnesses,
176                },
177                free_cycles: FreeCycleStats {
178                    freed,
179                },
180            },
181        }
182    }
183
184    fn capture_visited_edges(&self, live_ids: &[GcId]) -> Vec<VisitedEdge> {
185        let mut edges = Vec::new();
186        for &from_id in live_ids {
187            if let Some(cell) = self.object_downcast::<ValueCell>(from_id) {
188                let mut ordinal = 0usize;
189                cell.value.visit_edges(|relation, target| {
190                    edges.push((
191                        from_id,
192                        ordinal,
193                        VisitedEdge {
194                            from_id,
195                            to_id: target.0,
196                            relation,
197                        },
198                    ));
199                    ordinal += 1;
200                });
201            } else {
202                let object = self.objects[from_id]
203                    .as_ref()
204                    .and_then(|entry| entry.object.as_ref())
205                    .expect("live GC object must have a payload");
206                let mut ordinal = 0usize;
207                object.trace(&mut |to_id| {
208                    edges.push((
209                        from_id,
210                        ordinal,
211                        VisitedEdge {
212                            from_id,
213                            to_id,
214                            relation: EdgeRelation::Unknown,
215                        },
216                    ));
217                    ordinal += 1;
218                });
219            }
220        }
221        // Preserve per-source visit order (already HashKey-/name-sorted) while
222        // ordering sources by fromId. This keeps typed hash key order stable
223        // even though JSON only stores display labels.
224        edges.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
225        edges.into_iter().map(|(_, _, edge)| edge).collect()
226    }
227
228    fn build_restoration_witnesses(
229        &self,
230        edges: &[VisitedEdge],
231        trial_ref_counts: &HashMap<GcId, i32>,
232        restored_ids: &[GcId],
233    ) -> Vec<RestorationWitness> {
234        let mut adjacency: HashMap<GcId, Vec<&VisitedEdge>> = HashMap::new();
235        for edge in edges {
236            adjacency.entry(edge.from_id).or_default().push(edge);
237        }
238        // `edges` is already ordered by (fromId, visit ordinal); keep that order.
239
240        let mut roots: Vec<GcId> = trial_ref_counts
241            .iter()
242            .filter_map(|(&id, &rc)| (rc > 0).then_some(id))
243            .collect();
244        roots.sort_unstable();
245
246        // predecessor[object] = (predecessor_id, relation, root_id)
247        let mut predecessor: HashMap<GcId, (GcId, EdgeRelation, GcId)> = HashMap::new();
248        let mut visited: HashSet<GcId> = HashSet::new();
249        let mut queue: VecDeque<(GcId, GcId)> = VecDeque::new(); // (node, root)
250
251        for root in roots {
252            if visited.insert(root) {
253                queue.push_back((root, root));
254            }
255        }
256
257        while let Some((node, root)) = queue.pop_front() {
258            let Some(outs) = adjacency.get(&node) else {
259                continue;
260            };
261            for edge in outs {
262                if visited.insert(edge.to_id) {
263                    predecessor.insert(edge.to_id, (edge.from_id, edge.relation.clone(), root));
264                    queue.push_back((edge.to_id, root));
265                }
266            }
267        }
268
269        let mut witnesses = Vec::new();
270        for &object_id in restored_ids {
271            if let Some((predecessor_id, relation, root_id)) = predecessor.get(&object_id) {
272                witnesses.push(RestorationWitness {
273                    object_id,
274                    root_id: *root_id,
275                    predecessor_id: *predecessor_id,
276                    relation: relation.clone(),
277                });
278            }
279        }
280        witnesses.sort_by_key(|witness| witness.object_id);
281        witnesses
282    }
283
284    fn sorted_list_ids(&self, kind: GcListKind) -> Vec<GcId> {
285        let current = match kind {
286            GcListKind::GcObj => self.gc_obj_list.head,
287            GcListKind::Tmp => self.tmp_obj_list.head,
288            GcListKind::ZeroRef => self.gc_zero_ref_count_list.head,
289        };
290        let mut ids = GcListIter {
291            rt: self,
292            current,
293        }
294        .collect::<Vec<_>>();
295        ids.sort_unstable();
296        ids
297    }
298
299    fn gc_edge_count(&self) -> usize {
300        let mut count = 0;
301        for id in (GcListIter {
302            rt: self,
303            current: self.gc_obj_list.head,
304        }) {
305            let object = self.objects[id]
306                .as_ref()
307                .and_then(|entry| entry.object.as_ref())
308                .expect("live GC object must have a payload");
309            object.trace(&mut |_| count += 1);
310        }
311        count
312    }
313}
314
315fn edge_priority(
316    edge: &VisitedEdge,
317    candidates: &HashSet<GcId>,
318    witness_edges: &[(GcId, GcId, EdgeRelation)],
319) -> u8 {
320    if witness_edges.iter().any(|(from, to, relation)| {
321        *from == edge.from_id && *to == edge.to_id && *relation == edge.relation
322    }) {
323        return 0;
324    }
325    let from_c = candidates.contains(&edge.from_id);
326    let to_c = candidates.contains(&edge.to_id);
327    match (from_c, to_c) {
328        (true, true) => 1,
329        (false, true) => 2,
330        (true, false) => 3,
331        (false, false) => 4,
332    }
333}
334
335fn select_visited_edges(
336    all_edges: &[VisitedEdge],
337    candidates: &HashSet<GcId>,
338    witnesses: &[RestorationWitness],
339) -> (Vec<VisitedEdge>, usize) {
340    let witness_edges: Vec<(GcId, GcId, EdgeRelation)> = witnesses
341        .iter()
342        .map(|witness| (witness.predecessor_id, witness.object_id, witness.relation.clone()))
343        .collect();
344
345    let mut ranked = all_edges
346        .iter()
347        .enumerate()
348        .map(|(index, edge)| (edge_priority(edge, candidates, &witness_edges), index))
349        .collect::<Vec<_>>();
350    ranked.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
351
352    let selected_indices: HashSet<usize> = ranked
353        .into_iter()
354        .take(MAX_EDGE_DETAILS)
355        .map(|(_, index)| index)
356        .collect();
357    let kept = all_edges
358        .iter()
359        .enumerate()
360        .filter(|&(index, _edge)| selected_indices.contains(&index))
361        .map(|(_index, edge)| edge.clone())
362        .collect::<Vec<_>>();
363    let omitted = all_edges.len().saturating_sub(kept.len());
364    (kept, omitted)
365}
366
367fn witness_chain_ids(
368    witness: &RestorationWitness,
369    by_object: &HashMap<GcId, &RestorationWitness>,
370) -> Option<Vec<GcId>> {
371    let mut ids = vec![witness.object_id, witness.root_id, witness.predecessor_id];
372    let mut current = witness.object_id;
373    let mut seen = HashSet::new();
374    seen.insert(current);
375
376    while let Some(entry) = by_object.get(&current) {
377        if entry.predecessor_id == entry.root_id {
378            break;
379        }
380        if !seen.insert(entry.predecessor_id) {
381            return None;
382        }
383        ids.push(entry.predecessor_id);
384        current = entry.predecessor_id;
385        if current == witness.root_id {
386            break;
387        }
388        if !by_object.contains_key(&current) {
389            break;
390        }
391    }
392
393    ids.sort_unstable();
394    ids.dedup();
395    Some(ids)
396}
397
398#[allow(clippy::too_many_arguments)]
399fn select_object_decisions(
400    live_ids: &[GcId],
401    candidates: &HashSet<GcId>,
402    survivor_ids: &[GcId],
403    witnesses: &[RestorationWitness],
404    ref_count_before: &HashMap<GcId, i32>,
405    heap_incoming: &HashMap<GcId, usize>,
406    trial_ref_counts: &HashMap<GcId, i32>,
407    garbage_set: &HashSet<GcId>,
408) -> (Vec<ObjectDecision>, usize, HashSet<GcId>) {
409    let witness_by_object: HashMap<GcId, &RestorationWitness> = witnesses
410        .iter()
411        .map(|witness| (witness.object_id, witness))
412        .collect();
413
414    let mut witness_survivor_ids = HashSet::new();
415    for witness in witnesses {
416        if let Some(ids) = witness_chain_ids(witness, &witness_by_object) {
417            for id in ids {
418                if !candidates.contains(&id) {
419                    witness_survivor_ids.insert(id);
420                }
421            }
422        }
423        witness_survivor_ids.insert(witness.root_id);
424    }
425
426    let mut candidate_ids: Vec<GcId> = candidates.iter().copied().collect();
427    candidate_ids.sort_unstable();
428    let mut witness_survivors: Vec<GcId> = witness_survivor_ids.iter().copied().collect();
429    witness_survivors.sort_unstable();
430    let mut other_survivors: Vec<GcId> = survivor_ids
431        .iter()
432        .copied()
433        .filter(|id| !witness_survivor_ids.contains(id))
434        .collect();
435    other_survivors.sort_unstable();
436
437    let mut ordered = Vec::new();
438    ordered.extend(candidate_ids);
439    ordered.extend(witness_survivors);
440    ordered.extend(other_survivors);
441    for &id in live_ids {
442        if !ordered.contains(&id) {
443            ordered.push(id);
444        }
445    }
446
447    let selected: Vec<GcId> = ordered.into_iter().take(MAX_OBJECT_DECISIONS).collect();
448    let selected_set: HashSet<GcId> = selected.iter().copied().collect();
449    let omitted = live_ids.len().saturating_sub(selected.len());
450
451    let mut decisions = selected
452        .iter()
453        .map(|&object_id| {
454            let trial_ref_count = trial_ref_counts[&object_id];
455            let decision = if trial_ref_count == 0 {
456                TrialDecision::Candidate
457            } else {
458                TrialDecision::Survivor
459            };
460            let final_fate =
461                if decision == TrialDecision::Candidate && garbage_set.contains(&object_id) {
462                    FinalFate::Freed
463                } else {
464                    FinalFate::Retained
465                };
466            ObjectDecision {
467                object_id,
468                ref_count_before: ref_count_before[&object_id],
469                heap_incoming_edges: heap_incoming.get(&object_id).copied().unwrap_or(0),
470                trial_ref_count,
471                decision,
472                final_fate,
473            }
474        })
475        .collect::<Vec<_>>();
476    decisions.sort_by_key(|decision| decision.object_id);
477    (decisions, omitted, selected_set)
478}
479
480fn select_restoration_witnesses(
481    all_witnesses: &[RestorationWitness],
482    selected_decision_ids: &HashSet<GcId>,
483) -> (Vec<RestorationWitness>, usize) {
484    let by_object: HashMap<GcId, &RestorationWitness> = all_witnesses
485        .iter()
486        .map(|witness| (witness.object_id, witness))
487        .collect();
488
489    let mut kept = Vec::new();
490    for witness in all_witnesses {
491        if kept.len() >= MAX_RESTORATION_WITNESSES {
492            break;
493        }
494        let Some(chain) = witness_chain_ids(witness, &by_object) else {
495            continue;
496        };
497        if chain.iter().any(|id| !selected_decision_ids.contains(id)) {
498            continue;
499        }
500        kept.push(witness.clone());
501    }
502    let omitted = all_witnesses.len().saturating_sub(kept.len());
503    (kept, omitted)
504}