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#[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#[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
258pub(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}