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_mut(&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        }
202    }
203
204    fn list_push_back(&mut self, kind: GcListKind, id: GcId) {
205        let tail = self.list_mut(kind).tail;
206
207        {
208            let header = self.header_mut(id);
209            debug_assert!(
210                header.list_kind.is_none(),
211                "object already belongs to a GC list: {:?}",
212                header.list_kind
213            );
214            header.list_kind = Some(kind);
215            header.list_prev = tail;
216            header.list_next = None;
217        }
218
219        if let Some(tail_id) = tail {
220            self.header_mut(tail_id).list_next = Some(id);
221        } else {
222            self.list_mut(kind).head = Some(id);
223        }
224        self.list_mut(kind).tail = Some(id);
225    }
226
227    fn list_remove(&mut self, kind: GcListKind, id: GcId) {
228        let (prev, next) = {
229            let header = self.header(id);
230            debug_assert_eq!(header.list_kind, Some(kind), "object is not on the expected GC list");
231            (header.list_prev, header.list_next)
232        };
233
234        match prev {
235            Some(p) => self.header_mut(p).list_next = next,
236            None => self.list_mut(kind).head = next,
237        }
238
239        match next {
240            Some(n) => self.header_mut(n).list_prev = prev,
241            None => self.list_mut(kind).tail = prev,
242        }
243
244        let header = self.header_mut(id);
245        header.list_kind = None;
246        header.list_prev = None;
247        header.list_next = None;
248    }
249
250    fn list_remove_current(&mut self, id: GcId) {
251        let kind = self
252            .header(id)
253            .list_kind
254            .expect("object is not on a GC list");
255        self.list_remove(kind, id);
256    }
257
258    fn list_move(&mut self, from: GcListKind, to: GcListKind, id: GcId) {
259        self.list_remove(from, id);
260        self.list_push_back(to, id);
261    }
262
263    fn list_move_current_to(&mut self, to: GcListKind, id: GcId) {
264        let from = self
265            .header(id)
266            .list_kind
267            .expect("object is not on a GC list");
268        self.list_move(from, to, id);
269    }
270
271    fn alloc_slot(&mut self, entry: GcObjectEntry) -> GcId {
272        let id = if let Some(id) = self.free_slots.pop() {
273            self.objects[id] = Some(entry);
274            id
275        } else {
276            let id = self.objects.len();
277            self.objects.push(Some(entry));
278            id
279        };
280        self.malloc_state
281            .record_alloc(std::mem::size_of::<GcObjectEntry>());
282        id
283    }
284
285    fn free_slot(&mut self, id: GcId) {
286        self.malloc_state
287            .record_free(std::mem::size_of::<GcObjectEntry>());
288        self.objects[id] = None;
289        self.free_slots.push(id);
290    }
291
292    /// Register a new GC object with `ref_count = 1` on `gc_obj_list`.
293    /// Matches QuickJS `add_gc_object`.
294    pub fn add_gc_object(&mut self, object: Box<dyn GcObject>, gc_obj_type: GcObjectType) -> GcId {
295        let id = self.alloc_slot(GcObjectEntry {
296            header: GcObjectHeader::new(gc_obj_type, 1),
297            object: Some(object),
298        });
299        self.list_push_back(GcListKind::GcObj, id);
300        id
301    }
302
303    /// Increment refcount. Matches QuickJS `js_dup` for GC objects.
304    pub fn dup_gc(&mut self, id: GcId) -> GcId {
305        self.header_mut(id).ref_count += 1;
306        id
307    }
308
309    /// Decrement refcount and free when it reaches zero.
310    /// Matches QuickJS `JS_FreeValueRT` for GC objects.
311    pub fn free_gc(&mut self, id: GcId) {
312        let ref_count = match self.objects.get_mut(id).and_then(|slot| slot.as_mut()) {
313            Some(entry) => {
314                entry.header.ref_count -= 1;
315                entry.header.ref_count
316            }
317            None => return,
318        };
319
320        if ref_count > 0 {
321            return;
322        }
323
324        if self.gc_phase != GcPhase::RemoveCycles {
325            self.list_move(GcListKind::GcObj, GcListKind::ZeroRef, id);
326            if self.gc_phase == GcPhase::None {
327                self.free_zero_refcount();
328            }
329        }
330    }
331
332    /// Allocate a simple refcounted payload (strings, etc.). Not cycle-collected.
333    pub fn add_ref_counted<F>(&mut self, on_free: F) -> RefCountId
334    where
335        F: FnOnce(&mut GcRuntime) + 'static,
336    {
337        let entry = RefCountEntry {
338            header: RefCountHeader::new(1),
339            payload: Box::new(on_free),
340        };
341        let id = if let Some(id) = self.ref_count_free_slots.pop() {
342            self.ref_counts[id] = Some(entry);
343            id
344        } else {
345            let id = self.ref_counts.len();
346            self.ref_counts.push(Some(entry));
347            id
348        };
349        self.malloc_state
350            .record_alloc(std::mem::size_of::<RefCountEntry>());
351        id
352    }
353
354    pub fn dup_ref_counted(&mut self, id: RefCountId) -> RefCountId {
355        self.ref_counts[id]
356            .as_mut()
357            .expect("invalid RefCountId")
358            .header
359            .ref_count += 1;
360        id
361    }
362
363    pub fn free_ref_counted(&mut self, id: RefCountId) {
364        let ref_count = {
365            let entry = self.ref_counts[id].as_mut().expect("invalid RefCountId");
366            entry.header.ref_count -= 1;
367            entry.header.ref_count
368        };
369
370        if ref_count <= 0 {
371            let entry = self.ref_counts[id].take().expect("double free");
372            self.malloc_state
373                .record_free(std::mem::size_of::<RefCountEntry>());
374            (entry.payload)(self);
375            self.ref_count_free_slots.push(id);
376        }
377    }
378
379    /// Mark a GC object header during traversal. Matches `JS_MarkValue` for object tags.
380    pub fn mark_gc_header(&mut self, id: GcId, mark_func: MarkFunc) {
381        match mark_func {
382            MarkFunc::Decref => self.gc_decref_child(id),
383            MarkFunc::ScanIncref => self.gc_scan_incref_child(id),
384            MarkFunc::ScanIncref2 => self.gc_scan_incref_child2(id),
385        }
386    }
387
388    /// Traverse children of a GC object. Matches QuickJS `mark_children`.
389    pub fn mark_children(&mut self, id: GcId, mark_func: MarkFunc) {
390        // Move the box out temporarily so `trace` can call back into the runtime without
391        // materializing a child-id buffer.
392        let object = self.objects[id]
393            .as_mut()
394            .expect("invalid GcId")
395            .object
396            .take()
397            .expect("object already finalized");
398        object.trace(&mut |child| {
399            self.mark_gc_header(child, mark_func);
400        });
401        self.objects[id]
402            .as_mut()
403            .expect("object freed while tracing")
404            .object = Some(object);
405    }
406
407    /// Maybe run GC when tracked malloc exceeds threshold. Matches `js_trigger_gc`.
408    pub fn trigger_gc(&mut self, alloc_size: usize) {
409        let force_gc =
410            self.malloc_state.malloc_size.saturating_add(alloc_size) > self.malloc_gc_threshold;
411        if force_gc {
412            self.run_gc();
413            self.malloc_gc_threshold =
414                self.malloc_state.malloc_size + (self.malloc_state.malloc_size >> 1);
415        }
416    }
417
418    /// Run all collector phases atomically and return per-phase diagnostics.
419    pub fn run_gc_with_stats(&mut self) -> GcPhaseStats {
420        self.run_gc_with_stats_bundle().phases
421    }
422
423    /// Return false if the object has been freed during cycle collection.
424    /// Matches `JS_IsLiveObject`.
425    pub fn is_live_object(&self, id: GcId) -> bool {
426        self.objects
427            .get(id)
428            .and_then(|o| o.as_ref())
429            .is_some_and(|e| !e.header.free_mark)
430    }
431
432    pub fn ref_count(&self, id: GcId) -> i32 {
433        self.header(id).ref_count
434    }
435
436    pub fn object_exists(&self, id: GcId) -> bool {
437        self.objects.get(id).and_then(|o| o.as_ref()).is_some()
438    }
439
440    pub fn object_downcast<T: GcObject>(&self, id: GcId) -> Option<&T> {
441        let entry = self.objects.get(id)?.as_ref()?;
442        (entry.object.as_ref()?.as_ref() as &dyn Any).downcast_ref()
443    }
444
445    pub fn object_downcast_mut<T: GcObject>(&mut self, id: GcId) -> Option<&mut T> {
446        let entry = self.objects.get_mut(id)?.as_mut()?;
447        (entry.object.as_mut()?.as_mut() as &mut dyn Any).downcast_mut()
448    }
449
450    // --- Phase 1: trial deletion ---
451
452    fn gc_decref_child(&mut self, id: GcId) {
453        let header = self.header_mut(id);
454        debug_assert!(header.ref_count > 0);
455        header.ref_count -= 1;
456        if header.ref_count == 0 && header.mark == 1 {
457            self.list_move(GcListKind::GcObj, GcListKind::Tmp, id);
458        }
459    }
460
461    // --- Phase 2: restore live refs ---
462
463    fn gc_scan_incref_child(&mut self, id: GcId) {
464        let header = self.header_mut(id);
465        header.ref_count += 1;
466        if header.ref_count == 1 {
467            self.list_move(GcListKind::Tmp, GcListKind::GcObj, id);
468            self.header_mut(id).mark = 0;
469        }
470    }
471
472    fn gc_scan_incref_child2(&mut self, id: GcId) {
473        self.header_mut(id).ref_count += 1;
474    }
475
476    // --- Phase 3: free cyclic garbage ---
477
478    fn free_zero_refcount(&mut self) {
479        self.gc_phase = GcPhase::Decref;
480        loop {
481            let id = self.gc_zero_ref_count_list.head;
482            if id.is_none() {
483                break;
484            }
485            let id = id.unwrap();
486            debug_assert_eq!(self.header(id).ref_count, 0);
487            self.free_gc_object(id);
488        }
489        self.gc_phase = GcPhase::None;
490    }
491
492    fn free_gc_object(&mut self, id: GcId) {
493        match self.header(id).gc_obj_type {
494            GcObjectType::MonkeyObject | GcObjectType::FunctionBytecode => {
495                self.free_heap_object(id)
496            }
497            other => panic!("free_gc_object: unsupported type {:?}", other),
498        }
499    }
500
501    fn free_heap_object(&mut self, id: GcId) {
502        self.header_mut(id).free_mark = true;
503
504        // Process outgoing edges as `trace` reports them; GC freeing should not allocate
505        // a child snapshot.
506        let mut object = self.objects[id]
507            .as_mut()
508            .expect("invalid GcId")
509            .object
510            .take()
511            .expect("object already finalized");
512        object.trace(&mut |child| {
513            self.free_gc(child);
514        });
515        object.on_free(self);
516        drop(object);
517
518        let defer_free = self.gc_phase == GcPhase::RemoveCycles && self.header(id).ref_count != 0;
519        if !defer_free {
520            self.list_remove_current(id);
521            self.free_slot(id);
522        } else {
523            self.list_move_current_to(GcListKind::ZeroRef, id);
524        }
525    }
526}
527
528impl Default for GcRuntime {
529    fn default() -> Self {
530        Self::new()
531    }
532}