Skip to main content

oxide_core/heap/
code.rs

1//! Code units compiled at run time: `eval`, the `Function` constructors and
2//! `$262.evalScript` publish into the heap, which frees a unit once no
3//! bytecode record names it.
4//!
5//! A unit is reached through the `unit` index of its bytecode records, so a
6//! collection marks it while marking them: a live function, a running frame
7//! (through its function), a suspended generator and a pending job all reach
8//! one. A unit published but not yet instantiated has no record; it is
9//! pinned until its compilation ends. The loop holds a running unit through
10//! an [`Rc`], so freeing a slot never invalidates the code it is executing;
11//! by the time a slot is swept no frame names it and its count is one.
12//!
13//! A live unit marks its atoms ([`CodeUnit::atoms`]); a freed one returns its
14//! cache entries to the [`IcStore`](crate::vm::ic::IcStore) and its bytes to
15//! the heap's account.
16
17use std::rc::Rc;
18
19use crate::bytecode::{CodeUnit, CodeUnitId, DYNAMIC};
20
21#[derive(Default)]
22pub struct DynamicUnits {
23    /// By slot: the index of a dynamic [`CodeUnitId`] without its tag.
24    slots: Vec<Option<Rc<CodeUnit>>>,
25    /// Empty slots, reused lowest first.
26    free: Vec<u32>,
27    /// Under `gc-stress`, slots freed by a sweep are reusable only after the
28    /// next one, so a stale id is not immediately given to another unit.
29    #[cfg(feature = "gc-stress")]
30    quarantine: Vec<u32>,
31    /// Published units not yet instantiated, a stack the compilations
32    /// truncate when they end.
33    pinned: Vec<u32>,
34    marks: Vec<u64>,
35    live: usize,
36    live_bytes: usize,
37}
38
39#[inline]
40fn bit(index: u32) -> (usize, u64) {
41    ((index / 64) as usize, 1u64 << (index % 64))
42}
43
44impl DynamicUnits {
45    /// Stores a validated unit whose cache entries are reserved and pins it.
46    /// Returns its id and the bytes it holds.
47    pub(crate) fn publish(&mut self, mut unit: CodeUnit) -> (CodeUnitId, usize) {
48        unit.atoms = unit.key_atoms();
49        let bytes = unit.bytes();
50        let unit = Some(Rc::new(unit));
51        let index = match self.free.pop() {
52            Some(index) => {
53                self.slots[index as usize] = unit;
54                index
55            }
56            None => {
57                let index = u32::try_from(self.slots.len())
58                    .ok()
59                    .filter(|&index| index < DYNAMIC)
60                    .expect("dynamic code overflow");
61                self.slots.push(unit);
62                if index as usize / 64 == self.marks.len() {
63                    self.marks.push(0);
64                }
65                index
66            }
67        };
68        self.pinned.push(index);
69        self.live += 1;
70        self.live_bytes += bytes;
71        (CodeUnitId(index | DYNAMIC), bytes)
72    }
73
74    /// The live unit `id`.
75    #[inline]
76    #[track_caller]
77    pub fn get(&self, id: CodeUnitId) -> Rc<CodeUnit> {
78        Rc::clone(self.slot(id).expect("a live dynamic unit"))
79    }
80
81    /// The unit `id`, if it is live.
82    pub fn lookup(&self, id: CodeUnitId) -> Option<&CodeUnit> {
83        self.slot(id).map(|unit| &**unit)
84    }
85
86    #[inline]
87    fn slot(&self, id: CodeUnitId) -> Option<&Rc<CodeUnit>> {
88        debug_assert!(id.is_dynamic());
89        self.slots.get((id.0 & !DYNAMIC) as usize)?.as_ref()
90    }
91
92    /// The live units with their ids, by slot.
93    pub fn units(&self) -> impl Iterator<Item = (CodeUnitId, &CodeUnit)> {
94        self.slots.iter().enumerate().filter_map(|(index, unit)| {
95            let unit = unit.as_deref()?;
96            Some((CodeUnitId(index as u32 | DYNAMIC), unit))
97        })
98    }
99
100    /// Live units.
101    pub fn len(&self) -> usize {
102        self.live
103    }
104
105    pub fn is_empty(&self) -> bool {
106        self.live == 0
107    }
108
109    /// Bytes the live units hold.
110    pub fn live_bytes(&self) -> usize {
111        self.live_bytes
112    }
113
114    /// The position a compilation's pins start at.
115    pub(crate) fn pin_mark(&self) -> usize {
116        self.pinned.len()
117    }
118
119    /// Unpins the units published since `mark`: instantiated, they are
120    /// reached through their records; abandoned, the next collection frees
121    /// them.
122    pub(crate) fn unpin(&mut self, mark: usize) {
123        self.pinned.truncate(mark);
124    }
125
126    pub(crate) fn pinned(&self) -> &[u32] {
127        &self.pinned
128    }
129
130    pub(crate) fn clear_marks(&mut self) {
131        self.marks.fill(0);
132    }
133
134    /// Marks unit `id`; the unit if it was not marked yet.
135    #[inline]
136    pub(crate) fn mark(&mut self, id: CodeUnitId) -> Option<&CodeUnit> {
137        let index = id.0 & !DYNAMIC;
138        let (word, mask) = bit(index);
139        let bits = &mut self.marks[word];
140        if *bits & mask != 0 {
141            return None;
142        }
143        *bits |= mask;
144        let unit = self.slots[index as usize].as_deref();
145        debug_assert!(unit.is_some(), "a record names freed dynamic unit {index}");
146        unit
147    }
148
149    pub(crate) fn is_marked(&self, index: u32) -> bool {
150        let (word, mask) = bit(index);
151        self.marks[word] & mask != 0
152    }
153
154    /// Frees every unmarked unit, passing each to `dead` before it is
155    /// dropped. Returns the bytes freed.
156    pub(crate) fn sweep(&mut self, mut dead: impl FnMut(&CodeUnit)) -> usize {
157        #[cfg(feature = "gc-stress")]
158        let held = {
159            let released = std::mem::take(&mut self.quarantine);
160            self.free.extend_from_slice(&released);
161            self.free.len()
162        };
163        let mut freed = 0;
164        for index in (0..self.slots.len() as u32).rev() {
165            if self.is_marked(index) {
166                continue;
167            }
168            let Some(unit) = self.slots[index as usize].take() else {
169                continue;
170            };
171            debug_assert_eq!(
172                Rc::strong_count(&unit),
173                1,
174                "a swept dynamic unit is still running"
175            );
176            dead(&unit);
177            freed += unit.bytes();
178            self.live -= 1;
179            self.free.push(index);
180        }
181        self.live_bytes -= freed;
182        #[cfg(feature = "gc-stress")]
183        self.quarantine.extend(self.free.drain(held..));
184        freed
185    }
186
187    /// Debug check after marking: every live unpinned unit is marked and
188    /// every marked one is live.
189    pub(crate) fn verify(&self) {
190        for (index, unit) in self.slots.iter().enumerate() {
191            let index = index as u32;
192            assert_eq!(
193                unit.is_some(),
194                self.is_marked(index),
195                "gc verify: dynamic unit {index} is {}",
196                if unit.is_some() {
197                    "live but unreachable"
198                } else {
199                    "reachable but freed"
200                }
201            );
202        }
203    }
204}