Skip to main content

gc/
gc_runtime.rs

1use crate::header::{
2    GcId, GcListKind, GcObjectHeader, GcObjectType, GcPhase, RefCountHeader, RefCountId,
3};
4use crate::list::{GcList, GcListIter};
5use crate::malloc::{MallocState, DEFAULT_GC_THRESHOLD};
6use crate::report::GcPhaseStats;
7use std::any::Any;
8
9// Diagnostics behind `run_gc_with_stats` live in gc_stats.rs, a child module
10// so it can read the runtime's private object table and GC lists.
11#[path = "gc_stats.rs"]
12mod stats;
13
14/// Child-mark callback mode used during the three GC phases.
15/// Matches QuickJS `gc_decref_child`, `gc_scan_incref_child`, `gc_scan_incref_child2`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum MarkFunc {
18    Decref,      // Phase 1 trial deletion: subtract each heap edge from the child's ref count.
19    ScanIncref,  // Phase 2: give the edge back and rescue a child whose refs return above zero.
20    ScanIncref2, // Phase 2: give the edge back to remaining candidates without rescuing them.
21}
22
23/// Trait for objects stored in the GC heap. Implementors expose graph edges via `trace`.
24pub trait GcObject: Any {
25    fn trace(&self, visit: &mut dyn FnMut(GcId));
26    fn on_free(&mut self, _rt: &mut GcRuntime) {}
27}
28
29struct GcObjectEntry {
30    header: GcObjectHeader,
31    object: Option<Box<dyn GcObject>>,
32}
33
34struct RefCountEntry {
35    header: RefCountHeader,
36    payload: Box<dyn FnOnce(&mut GcRuntime)>,
37}
38
39/// QuickJS-style runtime heap: reference counting + three-phase cycle removal.
40pub struct GcRuntime {
41    objects: Vec<Option<GcObjectEntry>>,
42    free_slots: Vec<GcId>,
43    ref_counts: Vec<Option<RefCountEntry>>,
44    ref_count_free_slots: Vec<RefCountId>,
45
46    gc_obj_list: GcList,
47    gc_zero_ref_count_list: GcList,
48    tmp_obj_list: GcList,
49    gc_phase: GcPhase,
50    malloc_state: MallocState,
51    malloc_gc_threshold: usize,
52}
53
54impl GcRuntime {
55    pub fn new() -> Self {
56        GcRuntime {
57            objects: Vec::new(),
58            free_slots: Vec::new(),
59            ref_counts: Vec::new(),
60            ref_count_free_slots: Vec::new(),
61            gc_obj_list: GcList::new(),
62            gc_zero_ref_count_list: GcList::new(),
63            tmp_obj_list: GcList::new(),
64            gc_phase: GcPhase::None,
65            malloc_state: MallocState::new(),
66            malloc_gc_threshold: DEFAULT_GC_THRESHOLD,
67        }
68    }
69
70    /// Run the three-phase cycle collector. Matches `JS_RunGC`.
71    pub fn run_gc(&mut self) {
72        self.gc_decref();
73        self.gc_scan();
74        self.gc_free_cycles();
75    }
76
77    /// Phase 1: trial deletion.
78    fn gc_decref(&mut self) {
79        assert!(
80            self.tmp_obj_list.is_empty(),
81            "temporary GC list must be empty before trial deletion"
82        );
83        let mut current = self.gc_obj_list.head;
84        while let Some(id) = current {
85            let next = self.header(id).list_next;
86            debug_assert_eq!(self.header(id).mark, 0);
87            self.mark_children(id, MarkFunc::Decref);
88            let header = self.header_mut(id);
89            header.mark = 1;
90            if header.ref_count == 0 {
91                self.list_move(GcListKind::GcObj, GcListKind::Tmp, id);
92            }
93            current = next;
94        }
95    }
96
97    /// Phase 2: restore live refs.
98    fn gc_scan(&mut self) {
99        let mut current = self.gc_obj_list.head;
100        while let Some(id) = current {
101            let header = self.header_mut(id);
102            debug_assert!(header.ref_count > 0);
103            header.mark = 0;
104            self.mark_children(id, MarkFunc::ScanIncref);
105            current = self.header(id).list_next;
106        }
107
108        let mut current = self.tmp_obj_list.head;
109        while let Some(id) = current {
110            let next = self.header(id).list_next;
111            self.mark_children(id, MarkFunc::ScanIncref2);
112            current = next;
113        }
114    }
115
116    /// Phase 3: free cyclic garbage.
117    fn gc_free_cycles(&mut self) {
118        self.gc_phase = GcPhase::RemoveCycles;
119
120        loop {
121            let id = self.tmp_obj_list.head;
122            if id.is_none() {
123                break;
124            }
125            let id = id.unwrap();
126            let gc_obj_type = self.header(id).gc_obj_type;
127            match gc_obj_type {
128                GcObjectType::MonkeyObject | GcObjectType::FunctionBytecode => {
129                    self.free_gc_object(id);
130                }
131                _ => {
132                    self.list_move(GcListKind::Tmp, GcListKind::ZeroRef, id);
133                }
134            }
135        }
136
137        self.gc_phase = GcPhase::None;
138
139        while let Some(id) = self.gc_zero_ref_count_list.head {
140            let ty = self.header(id).gc_obj_type;
141            debug_assert!(
142                ty == GcObjectType::MonkeyObject || ty == GcObjectType::FunctionBytecode,
143                "unexpected deferred type: {:?}",
144                ty
145            );
146            self.list_remove_current(id);
147            self.free_slot(id);
148        }
149    }
150
151    pub fn malloc_state(&self) -> &MallocState {
152        &self.malloc_state
153    }
154
155    pub fn malloc_state_mut(&mut self) -> &mut MallocState {
156        &mut self.malloc_state
157    }
158
159    pub fn gc_threshold(&self) -> usize {
160        self.malloc_gc_threshold
161    }
162
163    /// `threshold == usize::MAX` disables automatic GC, matching `JS_SetGCThreshold(rt, -1)`.
164    pub fn set_gc_threshold(&mut self, threshold: usize) {
165        self.malloc_gc_threshold = threshold;
166    }
167
168    pub fn gc_phase(&self) -> GcPhase {
169        self.gc_phase
170    }
171
172    pub fn gc_object_count(&self) -> usize {
173        GcListIter {
174            rt: self,
175            current: self.gc_obj_list.head,
176        }
177        .count()
178    }
179
180    pub fn object_ids(&self) -> Vec<GcId> {
181        self.objects
182            .iter()
183            .enumerate()
184            .filter_map(|(id, entry)| entry.as_ref().map(|_| id))
185            .collect()
186    }
187
188    pub(crate) fn header(&self, id: GcId) -> &GcObjectHeader {
189        &self.objects[id].as_ref().expect("invalid GcId").header
190    }
191
192    pub(crate) fn header_mut(&mut self, id: GcId) -> &mut GcObjectHeader {
193        &mut self.objects[id].as_mut().expect("invalid GcId").header
194    }
195
196    fn list_ptr(&mut self, kind: GcListKind) -> *mut GcList {
197        (match kind {
198            GcListKind::GcObj => &mut self.gc_obj_list,
199            GcListKind::Tmp => &mut self.tmp_obj_list,
200            GcListKind::ZeroRef => &mut self.gc_zero_ref_count_list,
201        }) as *mut GcList
202    }
203
204    fn list_push_back(&mut self, kind: GcListKind, id: GcId) {
205        let list_ptr = self.list_ptr(kind);
206        let tail = unsafe { (*list_ptr).tail };
207
208        {
209            let header = self.header_mut(id);
210            debug_assert!(
211                header.list_kind.is_none(),
212                "object already belongs to a GC list: {:?}",
213                header.list_kind
214            );
215            header.list_kind = Some(kind);
216            header.list_prev = tail;
217            header.list_next = None;
218        }
219
220        if let Some(tail_id) = tail {
221            self.header_mut(tail_id).list_next = Some(id);
222        } else {
223            unsafe {
224                (*list_ptr).head = Some(id);
225            }
226        }
227        unsafe {
228            (*list_ptr).tail = Some(id);
229        }
230    }
231
232    fn list_remove(&mut self, kind: GcListKind, id: GcId) {
233        let list_ptr = self.list_ptr(kind);
234        let (prev, next) = {
235            let header = self.header(id);
236            debug_assert_eq!(header.list_kind, Some(kind), "object is not on the expected GC list");
237            (header.list_prev, header.list_next)
238        };
239
240        match prev {
241            Some(p) => self.header_mut(p).list_next = next,
242            None => unsafe {
243                (*list_ptr).head = next;
244            },
245        }
246
247        match next {
248            Some(n) => self.header_mut(n).list_prev = prev,
249            None => unsafe {
250                (*list_ptr).tail = prev;
251            },
252        }
253
254        let header = self.header_mut(id);
255        header.list_kind = None;
256        header.list_prev = None;
257        header.list_next = None;
258    }
259
260    fn list_remove_current(&mut self, id: GcId) {
261        let kind = self
262            .header(id)
263            .list_kind
264            .expect("object is not on a GC list");
265        self.list_remove(kind, id);
266    }
267
268    fn list_move(&mut self, from: GcListKind, to: GcListKind, id: GcId) {
269        self.list_remove(from, id);
270        self.list_push_back(to, id);
271    }
272
273    fn list_move_current_to(&mut self, to: GcListKind, id: GcId) {
274        let from = self
275            .header(id)
276            .list_kind
277            .expect("object is not on a GC list");
278        self.list_move(from, to, id);
279    }
280
281    fn alloc_slot(&mut self, entry: GcObjectEntry) -> GcId {
282        let id = if let Some(id) = self.free_slots.pop() {
283            self.objects[id] = Some(entry);
284            id
285        } else {
286            let id = self.objects.len();
287            self.objects.push(Some(entry));
288            id
289        };
290        self.malloc_state
291            .record_alloc(std::mem::size_of::<GcObjectEntry>());
292        id
293    }
294
295    fn free_slot(&mut self, id: GcId) {
296        self.malloc_state
297            .record_free(std::mem::size_of::<GcObjectEntry>());
298        self.objects[id] = None;
299        self.free_slots.push(id);
300    }
301
302    /// Register a new GC object with `ref_count = 1` on `gc_obj_list`.
303    /// Matches QuickJS `add_gc_object`.
304    pub fn add_gc_object(&mut self, object: Box<dyn GcObject>, gc_obj_type: GcObjectType) -> GcId {
305        let id = self.alloc_slot(GcObjectEntry {
306            header: GcObjectHeader::new(gc_obj_type, 1),
307            object: Some(object),
308        });
309        self.list_push_back(GcListKind::GcObj, id);
310        id
311    }
312
313    /// Increment refcount. Matches QuickJS `js_dup` for GC objects.
314    pub fn dup_gc(&mut self, id: GcId) -> GcId {
315        self.header_mut(id).ref_count += 1;
316        id
317    }
318
319    /// Decrement refcount and free when it reaches zero.
320    /// Matches QuickJS `JS_FreeValueRT` for GC objects.
321    pub fn free_gc(&mut self, id: GcId) {
322        let ref_count = match self.objects.get_mut(id).and_then(|slot| slot.as_mut()) {
323            Some(entry) => {
324                entry.header.ref_count -= 1;
325                entry.header.ref_count
326            }
327            None => return,
328        };
329
330        if ref_count > 0 {
331            return;
332        }
333
334        if self.gc_phase != GcPhase::RemoveCycles {
335            self.list_move(GcListKind::GcObj, GcListKind::ZeroRef, id);
336            if self.gc_phase == GcPhase::None {
337                self.free_zero_refcount();
338            }
339        }
340    }
341
342    /// Allocate a simple refcounted payload (strings, etc.). Not cycle-collected.
343    pub fn add_ref_counted<F>(&mut self, on_free: F) -> RefCountId
344    where
345        F: FnOnce(&mut GcRuntime) + 'static,
346    {
347        let entry = RefCountEntry {
348            header: RefCountHeader::new(1),
349            payload: Box::new(on_free),
350        };
351        let id = if let Some(id) = self.ref_count_free_slots.pop() {
352            self.ref_counts[id] = Some(entry);
353            id
354        } else {
355            let id = self.ref_counts.len();
356            self.ref_counts.push(Some(entry));
357            id
358        };
359        self.malloc_state
360            .record_alloc(std::mem::size_of::<RefCountEntry>());
361        id
362    }
363
364    pub fn dup_ref_counted(&mut self, id: RefCountId) -> RefCountId {
365        self.ref_counts[id]
366            .as_mut()
367            .expect("invalid RefCountId")
368            .header
369            .ref_count += 1;
370        id
371    }
372
373    pub fn free_ref_counted(&mut self, id: RefCountId) {
374        let ref_count = {
375            let entry = self.ref_counts[id].as_mut().expect("invalid RefCountId");
376            entry.header.ref_count -= 1;
377            entry.header.ref_count
378        };
379
380        if ref_count <= 0 {
381            let entry = self.ref_counts[id].take().expect("double free");
382            self.malloc_state
383                .record_free(std::mem::size_of::<RefCountEntry>());
384            (entry.payload)(self);
385            self.ref_count_free_slots.push(id);
386        }
387    }
388
389    /// Mark a GC object header during traversal. Matches `JS_MarkValue` for object tags.
390    pub fn mark_gc_header(&mut self, id: GcId, mark_func: MarkFunc) {
391        match mark_func {
392            MarkFunc::Decref => self.gc_decref_child(id),
393            MarkFunc::ScanIncref => self.gc_scan_incref_child(id),
394            MarkFunc::ScanIncref2 => self.gc_scan_incref_child2(id),
395        }
396    }
397
398    /// Traverse children of a GC object. Matches QuickJS `mark_children`.
399    pub fn mark_children(&mut self, id: GcId, mark_func: MarkFunc) {
400        // Move the box out temporarily so `trace` can call back into the runtime without
401        // materializing a child-id buffer.
402        let object = self.objects[id]
403            .as_mut()
404            .expect("invalid GcId")
405            .object
406            .take()
407            .expect("object already finalized");
408        object.trace(&mut |child| {
409            self.mark_gc_header(child, mark_func);
410        });
411        self.objects[id]
412            .as_mut()
413            .expect("object freed while tracing")
414            .object = Some(object);
415    }
416
417    /// Maybe run GC when tracked malloc exceeds threshold. Matches `js_trigger_gc`.
418    pub fn trigger_gc(&mut self, alloc_size: usize) {
419        let force_gc =
420            self.malloc_state.malloc_size.saturating_add(alloc_size) > self.malloc_gc_threshold;
421        if force_gc {
422            self.run_gc();
423            self.malloc_gc_threshold =
424                self.malloc_state.malloc_size + (self.malloc_state.malloc_size >> 1);
425        }
426    }
427
428    /// Run all collector phases atomically and return per-phase diagnostics.
429    pub fn run_gc_with_stats(&mut self) -> GcPhaseStats {
430        self.run_gc_with_stats_bundle().phases
431    }
432
433    /// Return false if the object has been freed during cycle collection.
434    /// Matches `JS_IsLiveObject`.
435    pub fn is_live_object(&self, id: GcId) -> bool {
436        self.objects
437            .get(id)
438            .and_then(|o| o.as_ref())
439            .is_some_and(|e| !e.header.free_mark)
440    }
441
442    pub fn ref_count(&self, id: GcId) -> i32 {
443        self.header(id).ref_count
444    }
445
446    pub fn object_exists(&self, id: GcId) -> bool {
447        self.objects.get(id).and_then(|o| o.as_ref()).is_some()
448    }
449
450    pub fn object_downcast<T: GcObject>(&self, id: GcId) -> Option<&T> {
451        let entry = self.objects.get(id)?.as_ref()?;
452        (entry.object.as_ref()?.as_ref() as &dyn Any).downcast_ref()
453    }
454
455    pub fn object_downcast_mut<T: GcObject>(&mut self, id: GcId) -> Option<&mut T> {
456        let entry = self.objects.get_mut(id)?.as_mut()?;
457        (entry.object.as_mut()?.as_mut() as &mut dyn Any).downcast_mut()
458    }
459
460    // --- Phase 1: trial deletion ---
461
462    fn gc_decref_child(&mut self, id: GcId) {
463        let header = self.header_mut(id);
464        debug_assert!(header.ref_count > 0);
465        header.ref_count -= 1;
466        if header.ref_count == 0 && header.mark == 1 {
467            self.list_move(GcListKind::GcObj, GcListKind::Tmp, id);
468        }
469    }
470
471    // --- Phase 2: restore live refs ---
472
473    fn gc_scan_incref_child(&mut self, id: GcId) {
474        let header = self.header_mut(id);
475        header.ref_count += 1;
476        if header.ref_count == 1 {
477            self.list_move(GcListKind::Tmp, GcListKind::GcObj, id);
478            self.header_mut(id).mark = 0;
479        }
480    }
481
482    fn gc_scan_incref_child2(&mut self, id: GcId) {
483        self.header_mut(id).ref_count += 1;
484    }
485
486    // --- Phase 3: free cyclic garbage ---
487
488    fn free_zero_refcount(&mut self) {
489        self.gc_phase = GcPhase::Decref;
490        loop {
491            let id = self.gc_zero_ref_count_list.head;
492            if id.is_none() {
493                break;
494            }
495            let id = id.unwrap();
496            debug_assert_eq!(self.header(id).ref_count, 0);
497            self.free_gc_object(id);
498        }
499        self.gc_phase = GcPhase::None;
500    }
501
502    fn free_gc_object(&mut self, id: GcId) {
503        match self.header(id).gc_obj_type {
504            GcObjectType::MonkeyObject | GcObjectType::FunctionBytecode => {
505                self.free_heap_object(id)
506            }
507            other => panic!("free_gc_object: unsupported type {:?}", other),
508        }
509    }
510
511    fn free_heap_object(&mut self, id: GcId) {
512        self.header_mut(id).free_mark = true;
513
514        // Process outgoing edges as `trace` reports them; GC freeing should not allocate
515        // a child snapshot.
516        let mut object = self.objects[id]
517            .as_mut()
518            .expect("invalid GcId")
519            .object
520            .take()
521            .expect("object already finalized");
522        object.trace(&mut |child| {
523            self.free_gc(child);
524        });
525        object.on_free(self);
526        drop(object);
527
528        let defer_free = self.gc_phase == GcPhase::RemoveCycles && self.header(id).ref_count != 0;
529        if !defer_free {
530            self.list_remove_current(id);
531            self.free_slot(id);
532        } else {
533            self.list_move_current_to(GcListKind::ZeroRef, id);
534        }
535    }
536}
537
538impl Default for GcRuntime {
539    fn default() -> Self {
540        Self::new()
541    }
542}