candela_vm/gc.rs
1// This file is derived from keel (https://github.com/horacehoff/keel),
2// Copyright 2026 Horace Hoff, licensed under the Apache License, Version 2.0.
3// It has been modified by the candela authors. See the NOTICE file.
4//! The garbage collector.
5//!
6//! Every heap value lives in one of three pools: arrays (which also hold
7//! structs, enums, function values and closure cells), maps, and strings too
8//! long to sit inside a [`Data`]. One collection covers all three, because the
9//! trace crosses between them: a list can hold a map, a map can hold a list,
10//! and either can hold a pooled string. A slot the trace does not reach goes on
11//! its pool's free list, and the next allocation in that pool reuses it.
12//!
13//! A collection is a cycle of three phases, and the collector can stop between
14//! any two steps of it and carry on later:
15//!
16//! - Idle: nothing to do. A cycle begins when a pool that has reached its
17//! threshold is down to its reserve of free slots: enough for the program
18//! to allocate from while the cycle marks, so the pool does not have to
19//! grow for it. A cycle also begins at the next allocation once the program
20//! has allocated more bytes since the last cycle than its byte threshold,
21//! so a program that allocates few but large lists does not grow its heap
22//! far past what it keeps. While the cycle runs, every allocation, and
23//! every [`DEBT_BYTES`] allocated, pays for a share of its work, done a
24//! slice at a time, so the program never stops for more than one slice.
25//! - Mark: beginning a cycle records how long each pool is, marks every slot
26//! already on a free list, and marks what the roots hold. Marking then walks
27//! the objects it has reached, a chunk of at most [`CHUNK`] entries at a time,
28//! until nothing is left to walk.
29//! - Sweep: walks each pool's marks up to the length recorded at the start and
30//! puts every unmarked slot on the free list. Then the cycle is over.
31//!
32//! Work is counted in units: one per chunk walked, one per 64 slots swept and
33//! one per slot freed. A step is given a budget in these units and stops once
34//! it has spent it.
35//!
36//! A host can do the same work while its program is idle, between frames, by
37//! calling [`GcState::collect`] with a budget of its own. That drives the same
38//! cycle the allocations drive: a cycle an allocation began is carried on by
39//! the host, and the other way round. A host that collects this way is taken
40//! to pace the collector, and the thresholds at which an allocation begins a
41//! cycle rise to leave the work to it.
42//!
43//! The program keeps running between steps, so marking is
44//! snapshot-at-the-beginning: every object reachable when the cycle began is
45//! marked before the sweep starts, whatever the program does to the heap in
46//! the meantime. Three rules keep that true:
47//!
48//! - A store that overwrites or removes a value from a list, struct, cell or
49//! map while marking runs marks the value it takes out first (the deletion
50//! barrier, [`shade_overwritten`]). A value the program read out of the heap
51//! and kept only in a register is still marked that way.
52//! - A list or map whose entries move in place (a removal, a sort, a reverse)
53//! after marking has walked part of it is walked again from the start
54//! ([`rescan_list`], [`rescan_map`]), since an entry could have moved from
55//! the part not yet walked into the part already walked.
56//! - Interning a string hands out a slot found by its text, which the program
57//! may have held no reference to; the slot is marked as it is handed out.
58//!
59//! Registers and the recursion stack take no barrier. They are read once, when
60//! the cycle begins, and anything they receive later was either reachable then
61//! or allocated since.
62//!
63//! Building with the `gc-torture` feature runs a cycle on every allocation, a
64//! unit of work at a time, and checks at the end of every mark phase that a
65//! fresh trace from the roots finds nothing unmarked.
66
67use crate::data::Data;
68use crate::data::PoolString;
69use crate::vm::CandelaMap;
70use crate::vm::MapPool;
71use crate::vm::ObjectPool;
72use crate::vm::RegisterFile;
73use crate::vm::StringPool;
74
75/// The fewest slots a pool holds before its first collection, and the least a
76/// collection sets the next one's threshold to.
77pub const MIN_GC_THRESHOLD: u32 = 256;
78
79/// The most elements a freed array or map keeps room for. A freed slot holds
80/// on to its buffer so the next value written there, usually a struct or a
81/// short list, needs no allocation; a buffer larger than this goes back to the
82/// allocator instead of sitting in a dead slot until something reuses it.
83const KEEP_FREED_CAPACITY: usize = 64;
84
85/// Collect on every allocation, a unit at a time, and check every mark phase
86/// against a fresh trace. See the module documentation.
87const TORTURE: bool = cfg!(feature = "gc-torture");
88
89/// The least a threshold falls to: under torture every allocation starts a
90/// cycle.
91const THRESHOLD_FLOOR: u32 = if TORTURE { 0 } else { MIN_GC_THRESHOLD };
92
93/// Units of collector work each allocation pays for while a cycle runs. A
94/// cycle with `n` units of work finishes within `n / ALLOC_DEBT` allocations.
95const ALLOC_DEBT: u32 = 16;
96
97/// The units one allocation-driven step does: the allocations' debt is paid
98/// once it reaches this, which bounds how long one allocation stops the
99/// program.
100const SLICE: u32 = 512;
101
102/// The least the byte threshold is: the bytes a program allocates before its
103/// first collection by bytes, and the floor under every later threshold. A
104/// program whose heap stays under it is paced by its object count alone.
105pub const MIN_BYTE_THRESHOLD: usize = 8 << 20;
106
107/// Bytes allocated while a cycle runs that pay for the same share of its work
108/// as one allocation does, [`ALLOC_DEBT`] units. A program building large
109/// lists allocates few objects, and this keeps a cycle it is in from lasting
110/// for as many bytes as the whole heap holds.
111pub const DEBT_BYTES: usize = 8 << 10;
112
113/// What one list element costs in bytes.
114pub const LIST_ENTRY_BYTES: usize = size_of::<Data>();
115
116/// What one map entry costs in bytes: the key, the value, the hash the entry
117/// keeps and the index slot that finds it.
118pub const MAP_ENTRY_BYTES: usize = 2 * size_of::<Data>() + 2 * size_of::<usize>();
119
120/// The most entries of one list or map a single unit of marking walks. A large
121/// list is walked a chunk at a time, so no single step has to walk all of it.
122pub const CHUNK: u32 = 64;
123
124/// One mark bit per pool slot.
125///
126/// A bit per slot rather than a `bool` keeps the marks of a large pool in an
127/// eighth of the memory, so clearing them and walking them for the free slots
128/// touch an eighth of the cache lines.
129#[derive(Default)]
130pub struct MarkBits(Vec<u64>);
131
132impl MarkBits {
133 /// Clears every mark and sizes the set for `len` slots.
134 pub fn reset(&mut self, len: usize) {
135 self.0.clear();
136 self.0.resize(len.div_ceil(64), 0);
137 }
138 /// Whether slot `i` is marked.
139 #[inline(always)]
140 #[must_use]
141 pub fn contains(&self, i: usize) -> bool {
142 unsafe { self.0.get_unchecked(i / 64) & (1u64 << (i % 64)) != 0 }
143 }
144 /// Marks slot `i` and reports whether it was already marked.
145 #[inline(always)]
146 pub fn insert(&mut self, i: usize) -> bool {
147 let word = unsafe { self.0.get_unchecked_mut(i / 64) };
148 let bit = 1u64 << (i % 64);
149 let was = *word & bit != 0;
150 *word |= bit;
151 was
152 }
153 /// The unmarked slots among the 64 that word `w` covers, below `limit`, as
154 /// set bits.
155 #[inline(always)]
156 fn unmarked_in_word(&self, w: usize, limit: usize) -> u64 {
157 let free = !unsafe { *self.0.get_unchecked(w) };
158 let first = w * 64;
159 if limit - first >= 64 {
160 free
161 } else {
162 free & ((1u64 << (limit - first)) - 1)
163 }
164 }
165}
166
167/// The threshold the next collection of a pool runs at: `factor` times what
168/// survived this one. A pool that doubles its live set between collections
169/// pays for each collection with as many fresh allocations as the trace
170/// visited, and a pool whose live set stays flat stops growing instead of
171/// doubling on every collection whether anything survived or not.
172#[inline(always)]
173fn next_threshold(pool_len: usize, freed: usize, factor: usize) -> u32 {
174 if TORTURE {
175 return 0;
176 }
177 let live = pool_len - freed;
178 (live.saturating_mul(factor).min(u32::MAX as usize) as u32).max(MIN_GC_THRESHOLD)
179}
180
181/// How far past its live set a pool grows before an allocation begins a cycle,
182/// for a program nobody else collects for.
183const GROWTH: usize = 2;
184
185/// The same, once the host collects between frames: the allocations leave room
186/// for the host to do the work first, and begin a cycle themselves only when
187/// it falls behind.
188const HOST_PACED_GROWTH: usize = 4;
189
190/// The share of a pool its free slots have to fall below before an idle
191/// collection begins a cycle for it: a pool with more free than this has room
192/// to allocate in without growing.
193const IDLE_FREE_SHARE: usize = 8;
194
195/// What the collector has done over a set of pools, and how large they are.
196///
197/// Work is counted in the units a budget is given in, never in time: the count
198/// is the same on every machine and every run, and a host that wants time
199/// measures its own calls.
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
201pub struct GcStats {
202 /// How many collection cycles have begun.
203 pub cycles: u64,
204 /// Every unit of collection work done, across all cycles.
205 pub units: u64,
206 /// The most units one step did: the longest the collector held up the
207 /// program at once, whether an allocation or the host asked for the step.
208 pub largest_slice: u32,
209 /// The object pool: lists, structs, enum values, functions and closure
210 /// cells.
211 pub arrays: PoolStats,
212 /// The map pool.
213 pub maps: PoolStats,
214 /// The pool of strings too long to sit inside a value.
215 pub strings: PoolStats,
216}
217
218/// How large one pool is.
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct PoolStats {
221 /// Every slot the pool holds, live or free.
222 pub len: usize,
223 /// The slots on its free list, waiting for an allocation to reuse them.
224 pub free: usize,
225}
226
227/// An object the trace has reached and not yet walked to its end: the list or
228/// map, and the entry the next chunk starts at.
229#[derive(Clone, Copy)]
230struct Grey {
231 id: u32,
232 from: u32,
233 map: bool,
234}
235
236/// Where a cycle stands.
237#[derive(Clone, Copy, PartialEq, Eq)]
238enum Phase {
239 Idle,
240 Mark,
241 /// Sweeping the pool named, arrays first, then maps, then strings.
242 Sweep(Swept),
243}
244
245#[derive(Clone, Copy, PartialEq, Eq)]
246enum Swept {
247 Arrays,
248 Maps,
249 Strings,
250}
251
252/// The marks of the two object pools and the objects still to walk.
253///
254/// Every slot at or past a pool's limit was allocated after the cycle began,
255/// and counts as marked without a bit: nothing a cycle allocates is freed by
256/// it. The string pool keeps its own marks, see [`StringPool::shade`].
257#[derive(Default)]
258struct Tracer {
259 array_marks: MarkBits,
260 map_marks: MarkBits,
261 array_limit: u32,
262 map_limit: u32,
263 work: Vec<Grey>,
264 /// The bytes the lists, maps and pooled strings marked so far hold.
265 live_bytes: usize,
266}
267
268impl Tracer {
269 /// Marks what `d` refers to, queueing a list or map it marks for its
270 /// entries to be walked.
271 #[inline(always)]
272 fn shade(&mut self, strings: &mut StringPool, d: Data) {
273 if d.is_map() {
274 let id = d.as_map() as u32;
275 if id < self.map_limit && !self.map_marks.insert(id as usize) {
276 self.work.push(Grey {
277 id,
278 from: 0,
279 map: true,
280 });
281 }
282 } else if d.is_array() || d.is_struct() || d.is_enum() {
283 let id = d.as_array() as u32;
284 if id < self.array_limit && !self.array_marks.insert(id as usize) {
285 self.work.push(Grey {
286 id,
287 from: 0,
288 map: false,
289 });
290 }
291 } else if d.is_large_str() {
292 self.live_bytes += strings.shade_counted(d.get_str_pool_id());
293 }
294 }
295
296 /// Walks the next chunk of `g`, queueing the rest of it when it is longer.
297 ///
298 /// Each entry is tested on its own, never read as a verdict on its
299 /// neighbours: a list or map typed `any`, which is what a parsed json
300 /// document is made of, mixes scalars and objects freely.
301 #[inline(always)]
302 fn walk(&mut self, objs: &ObjectPool, maps: &MapPool, strings: &mut StringPool, g: Grey) {
303 let from = g.from as usize;
304 if g.map {
305 let map = &maps[g.id as usize];
306 if from == 0 {
307 self.live_bytes += map.capacity() * MAP_ENTRY_BYTES;
308 }
309 let end = map.len().min(from + CHUNK as usize);
310 if let Some(entries) = map.get_range(from..end) {
311 for (k, v) in entries {
312 self.shade(strings, *k);
313 self.shade(strings, *v);
314 }
315 }
316 if end < map.len() {
317 self.work.push(Grey {
318 from: end as u32,
319 ..g
320 });
321 }
322 } else {
323 let list = &objs[g.id as usize];
324 if from == 0 {
325 self.live_bytes += list.capacity() * LIST_ENTRY_BYTES;
326 }
327 let end = list.len().min(from + CHUNK as usize);
328 if from < end {
329 for &e in unsafe { list.get_unchecked(from..end) } {
330 self.shade(strings, e);
331 }
332 }
333 if end < list.len() {
334 self.work.push(Grey {
335 from: end as u32,
336 ..g
337 });
338 }
339 }
340 }
341}
342
343/// A pool's free slots, kept in two lists so that the common allocation asks
344/// the collector nothing: it takes a slot from `ready`, and only when that is
345/// empty does it go to the collector, which decides from there.
346struct FreeSlots<'a> {
347 /// Slots an allocation takes without asking the collector.
348 ready: &'a mut Vec<u32>,
349 /// The rest: the reserve that feeds the next cycle, and every free slot
350 /// while a cycle runs, so each allocation then pays its share.
351 held: &'a mut Vec<u32>,
352}
353
354impl FreeSlots<'_> {
355 /// A free slot, ready or held.
356 fn take(&mut self) -> Option<u32> {
357 self.ready.pop().or_else(|| self.held.pop())
358 }
359 /// Holds every free slot back, so each allocation goes to the collector.
360 /// The lists trade places rather than copy when one is empty, which is the
361 /// usual case, since a pool's free list can run to most of the pool.
362 fn hold_all(&mut self) {
363 if self.held.is_empty() {
364 std::mem::swap(self.ready, self.held);
365 } else {
366 self.held.append(self.ready);
367 }
368 }
369 /// Makes every free slot ready except `reserve` of them.
370 fn release_above(&mut self, reserve: usize) {
371 if self.ready.is_empty() {
372 std::mem::swap(self.ready, self.held);
373 let keep = reserve.min(self.ready.len());
374 let from = self.ready.len() - keep;
375 self.held.extend(self.ready.drain(from..));
376 } else if self.held.len() > reserve {
377 self.ready.extend(self.held.drain(reserve..));
378 }
379 }
380}
381
382/// What the collector knows about the pools between two allocations.
383///
384/// This lives with the pools rather than with one run of the interpreter
385/// because the pools do: a host that calls into a resident program many times
386/// keeps every object the earlier calls left behind, and a collector that
387/// forgot its free lists and started its thresholds over on each call would
388/// run a full mark and sweep on the first few allocations of every call once
389/// the pools had grown past the starting threshold, and would never reuse a
390/// slot the previous call had freed. A cycle in progress carries over between
391/// calls the same way.
392pub struct GcState {
393 /// Object-pool slots (arrays, structs, enums and function values) an
394 /// allocation takes without asking the collector.
395 pub(crate) free_arrays: Vec<u32>,
396 /// Map-pool slots an allocation takes without asking the collector.
397 pub(crate) free_maps: Vec<u32>,
398 /// String-pool slots an allocation takes without asking the collector.
399 pub(crate) free_strings: Vec<u32>,
400 /// Object-pool length from which a cycle may begin.
401 array_threshold: u32,
402 /// Map-pool length from which a cycle may begin.
403 map_threshold: u32,
404 /// String-pool length from which a cycle may begin.
405 string_threshold: u32,
406 phase: Phase,
407 tracer: Tracer,
408 /// The next mark word the sweep reads in the pool it is sweeping.
409 sweep_word: usize,
410 /// The free slots below the limit of the pool being swept: those already
411 /// free when the cycle began, plus those the sweep has freed so far. The
412 /// program reuses freed slots while the sweep runs, so the free list's
413 /// length says nothing about what survived.
414 swept_free: usize,
415 /// Each pool's free slots when the cycle began: arrays, maps, strings.
416 free_at_begin: [usize; 3],
417 /// Units of work the allocations of the cycle in progress have paid for
418 /// and no step has done yet.
419 debt: u32,
420 /// How many free slots each pool keeps back for the next cycle to
421 /// allocate from while it marks. Set from how much the last cycle marked.
422 reserve: usize,
423 /// Units the cycle in progress has spent marking.
424 mark_units: usize,
425 /// How many cycles have begun.
426 cycles: u64,
427 /// Every unit of work done, across all cycles.
428 units: u64,
429 /// The most units one step has spent.
430 largest_slice: u32,
431 /// Each pool's free slots held back from the ready lists above: arrays,
432 /// maps, strings. See [`FreeSlots`].
433 held: [Vec<u32>; 3],
434 /// Each pool's occupied slots when the last cycle swept it: arrays, maps,
435 /// strings. An idle collection measures growth from here.
436 occupied_after: [usize; 3],
437 /// Whether the host has collected, which raises the thresholds.
438 host_paced: bool,
439 /// Bytes allocated since the last cycle began, as far as the allocations
440 /// that report them say: list and map buffers and pooled strings.
441 bytes: usize,
442 /// The bytes past which an allocation begins a cycle.
443 byte_threshold: usize,
444 /// The count of `bytes` at which [`note_bytes`](Self::note_bytes) next
445 /// has something to do: the threshold while no cycle runs, the next debt
446 /// step while one does.
447 byte_trigger: usize,
448 /// The bytes the last finished mark phase found live.
449 live_bytes: usize,
450}
451
452impl Default for GcState {
453 fn default() -> Self {
454 Self {
455 free_arrays: Vec::new(),
456 free_maps: Vec::new(),
457 free_strings: Vec::new(),
458 array_threshold: THRESHOLD_FLOOR,
459 map_threshold: THRESHOLD_FLOOR,
460 string_threshold: THRESHOLD_FLOOR,
461 phase: Phase::Idle,
462 tracer: Tracer::default(),
463 sweep_word: 0,
464 swept_free: 0,
465 free_at_begin: [0; 3],
466 debt: 0,
467 reserve: 0,
468 mark_units: 0,
469 cycles: 0,
470 units: 0,
471 largest_slice: 0,
472 held: [Vec::new(), Vec::new(), Vec::new()],
473 occupied_after: [0; 3],
474 host_paced: false,
475 bytes: 0,
476 byte_threshold: MIN_BYTE_THRESHOLD,
477 byte_trigger: if TORTURE {
478 usize::MAX
479 } else {
480 MIN_BYTE_THRESHOLD
481 },
482 live_bytes: 0,
483 }
484 }
485}
486
487impl GcState {
488 /// Whether this build runs a cycle on every allocation, a unit of work at
489 /// a time, which is what the `gc-torture` feature turns on.
490 pub const TORTURE: bool = TORTURE;
491
492 /// How many collections have begun over these pools.
493 #[must_use]
494 pub const fn cycles(&self) -> u64 {
495 self.cycles
496 }
497
498 /// What the collector has done over `objs`, `maps` and `strings`, the
499 /// pools this state collects.
500 #[must_use]
501 pub fn stats(&self, objs: &ObjectPool, maps: &MapPool, strings: &StringPool) -> GcStats {
502 GcStats {
503 cycles: self.cycles,
504 units: self.units,
505 largest_slice: self.largest_slice,
506 arrays: PoolStats {
507 len: objs.len(),
508 free: self.free_len(Swept::Arrays),
509 },
510 maps: PoolStats {
511 len: maps.len(),
512 free: self.free_len(Swept::Maps),
513 },
514 strings: PoolStats {
515 len: strings.len(),
516 free: self.free_len(Swept::Strings),
517 },
518 }
519 }
520
521 /// Collection work for a host to do while its program is idle: carries
522 /// the cycle in progress forward by about `budget` units, beginning one
523 /// first if the pools have grown enough since the last to be worth it.
524 /// Answers whether no collection work is left.
525 ///
526 /// `registers` are the roots. The host calls this between calls into the
527 /// program, when the registers are the only roots there are.
528 ///
529 /// Beginning a cycle reads every root at once, whatever the budget; after
530 /// that a step stops within one chunk or one mark word of the budget. A
531 /// cycle begins only when some pool has grown by half its live set (and by
532 /// at least [`MIN_GC_THRESHOLD`] slots) since the last one and has fewer
533 /// than an eighth of its slots free, or the program has allocated half as
534 /// many bytes as the last cycle found live (and at least
535 /// [`MIN_BYTE_THRESHOLD`]); otherwise the call does nothing.
536 pub fn collect(
537 &mut self,
538 objs: &mut ObjectPool,
539 maps: &mut MapPool,
540 strings: &mut StringPool,
541 registers: &[Data],
542 budget: u32,
543 ) -> bool {
544 self.host_paced = true;
545 if !self.active() && !self.worth_collecting(objs.len(), maps.len(), strings.len()) {
546 return true;
547 }
548 if budget == 0 {
549 return false;
550 }
551 let roots = [registers, &[][..]];
552 let mut spent = 0;
553 if !self.active() {
554 spent = self.begin(objs, maps, strings, roots);
555 }
556 spent += self.advance(objs, maps, strings, roots, budget);
557 // Work the host did is work the allocations no longer owe.
558 self.debt = self.debt.saturating_sub(spent);
559 self.record(spent);
560 !self.active()
561 }
562
563 /// Whether an idle collection should begin a cycle: some pool has grown
564 /// by half its live set since the last cycle and has few free slots left,
565 /// or the program has allocated half as many bytes as the last cycle found
566 /// live (and at least [`MIN_BYTE_THRESHOLD`]).
567 fn worth_collecting(&self, arrays: usize, maps: usize, strings: usize) -> bool {
568 if self.bytes >= (self.live_bytes / 2).max(MIN_BYTE_THRESHOLD) {
569 return true;
570 }
571 let lens = [arrays, maps, strings];
572 [Swept::Arrays, Swept::Maps, Swept::Strings]
573 .into_iter()
574 .enumerate()
575 .any(|(i, pool)| {
576 let free = self.free_len(pool);
577 let occupied = lens[i].saturating_sub(free);
578 let live = self.occupied_after[i];
579 let grown = occupied.saturating_sub(live);
580 grown >= (live / 2).max(MIN_GC_THRESHOLD as usize)
581 && free < lens[i] / IDLE_FREE_SHARE
582 })
583 }
584
585 /// How many free slots `pool` has, ready and held.
586 fn free_len(&self, pool: Swept) -> usize {
587 let (ready, held) = match pool {
588 Swept::Arrays => (&self.free_arrays, &self.held[0]),
589 Swept::Maps => (&self.free_maps, &self.held[1]),
590 Swept::Strings => (&self.free_strings, &self.held[2]),
591 };
592 ready.len() + held.len()
593 }
594
595 /// The factor [`next_threshold`] grows a pool's live set by.
596 const fn growth(&self) -> usize {
597 if self.host_paced {
598 HOST_PACED_GROWTH
599 } else {
600 GROWTH
601 }
602 }
603
604 /// Whether marking is under way, which is when a store has to mark the
605 /// value it overwrites.
606 #[inline(always)]
607 #[must_use]
608 pub(crate) fn marking(&self) -> bool {
609 self.phase == Phase::Mark
610 }
611
612 /// The free slots of `pool`, ready and held.
613 fn free(&mut self, pool: Swept) -> FreeSlots<'_> {
614 let [arrays, maps, strings] = &mut self.held;
615 match pool {
616 Swept::Arrays => FreeSlots {
617 ready: &mut self.free_arrays,
618 held: arrays,
619 },
620 Swept::Maps => FreeSlots {
621 ready: &mut self.free_maps,
622 held: maps,
623 },
624 Swept::Strings => FreeSlots {
625 ready: &mut self.free_strings,
626 held: strings,
627 },
628 }
629 }
630
631 /// Whether a cycle is in progress.
632 #[inline(always)]
633 #[must_use]
634 pub(crate) fn active(&self) -> bool {
635 self.phase != Phase::Idle
636 }
637
638 /// Begins a cycle: records each pool's length, holds back and marks every
639 /// free slot, and marks what the roots hold. Answers the units it spent,
640 /// which it spends whatever the budget, since the roots have to be read
641 /// all at once. The caller adds them to the slice it records.
642 fn begin(
643 &mut self,
644 objs: &ObjectPool,
645 maps: &MapPool,
646 strings: &mut StringPool,
647 roots: [&[Data]; 2],
648 ) -> u32 {
649 debug_assert!(!self.active());
650 self.cycles += 1;
651 self.mark_units = 0;
652 self.debt = 0;
653 self.bytes = 0;
654 self.byte_trigger = if TORTURE { usize::MAX } else { DEBT_BYTES };
655 self.tracer.live_bytes = 0;
656 self.free(Swept::Arrays).hold_all();
657 self.free(Swept::Maps).hold_all();
658 self.free(Swept::Strings).hold_all();
659 self.free_at_begin = [self.held[0].len(), self.held[1].len(), self.held[2].len()];
660 let tracer = &mut self.tracer;
661 tracer.array_limit = objs.len() as u32;
662 tracer.map_limit = maps.len() as u32;
663 tracer.array_marks.reset(objs.len());
664 tracer.map_marks.reset(maps.len());
665 strings.begin_marking();
666 // A free slot is garbage already, and marking it keeps the sweep from
667 // putting it on the free list a second time.
668 for &id in &self.held[0] {
669 tracer.array_marks.insert(id as usize);
670 }
671 for &id in &self.held[1] {
672 tracer.map_marks.insert(id as usize);
673 }
674 for &id in &self.held[2] {
675 strings.shade(id as usize);
676 }
677 let mut read = self.free_at_begin.iter().sum::<usize>();
678 for root in roots {
679 read += root.len();
680 for &d in root {
681 tracer.shade(strings, d);
682 }
683 }
684 self.phase = Phase::Mark;
685 // One unit per 64 values read, and one per 64 mark words cleared.
686 let cleared = (objs.len() + maps.len() + strings.len()) / 64;
687 ((read + cleared) / 64 + 1).min(u32::MAX as usize) as u32
688 }
689
690 /// Carries the cycle forward by about `budget` units, and answers the
691 /// units spent. Marking stops at the budget; sweeping can pass it by up to
692 /// one mark word's worth of freed slots. A cycle that has not begun has
693 /// nothing to do. The caller records the slice.
694 fn advance(
695 &mut self,
696 objs: &mut ObjectPool,
697 maps: &mut MapPool,
698 strings: &mut StringPool,
699 roots: [&[Data]; 2],
700 budget: u32,
701 ) -> u32 {
702 let mut spent = 0u32;
703 while spent < budget {
704 match self.phase {
705 Phase::Idle => break,
706 Phase::Mark => {
707 let before = spent;
708 while spent < budget {
709 let Some(g) = self.tracer.work.pop() else {
710 break;
711 };
712 self.tracer.walk(objs, maps, strings, g);
713 spent += 1;
714 }
715 self.mark_units += (spent - before) as usize;
716 if self.tracer.work.is_empty() {
717 if TORTURE {
718 self.verify(objs, maps, strings, roots);
719 }
720 // The next cycle marks about as much as this one did,
721 // and the program allocates from the reserve while it
722 // does.
723 self.reserve = (self.mark_units + SLICE as usize) / ALLOC_DEBT as usize;
724 self.phase = Phase::Sweep(Swept::Arrays);
725 self.sweep_word = 0;
726 self.swept_free = self.free_at_begin[0];
727 }
728 }
729 Phase::Sweep(pool) => {
730 spent += self.sweep(pool, objs, maps, strings, budget - spent);
731 }
732 }
733 }
734 spent
735 }
736
737 /// Adds a step of `spent` units to the totals.
738 fn record(&mut self, spent: u32) {
739 self.units += u64::from(spent);
740 self.largest_slice = self.largest_slice.max(spent);
741 }
742
743 /// Sweeps `pool` for at most about `budget` units, moving on to the next
744 /// pool, or ending the cycle, once it is done. Answers the units spent.
745 fn sweep(
746 &mut self,
747 pool: Swept,
748 objs: &mut ObjectPool,
749 maps: &mut MapPool,
750 strings: &mut StringPool,
751 budget: u32,
752 ) -> u32 {
753 let mut spent = 0u32;
754 let (limit, marks, free) = match pool {
755 Swept::Arrays => (
756 self.tracer.array_limit as usize,
757 &self.tracer.array_marks,
758 &mut self.held[0],
759 ),
760 Swept::Maps => (
761 self.tracer.map_limit as usize,
762 &self.tracer.map_marks,
763 &mut self.held[1],
764 ),
765 Swept::Strings => (strings.mark_limit(), strings.marks(), &mut self.held[2]),
766 };
767 let words = limit.div_ceil(64);
768 // The string marks are borrowed from the pool the freed slots are
769 // emptied in, so the sweep gathers the slots first and empties them
770 // after.
771 let freed_from = free.len();
772 while self.sweep_word < words && spent < budget {
773 let w = self.sweep_word;
774 let mut unmarked = marks.unmarked_in_word(w, limit);
775 while unmarked != 0 {
776 free.push((w * 64) as u32 + unmarked.trailing_zeros());
777 unmarked &= unmarked - 1;
778 spent += 1;
779 }
780 spent += 1;
781 self.sweep_word += 1;
782 }
783 self.swept_free += free.len() - freed_from;
784 let done = self.sweep_word >= words;
785 match pool {
786 Swept::Arrays => {
787 for &id in &self.held[0][freed_from..] {
788 let slot = objs.get_mut(id as usize);
789 if slot.capacity() > KEEP_FREED_CAPACITY {
790 *slot = Vec::new();
791 }
792 }
793 if done {
794 self.array_threshold = next_threshold(limit, self.swept_free, self.growth());
795 self.occupied_after[0] = objs.len() - self.free_len(pool);
796 self.phase = Phase::Sweep(Swept::Maps);
797 self.swept_free = self.free_at_begin[1];
798 }
799 }
800 Swept::Maps => {
801 for &id in &self.held[1][freed_from..] {
802 let slot = maps.get_mut(id as usize);
803 if slot.capacity() > KEEP_FREED_CAPACITY {
804 *slot = CandelaMap::default();
805 }
806 }
807 if done {
808 self.map_threshold = next_threshold(limit, self.swept_free, self.growth());
809 self.occupied_after[1] = maps.len() - self.free_len(pool);
810 self.phase = Phase::Sweep(Swept::Strings);
811 self.swept_free = self.free_at_begin[2];
812 }
813 }
814 Swept::Strings => {
815 // A freed slot gives up its text. Interning finds a string by
816 // comparing text across the whole pool, so a freed slot that
817 // kept its text would be handed out as a live string and then
818 // overwritten by the next allocation that reuses the slot.
819 for &id in &self.held[2][freed_from..] {
820 strings.release(id as usize);
821 }
822 if done {
823 self.string_threshold = next_threshold(limit, self.swept_free, self.growth());
824 self.occupied_after[2] = strings.len() - self.free_len(pool);
825 strings.end_marking();
826 self.finish();
827 }
828 }
829 }
830 if done {
831 self.sweep_word = 0;
832 }
833 spent
834 }
835
836 /// Ends the cycle: every free slot above each pool's reserve becomes ready
837 /// for allocations to take without asking the collector.
838 fn finish(&mut self) {
839 self.phase = Phase::Idle;
840 self.live_bytes = self.tracer.live_bytes;
841 if !TORTURE {
842 self.byte_threshold = self
843 .live_bytes
844 .saturating_mul(self.growth())
845 .max(MIN_BYTE_THRESHOLD);
846 self.byte_trigger = self.byte_threshold;
847 let reserve = self.reserve;
848 self.free(Swept::Arrays).release_above(reserve);
849 self.free(Swept::Maps).release_above(reserve);
850 self.free(Swept::Strings).release_above(reserve);
851 }
852 }
853
854 /// What an allocation does when its pool has no ready slot. With a cycle
855 /// running it adds ALLOC_DEBT units to what the program owes, and a step
856 /// pays the debt off once it reaches SLICE. With none running, a pool at
857 /// its threshold begins one, and so does any pool once the bytes allocated
858 /// since the last cycle pass the byte threshold; otherwise the pool makes
859 /// its reserve ready, since it has room to grow before it needs a cycle.
860 #[inline(never)]
861 fn on_empty(
862 &mut self,
863 pool: Swept,
864 objs: &mut ObjectPool,
865 maps: &mut MapPool,
866 strings: &mut StringPool,
867 roots: [&[Data]; 2],
868 ) {
869 if TORTURE {
870 let mut spent = 0;
871 if !self.active() {
872 spent = self.begin(objs, maps, strings, roots);
873 }
874 spent += self.advance(objs, maps, strings, roots, 1);
875 self.record(spent);
876 return;
877 }
878 if self.active() {
879 self.debt += ALLOC_DEBT;
880 if self.debt >= SLICE {
881 // Bytes can run the debt up by more than a slice at once, and
882 // what is past one slice waits for the next allocation.
883 let budget = self.debt.min(SLICE);
884 self.debt -= budget;
885 let spent = self.advance(objs, maps, strings, roots, budget);
886 self.record(spent);
887 }
888 return;
889 }
890 let (len, threshold) = match pool {
891 Swept::Arrays => (objs.len(), self.array_threshold),
892 Swept::Maps => (maps.len(), self.map_threshold),
893 Swept::Strings => (strings.len(), self.string_threshold),
894 };
895 if len >= threshold as usize || self.bytes >= self.byte_threshold {
896 let spent = self.begin(objs, maps, strings, roots);
897 self.record(spent);
898 } else {
899 self.free(pool).release_above(0);
900 }
901 }
902
903 /// Counts `n` bytes the program has just allocated: a list or map buffer
904 /// that grew, or a pooled string. Past the byte threshold the next
905 /// allocation begins a cycle; while one runs, every [`DEBT_BYTES`] add to
906 /// what the allocations owe it.
907 #[inline(always)]
908 pub(crate) fn note_bytes(&mut self, n: usize) {
909 self.bytes += n;
910 if self.bytes >= self.byte_trigger {
911 self.bytes_due();
912 }
913 }
914
915 /// [`note_bytes`](Self::note_bytes) once the count reaches its trigger.
916 #[cold]
917 #[inline(never)]
918 fn bytes_due(&mut self) {
919 if self.active() {
920 let steps = (self.bytes - self.byte_trigger) / DEBT_BYTES + 1;
921 let owed = u32::try_from(steps)
922 .unwrap_or(u32::MAX)
923 .saturating_mul(ALLOC_DEBT);
924 self.debt = self.debt.saturating_add(owed);
925 self.byte_trigger = self.bytes.saturating_add(DEBT_BYTES);
926 // The debt is paid at the next allocation, where the roots are at
927 // hand. Every free slot is held while a cycle runs, so it asks.
928 return;
929 }
930 // A cycle has to begin where the roots are at hand, which is an
931 // allocation. Holding every free slot back makes the next one ask the
932 // collector, whichever pool it is in, and it begins the cycle.
933 self.byte_trigger = usize::MAX;
934 self.free(Swept::Arrays).hold_all();
935 self.free(Swept::Maps).hold_all();
936 self.free(Swept::Strings).hold_all();
937 }
938
939 /// Checks a finished mark phase against a fresh trace from `roots`: every
940 /// object the trace reaches that the cycle is responsible for has to be
941 /// marked, or the sweep would free something the program can still read.
942 #[cold]
943 #[inline(never)]
944 fn verify(&self, objs: &ObjectPool, maps: &MapPool, strings: &StringPool, roots: [&[Data]; 2]) {
945 let t = &self.tracer;
946 let mut seen_arrays = vec![false; objs.len()];
947 let mut seen_maps = vec![false; maps.len()];
948 let mut stack: Vec<Data> = roots.iter().flat_map(|r| r.iter().copied()).collect();
949 while let Some(d) = stack.pop() {
950 if d.is_map() {
951 let id = d.as_map();
952 if std::mem::replace(&mut seen_maps[id], true) {
953 continue;
954 }
955 assert!(
956 id >= t.map_limit as usize || t.map_marks.contains(id),
957 "gc-torture: map {id} is reachable but unmarked at the end of marking"
958 );
959 for (k, v) in &maps[id] {
960 stack.push(*k);
961 stack.push(*v);
962 }
963 } else if d.is_array() || d.is_struct() || d.is_enum() {
964 let id = d.as_array();
965 if std::mem::replace(&mut seen_arrays[id], true) {
966 continue;
967 }
968 assert!(
969 id >= t.array_limit as usize || t.array_marks.contains(id),
970 "gc-torture: object {id} is reachable but unmarked at the end of marking"
971 );
972 stack.extend_from_slice(&objs[id]);
973 } else if d.is_large_str() {
974 let id = d.get_str_pool_id();
975 assert!(
976 id >= strings.mark_limit() || strings.marks().contains(id),
977 "gc-torture: string {id} is reachable but unmarked at the end of marking"
978 );
979 }
980 }
981 }
982}
983
984/// Allocates an empty array in the object pool, reusing a freed slot when
985/// there is one.
986#[inline(always)]
987pub fn alloc_array(
988 objs: &mut ObjectPool,
989 maps: &mut MapPool,
990 strings: &mut StringPool,
991 registers: &RegisterFile,
992 recursion_stack: &RegisterFile,
993 gc: &mut GcState,
994) -> u32 {
995 if let Some(id) = gc.free_arrays.pop() {
996 objs[id as usize].clear();
997 return id;
998 }
999 alloc_array_slow(objs, maps, strings, registers, recursion_stack, gc)
1000}
1001
1002/// [`alloc_array`] when no slot is ready.
1003#[inline(never)]
1004fn alloc_array_slow(
1005 objs: &mut ObjectPool,
1006 maps: &mut MapPool,
1007 strings: &mut StringPool,
1008 registers: &RegisterFile,
1009 recursion_stack: &RegisterFile,
1010 gc: &mut GcState,
1011) -> u32 {
1012 let roots = [®isters.0[..], &recursion_stack.0[..]];
1013 gc.on_empty(Swept::Arrays, objs, maps, strings, roots);
1014 if let Some(id) = gc.free(Swept::Arrays).take() {
1015 objs[id as usize].clear();
1016 return id;
1017 }
1018 let id = objs.len() as u32;
1019 objs.push(Vec::new());
1020 id
1021}
1022
1023/// Allocates an empty map in the map pool, reusing a freed slot when there is
1024/// one.
1025#[inline(always)]
1026pub fn alloc_map(
1027 objs: &mut ObjectPool,
1028 maps: &mut MapPool,
1029 strings: &mut StringPool,
1030 registers: &RegisterFile,
1031 recursion_stack: &RegisterFile,
1032 gc: &mut GcState,
1033) -> u32 {
1034 if let Some(id) = gc.free_maps.pop() {
1035 maps[id as usize].clear();
1036 return id;
1037 }
1038 let roots = [®isters.0[..], &recursion_stack.0[..]];
1039 gc.on_empty(Swept::Maps, objs, maps, strings, roots);
1040 if let Some(id) = gc.free(Swept::Maps).take() {
1041 maps[id as usize].clear();
1042 return id;
1043 }
1044 let id = maps.len() as u32;
1045 maps.push(CandelaMap::default());
1046 id
1047}
1048
1049/// Stores `s` in the string pool, reusing a freed slot when there is one, and
1050/// answers the slot. The caller has checked that `s` is too long to inline.
1051#[inline(always)]
1052pub fn alloc_string<S: PoolString>(
1053 s: S,
1054 objs: &mut ObjectPool,
1055 maps: &mut MapPool,
1056 strings: &mut StringPool,
1057 registers: &RegisterFile,
1058 recursion_stack: &RegisterFile,
1059 gc: &mut GcState,
1060) -> u64 {
1061 gc.note_bytes(s.str_len());
1062 let slot = match gc.free_strings.pop() {
1063 Some(id) => Some(id),
1064 None => {
1065 let roots = [®isters.0[..], &recursion_stack.0[..]];
1066 gc.on_empty(Swept::Strings, objs, maps, strings, roots);
1067 gc.free(Swept::Strings).take()
1068 }
1069 };
1070 if let Some(id) = slot {
1071 s.move_to_slot(strings.get_mut(id as usize));
1072 u64::from(id)
1073 } else {
1074 let id = strings.len() as u64;
1075 s.push_to_pool(strings);
1076 id
1077 }
1078}
1079
1080/// The pools and collector a value built outside the instruction stream
1081/// allocates through, with the roots a collection it starts reads.
1082///
1083/// Every allocation here can start or carry on a collection, so whatever the
1084/// builder holds has to be reachable from `registers` or `recursion_stack`
1085/// before it allocates again. A nested value is built top-down to keep it so:
1086/// the outermost container sits on `recursion_stack` while it is filled, and
1087/// each entry goes into its parent before anything inside it is allocated.
1088pub(crate) struct Heap<'a> {
1089 pub objs: &'a mut ObjectPool,
1090 pub maps: &'a mut MapPool,
1091 pub strings: &'a mut StringPool,
1092 pub gc: &'a mut GcState,
1093 pub registers: &'a RegisterFile,
1094 pub recursion_stack: &'a mut RegisterFile,
1095}
1096
1097impl Heap<'_> {
1098 /// A new empty list.
1099 pub fn array(&mut self) -> Data {
1100 Data::array(alloc_array(
1101 self.objs,
1102 self.maps,
1103 self.strings,
1104 self.registers,
1105 self.recursion_stack,
1106 self.gc,
1107 ))
1108 }
1109
1110 /// A new empty map.
1111 pub fn map(&mut self) -> Data {
1112 Data::map(alloc_map(
1113 self.objs,
1114 self.maps,
1115 self.strings,
1116 self.registers,
1117 self.recursion_stack,
1118 self.gc,
1119 ))
1120 }
1121
1122 /// `s` as a value, sharing the pool slot of an equal string, since a map
1123 /// finds a string key by its slot.
1124 pub fn string(&mut self, s: &str) -> Data {
1125 Data::interned(s, self.strings).unwrap_or_else(|| {
1126 Data::string(
1127 s,
1128 self.objs,
1129 self.maps,
1130 self.strings,
1131 self.registers,
1132 self.recursion_stack,
1133 self.gc,
1134 )
1135 })
1136 }
1137}
1138
1139/// The deletion barrier: marks `old`, a value a store is about to overwrite or
1140/// remove from the heap while marking runs. Out of line, so a store pays one
1141/// test of [`GcState::marking`] when no cycle is marking.
1142#[cold]
1143#[inline(never)]
1144pub fn shade_overwritten(gc: &mut GcState, strings: &mut StringPool, old: Data) {
1145 gc.tracer.shade(strings, old);
1146}
1147
1148/// Walks list `id` again from its first entry, if marking has already reached
1149/// it. Called after its entries moved in place.
1150#[cold]
1151#[inline(never)]
1152pub fn rescan_list(gc: &mut GcState, id: usize) {
1153 let t = &mut gc.tracer;
1154 if id < t.array_limit as usize && t.array_marks.contains(id) {
1155 t.work.push(Grey {
1156 id: id as u32,
1157 from: 0,
1158 map: false,
1159 });
1160 }
1161}
1162
1163/// Walks map `id` again from its first entry, if marking has already reached
1164/// it. Called after its entries moved in place.
1165#[cold]
1166#[inline(never)]
1167pub fn rescan_map(gc: &mut GcState, id: usize) {
1168 let t = &mut gc.tracer;
1169 if id < t.map_limit as usize && t.map_marks.contains(id) {
1170 t.work.push(Grey {
1171 id: id as u32,
1172 from: 0,
1173 map: true,
1174 });
1175 }
1176}