Skip to main content

gc/
report.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, HashSet};
3
4use serde::Serialize;
5
6use crate::value::{Value, ValueCell, ValueKind};
7use crate::{GcHeap, GcId, GcObjectType, GcRef, GcRuntime};
8
9pub type ValueKindCounts = BTreeMap<ValueKind, usize>;
10
11pub const MAX_EDGE_DETAILS: usize = 500;
12pub const MAX_OBJECT_DECISIONS: usize = 500;
13pub const MAX_RESTORATION_WITNESSES: usize = 500;
14pub const MAX_GLOBAL_ROOTS: usize = 500;
15
16pub use crate::value::{EdgeRelation, HashKeyKind};
17
18const VALUE_KINDS: [ValueKind; 14] = [
19    ValueKind::Class,
20    ValueKind::Instance,
21    ValueKind::BoundMethod,
22    ValueKind::Closure,
23    ValueKind::Array,
24    ValueKind::Hash,
25    ValueKind::Integer,
26    ValueKind::Boolean,
27    ValueKind::String,
28    ValueKind::Null,
29    ValueKind::Error,
30    ValueKind::CompiledFunction,
31    ValueKind::Builtin,
32    ValueKind::Other,
33];
34
35#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct HeapSnapshot {
38    pub object_count: usize,
39    pub tracked_bytes: usize,
40    pub by_value_kind: ValueKindCounts,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub enum TrialDecision {
46    Candidate,
47    Survivor,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum FinalFate {
53    Retained,
54    Freed,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct ObjectDecision {
60    pub object_id: GcId,
61    pub ref_count_before: i32,
62    pub heap_incoming_edges: usize,
63    pub trial_ref_count: i32,
64    pub decision: TrialDecision,
65    #[serde(rename = "final")]
66    pub final_fate: FinalFate,
67}
68
69impl EdgeRelation {
70    pub fn kind_rank(&self) -> u8 {
71        match self {
72            EdgeRelation::ArrayElement {
73                ..
74            } => 0,
75            EdgeRelation::HashValue {
76                ..
77            } => 1,
78            EdgeRelation::ClosureFunction => 2,
79            EdgeRelation::ClosureFree {
80                ..
81            } => 3,
82            EdgeRelation::ClassConstructor => 4,
83            EdgeRelation::ClassMethod {
84                ..
85            } => 5,
86            EdgeRelation::InstanceClass => 6,
87            EdgeRelation::InstanceField {
88                ..
89            } => 7,
90            EdgeRelation::BoundMethodReceiver => 8,
91            EdgeRelation::BoundMethodFunction => 9,
92            EdgeRelation::Unknown => 10,
93        }
94    }
95
96    pub fn sort_payload(&self) -> RelationSortKey<'_> {
97        match self {
98            EdgeRelation::ArrayElement {
99                index,
100            } => RelationSortKey::Index(*index),
101            EdgeRelation::HashValue {
102                key_kind,
103                key,
104            } => RelationSortKey::HashKey(*key_kind, key),
105            EdgeRelation::ClosureFunction => RelationSortKey::None,
106            EdgeRelation::ClosureFree {
107                index,
108            } => RelationSortKey::Index(*index),
109            EdgeRelation::ClassConstructor => RelationSortKey::None,
110            EdgeRelation::ClassMethod {
111                name,
112            } => RelationSortKey::Name(name),
113            EdgeRelation::InstanceClass => RelationSortKey::None,
114            EdgeRelation::InstanceField {
115                name,
116            } => RelationSortKey::Name(name),
117            EdgeRelation::BoundMethodReceiver => RelationSortKey::None,
118            EdgeRelation::BoundMethodFunction => RelationSortKey::None,
119            EdgeRelation::Unknown => RelationSortKey::None,
120        }
121    }
122}
123
124#[derive(Eq, PartialEq)]
125pub enum RelationSortKey<'a> {
126    None,
127    Index(usize),
128    HashKey(HashKeyKind, &'a str),
129    Name(&'a str),
130}
131
132impl Ord for RelationSortKey<'_> {
133    fn cmp(&self, other: &Self) -> Ordering {
134        match (self, other) {
135            (RelationSortKey::None, RelationSortKey::None) => Ordering::Equal,
136            (RelationSortKey::None, _) => Ordering::Less,
137            (_, RelationSortKey::None) => Ordering::Greater,
138            (RelationSortKey::Index(a), RelationSortKey::Index(b)) => a.cmp(b),
139            (RelationSortKey::Index(_), _) => Ordering::Less,
140            (_, RelationSortKey::Index(_)) => Ordering::Greater,
141            (RelationSortKey::HashKey(a_kind, a), RelationSortKey::HashKey(b_kind, b)) => {
142                a_kind.cmp(b_kind).then(a.cmp(b))
143            }
144            (RelationSortKey::HashKey(_, _), RelationSortKey::Name(_)) => Ordering::Less,
145            (RelationSortKey::Name(_), RelationSortKey::HashKey(_, _)) => Ordering::Greater,
146            (RelationSortKey::Name(a), RelationSortKey::Name(b)) => a.cmp(b),
147        }
148    }
149}
150
151impl PartialOrd for RelationSortKey<'_> {
152    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
153        Some(self.cmp(other))
154    }
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct VisitedEdge {
160    pub from_id: GcId,
161    pub to_id: GcId,
162    pub relation: EdgeRelation,
163}
164
165impl VisitedEdge {
166    pub fn sort_key(&self) -> (GcId, u8, RelationSortKey<'_>, GcId) {
167        (self.from_id, self.relation.kind_rank(), self.relation.sort_payload(), self.to_id)
168    }
169}
170
171#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct RestorationWitness {
174    pub object_id: GcId,
175    pub root_id: GcId,
176    pub predecessor_id: GcId,
177    pub relation: EdgeRelation,
178}
179
180#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
181#[serde(rename_all = "camelCase")]
182pub struct TrialDeletionStats {
183    pub edges_visited: usize,
184    pub candidates: usize,
185    pub object_decisions: Vec<ObjectDecision>,
186    pub visited_edges: Vec<VisitedEdge>,
187    pub omitted_object_decisions: usize,
188    pub omitted_edge_details: usize,
189}
190
191#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct ScanStats {
194    pub restored: usize,
195    pub garbage_candidates: usize,
196    pub restored_objects: Vec<GcObjectSummary>,
197    pub garbage_candidate_objects: Vec<GcObjectSummary>,
198    pub restoration_witnesses: Vec<RestorationWitness>,
199    pub omitted_witnesses: usize,
200}
201
202#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
203#[serde(rename_all = "camelCase")]
204pub struct GcObjectSummary {
205    pub id: GcId,
206    pub kind: ValueKind,
207    pub label: String,
208}
209
210#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
211#[serde(rename_all = "camelCase")]
212pub struct FreeCycleStats {
213    pub freed: usize,
214}
215
216#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
217#[serde(rename_all = "camelCase")]
218pub struct GcPhaseStats {
219    pub trial_deletion: TrialDeletionStats,
220    pub scan: ScanStats,
221    pub free_cycles: FreeCycleStats,
222}
223
224/// Diagnostics returned by `run_gc_with_stats_bundle`: object catalog plus phase stats.
225#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct GcStatsBundle {
228    pub objects: Vec<GcObjectSummary>,
229    pub phases: GcPhaseStats,
230}
231
232/// A global variable name and the object its slot references at report time.
233/// This is a present-tense fact read from the VM's global table — the defined
234/// root set, not a guess about aliases.
235#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
236#[serde(rename_all = "camelCase")]
237pub struct GlobalRoot {
238    pub name: String,
239    pub object_id: GcId,
240}
241
242#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
243#[serde(rename_all = "camelCase")]
244pub struct GcCollectionReport {
245    pub before: HeapSnapshot,
246    pub after: HeapSnapshot,
247    pub objects: Vec<GcObjectSummary>,
248    pub global_roots: Vec<GlobalRoot>,
249    pub omitted_global_roots: usize,
250    pub phases: GcPhaseStats,
251    pub collected_by_value_kind: ValueKindCounts,
252}
253
254pub(crate) fn empty_value_kind_counts() -> ValueKindCounts {
255    VALUE_KINDS.iter().copied().map(|kind| (kind, 0)).collect()
256}
257
258/// Cap the root list at `MAX_GLOBAL_ROOTS`. Roots whose object the catalog
259/// already explains are kept first, so names attach to rows and graph nodes
260/// the reader can actually see; slot order is preserved within the kept set.
261pub(crate) fn select_global_roots(
262    roots: Vec<GlobalRoot>,
263    cataloged: &HashSet<GcId>,
264) -> (Vec<GlobalRoot>, usize) {
265    if roots.len() <= MAX_GLOBAL_ROOTS {
266        return (roots, 0);
267    }
268    let omitted = roots.len() - MAX_GLOBAL_ROOTS;
269    let mut keep = vec![false; roots.len()];
270    let mut kept = 0usize;
271    for want_cataloged in [true, false] {
272        for (index, root) in roots.iter().enumerate() {
273            if kept == MAX_GLOBAL_ROOTS {
274                break;
275            }
276            if !keep[index] && cataloged.contains(&root.object_id) == want_cataloged {
277                keep[index] = true;
278                kept += 1;
279            }
280        }
281    }
282    let selected = roots
283        .into_iter()
284        .zip(keep)
285        .filter_map(|(root, keep_root)| keep_root.then_some(root))
286        .collect();
287    (selected, omitted)
288}
289
290pub(crate) fn summarize_gc_objects(runtime: &GcRuntime, ids: &[GcId]) -> Vec<GcObjectSummary> {
291    ids.iter()
292        .copied()
293        .map(|id| summarize_gc_object(runtime, id))
294        .collect()
295}
296
297pub(crate) fn summarize_gc_object(runtime: &GcRuntime, id: GcId) -> GcObjectSummary {
298    let Some(cell) = runtime.object_downcast::<ValueCell>(id) else {
299        let name = match runtime.header(id).gc_obj_type {
300            GcObjectType::MonkeyObject => "Object",
301            GcObjectType::FunctionBytecode => "FunctionBytecode",
302            GcObjectType::Shape => "Shape",
303            GcObjectType::VarRef => "VarRef",
304            GcObjectType::AsyncFunction => "AsyncFunction",
305            GcObjectType::MonkeyContext => "MonkeyContext",
306        };
307        return GcObjectSummary {
308            id,
309            kind: ValueKind::Other,
310            label: format!("{}#{}", name, id),
311        };
312    };
313
314    let kind = cell.value.kind();
315    let name = match &cell.value {
316        Value::Class(class) => format!("Class({})", class.name),
317        Value::Instance(instance) => {
318            format!("Instance({})", class_name(runtime, instance.class))
319        }
320        Value::BoundMethod(method) => format!(
321            "BoundMethod({}.{})",
322            instance_class_name(runtime, method.receiver),
323            method.name
324        ),
325        Value::Closure(closure) => closure_name(runtime, closure.func)
326            .map(|name| format!("Closure({})", name))
327            .unwrap_or_else(|| "Closure".to_string()),
328        Value::Array(_) => "Array".to_string(),
329        Value::Hash(_) => "Hash".to_string(),
330        Value::Integer(_) => "Integer".to_string(),
331        Value::Boolean(_) => "Boolean".to_string(),
332        Value::String(_) => "String".to_string(),
333        Value::Null => "Null".to_string(),
334        Value::Error(_) => "Error".to_string(),
335        Value::CompiledFunction(_) => "CompiledFunction".to_string(),
336        Value::Builtin(_) => "Builtin".to_string(),
337    };
338
339    GcObjectSummary {
340        id,
341        kind,
342        label: format!("{}#{}", name, id),
343    }
344}
345
346fn class_name(runtime: &GcRuntime, reference: GcRef) -> &str {
347    runtime
348        .object_downcast::<ValueCell>(reference.0)
349        .and_then(|cell| match &cell.value {
350            Value::Class(class) => Some(class.name.as_str()),
351            _ => None,
352        })
353        .unwrap_or("<unknown>")
354}
355
356fn closure_name(runtime: &GcRuntime, reference: GcRef) -> Option<&str> {
357    runtime
358        .object_downcast::<ValueCell>(reference.0)
359        .and_then(|cell| match &cell.value {
360            Value::CompiledFunction(function) if !function.name.is_empty() => {
361                Some(function.name.as_str())
362            }
363            _ => None,
364        })
365}
366
367fn instance_class_name(runtime: &GcRuntime, reference: GcRef) -> &str {
368    runtime
369        .object_downcast::<ValueCell>(reference.0)
370        .and_then(|cell| match &cell.value {
371            Value::Instance(instance) => Some(class_name(runtime, instance.class)),
372            _ => None,
373        })
374        .unwrap_or("<unknown>")
375}
376
377impl GcHeap {
378    pub fn snapshot(&self) -> HeapSnapshot {
379        let mut by_value_kind = empty_value_kind_counts();
380        for kind in self.value_kinds_by_id().values() {
381            *by_value_kind.entry(*kind).or_default() += 1;
382        }
383        HeapSnapshot {
384            object_count: self.runtime().gc_object_count(),
385            tracked_bytes: self.malloc_state().malloc_size,
386            by_value_kind,
387        }
388    }
389
390    pub(crate) fn value_kinds_by_id(&self) -> BTreeMap<GcId, ValueKind> {
391        self.runtime()
392            .object_ids()
393            .into_iter()
394            .filter_map(|id| {
395                self.runtime()
396                    .object_downcast::<ValueCell>(id)
397                    .map(|cell| (id, cell.value.kind()))
398            })
399            .collect()
400    }
401}