gc_arena/
metrics.rs

1use alloc::rc::Rc;
2use core::cell::Cell;
3
4/// Tuning parameters for a given garbage collected [`crate::Arena`].
5#[derive(Debug, Copy, Clone)]
6pub struct Pacing {
7    pub(crate) pause_factor: f64,
8    pub(crate) debit_factor: f64,
9    pub(crate) min_sleep: usize,
10}
11
12/// Creates a default `Pacing` with `pause_factor` set to 0.5, `timing_factor` set to 1.5,
13/// and `min_sleep` set to 4096.
14impl Default for Pacing {
15    #[inline]
16    fn default() -> Pacing {
17        const PAUSE_FACTOR: f64 = 0.5;
18        const TIMING_FACTOR: f64 = 1.5;
19        const MIN_SLEEP: usize = 4096;
20
21        Pacing {
22            pause_factor: PAUSE_FACTOR,
23            debit_factor: timing_to_debit_factor(TIMING_FACTOR),
24            min_sleep: MIN_SLEEP,
25        }
26    }
27}
28
29impl Pacing {
30    /// The garbage collector will wait until the live size reaches `<current heap size> + <previous
31    /// retained size> * pause_multiplier` before beginning a new collection. Must be >= 0.0,
32    /// setting this to 0.0 causes the collector to never sleep longer than `min_sleep` before
33    /// beginning a new collection.
34    #[inline]
35    pub fn with_pause_factor(mut self, pause_factor: f64) -> Pacing {
36        assert!(pause_factor >= 0.0);
37        self.pause_factor = pause_factor;
38        self
39    }
40
41    /// The garbage collector will try and finish a collection by the time `<current heap size> *
42    /// timing_factor` additional bytes are allocated. For example, if the collection is started
43    /// when the arena has 100KB live data, and the timing_multiplier is 1.0, the collector should
44    /// finish its final phase of this collection after another 100KB has been allocated. Must be >=
45    /// 0.0, setting this to 0.0 causes the collector to behave like a stop-the-world collector.
46    #[inline]
47    pub fn with_timing_factor(mut self, timing_factor: f64) -> Pacing {
48        self.debit_factor = timing_to_debit_factor(timing_factor);
49        self
50    }
51
52    /// The minimum allocation amount during sleep before the arena starts collecting again. This is
53    /// mostly useful when the heap is very small to prevent rapidly restarting collections.
54    #[inline]
55    pub fn with_min_sleep(mut self, min_sleep: usize) -> Pacing {
56        self.min_sleep = min_sleep;
57        self
58    }
59}
60
61#[derive(Debug, Default)]
62struct MetricsInner {
63    pacing: Cell<Pacing>,
64
65    total_gcs: Cell<usize>,
66    total_gc_bytes: Cell<usize>,
67    total_external_bytes: Cell<usize>,
68
69    wakeup_amount: Cell<f64>,
70
71    allocated_gc_bytes: Cell<usize>,
72    traced_gcs: Cell<usize>,
73    traced_gc_bytes: Cell<usize>,
74    freed_gc_bytes: Cell<usize>,
75
76    remembered_gcs: Cell<usize>,
77    remembered_gc_bytes: Cell<usize>,
78
79    allocated_external_bytes: Cell<usize>,
80    freed_external_bytes: Cell<usize>,
81}
82
83#[derive(Clone)]
84pub struct Metrics(Rc<MetricsInner>);
85
86impl Metrics {
87    pub(crate) fn new() -> Self {
88        let this = Self(Default::default());
89        this.start_cycle();
90        this
91    }
92
93    /// Return a value identifying the arena, for logging purposes.
94    #[cfg(feature = "tracing")]
95    pub(crate) fn arena_id(&self) -> tracing::field::DebugValue<*const ()> {
96        // Be very cheeky and use the `Metrics` address as a (temporally) unique ID.
97        // TODO: use a monotonically increasing global counter instead?
98        tracing::field::debug(Rc::as_ptr(&self.0) as *const ())
99    }
100
101    /// Sets the pacing parameters used by the collection algorithm.
102    ///
103    /// The factors that affect the gc pause time will not take effect until the start of the next
104    /// collection.
105    #[inline]
106    pub fn set_pacing(&self, pacing: Pacing) {
107        self.0.pacing.set(pacing);
108    }
109
110    /// Returns the total bytes allocated by the arena itself, used as the backing storage for `Gc`
111    /// pointers.
112    #[inline]
113    pub fn total_gc_allocation(&self) -> usize {
114        self.0.total_gc_bytes.get()
115    }
116
117    /// Returns the total bytes that have been marked as externally allocated.
118    ///
119    /// A call to `Metrics::mark_external_allocation` will increase this count, and a call to
120    /// `Metrics::mark_external_deallocation` will decrease it.
121    #[inline]
122    pub fn total_external_allocation(&self) -> usize {
123        self.0.total_external_bytes.get()
124    }
125
126    /// Returns the sum of `Metrics::total_gc_allocation()` and
127    /// `Metrics::total_external_allocation()`.
128    #[inline]
129    pub fn total_allocation(&self) -> usize {
130        self.0
131            .total_gc_bytes
132            .get()
133            .saturating_add(self.0.total_external_bytes.get())
134    }
135
136    /// All arena allocation causes the arena to accumulate "allocation debt". This debt is then
137    /// used to time incremental garbage collection based on the tuning parameters in the current
138    /// `Pacing`. The allocation debt is measured in bytes, but will generally increase at a rate
139    /// faster than that of allocation so that collection will always complete.
140    #[inline]
141    pub fn allocation_debt(&self) -> f64 {
142        let total_gcs = self.0.total_gcs.get();
143        if total_gcs == 0 {
144            // If we have no live `Gc`s, then there is no possible collection to do so always
145            // return zero debt.
146            return 0.0;
147        }
148
149        // Every allocation in a cycle is a debit.
150        let raw_cycle_debits =
151            self.0.allocated_gc_bytes.get() as f64 + self.0.allocated_external_bytes.get() as f64;
152
153        // We do not begin collection at all until the allocations in a cycle reach the wakeup
154        // amount.
155        let cycle_debits = raw_cycle_debits - self.0.wakeup_amount.get();
156        if cycle_debits <= 0.0 {
157            return 0.0;
158        }
159
160        // Estimate the amount of external memory that has been traced assuming that each Gc
161        // owns an even share of the external memory.
162        let traced_external_estimate = self.0.traced_gcs.get() as f64 / total_gcs as f64
163            * self.0.total_external_bytes.get() as f64;
164
165        // Tracing or freeing memory counts as a credit.
166        let cycle_credits = self.0.traced_gc_bytes.get() as f64
167            + self.0.freed_gc_bytes.get() as f64
168            + traced_external_estimate
169            + self.0.freed_external_bytes.get() as f64;
170
171        let debt = cycle_debits * self.0.pacing.get().debit_factor - cycle_credits;
172
173        debt.max(0.0)
174    }
175
176    /// Call to mark that bytes have been externally allocated that are owned by an arena.
177    ///
178    /// This affects the GC pacing, marking external bytes as allocated will trigger allocation
179    /// debt.
180    #[inline]
181    pub fn mark_external_allocation(&self, bytes: usize) {
182        cell_update(&self.0.total_external_bytes, |b| b.saturating_add(bytes));
183        cell_update(&self.0.allocated_external_bytes, |b| {
184            b.saturating_add(bytes)
185        });
186    }
187
188    /// Call to mark that bytes which have been marked as allocated with
189    /// `Metrics::mark_external_allocation` have been since deallocated.
190    ///
191    /// This affects the GC pacing, marking external bytes as deallocated will reduce allocation
192    /// debt.
193    ///
194    /// It is safe, but may result in unspecified behavior (such as very weird or non-existent gc
195    /// pacing), if the amount of bytes marked for deallocation is greater than the number of bytes
196    /// marked for allocation.
197    #[inline]
198    pub fn mark_external_deallocation(&self, bytes: usize) {
199        cell_update(&self.0.total_external_bytes, |b| b.saturating_sub(bytes));
200        cell_update(&self.0.freed_external_bytes, |b| b.saturating_add(bytes));
201    }
202
203    pub(crate) fn start_cycle(&self) {
204        let pacing = self.0.pacing.get();
205        let total_gcs = self.0.total_gcs.get();
206        let wakeup_amount = if total_gcs == 0 {
207            // If we have no live `Gc`s, then the root cannot possibly own any data, so we should
208            // sleep for `min_sleep.`
209            pacing.min_sleep as f64
210        } else {
211            // Estimate the amount of external memory that is remembered assuming that each Gc owns
212            // an even share of the external memory.
213            let remembered_external_size_estimate = self.0.remembered_gcs.get() as f64
214                / total_gcs as f64
215                * self.0.total_external_bytes.get() as f64;
216
217            let remembered_size =
218                self.0.remembered_gc_bytes.get() as f64 + remembered_external_size_estimate;
219
220            (remembered_size * pacing.pause_factor).max(pacing.min_sleep as f64)
221        };
222
223        self.0.wakeup_amount.set(wakeup_amount);
224        self.0.allocated_gc_bytes.set(0);
225        self.0.freed_gc_bytes.set(0);
226        self.0.traced_gcs.set(0);
227        self.0.traced_gc_bytes.set(0);
228        self.0.remembered_gcs.set(0);
229        self.0.remembered_gc_bytes.set(0);
230        self.0.allocated_external_bytes.set(0);
231        self.0.freed_external_bytes.set(0);
232    }
233
234    #[inline]
235    pub(crate) fn mark_gc_allocated(&self, bytes: usize) {
236        cell_update(&self.0.total_gcs, |c| c + 1);
237        cell_update(&self.0.total_gc_bytes, |b| b + bytes);
238        cell_update(&self.0.allocated_gc_bytes, |b| b.saturating_add(bytes));
239    }
240
241    #[inline]
242    pub(crate) fn mark_gc_traced(&self, bytes: usize) {
243        cell_update(&self.0.traced_gcs, |c| c.saturating_add(1));
244        cell_update(&self.0.traced_gc_bytes, |b| b.saturating_add(bytes));
245    }
246
247    #[inline]
248    pub(crate) fn mark_gc_deallocated(&self, bytes: usize) {
249        cell_update(&self.0.total_gcs, |c| c - 1);
250        cell_update(&self.0.total_gc_bytes, |b| b - bytes);
251        cell_update(&self.0.freed_gc_bytes, |b| b.saturating_add(bytes));
252    }
253
254    #[inline]
255    pub(crate) fn mark_gc_remembered(&self, bytes: usize) {
256        cell_update(&self.0.remembered_gcs, |c| c + 1);
257        cell_update(&self.0.remembered_gc_bytes, |b| b + bytes);
258    }
259}
260
261// TODO: Use `Cell::update` when it is available, see:
262// https://github.com/rust-lang/rust/issues/50186
263#[inline]
264fn cell_update<T: Copy>(c: &Cell<T>, f: impl FnOnce(T) -> T) {
265    c.set(f(c.get()))
266}
267
268// Computes the `debit_factor` (aka, the amount that debits are multiplied by) from the
269// `timing_factor` configured by the user. It should always be > 1.0.
270//
271// Debits count for `1.0 + 1.0 / timing_factor` times their actual value. This means that if
272// `wakeup_amount` is 100KB, and `timing_factor` is 1.0, then it will take 100KB more allocation
273// before the collector finishes a cycle... when the cycle_debits hit 100KB, the cycle_credits must
274// be 200KB to get back to 0 debt, and since the total heap size is now 200KB, this is a full cycle.
275//
276// Lowering `timing_factor` makes the collector behave more like a stop-the-world collector, and
277// raising it slows the collector down. This factor is configured this way because the lowest
278// allowable value (0.0) is a sensible stop-the-world behavior, and higher (finite) values are
279// slower but still valid. If `debit_factor` ended up being <= 1.0, then we could not be sure that
280// collection would ever *finish*.
281fn timing_to_debit_factor(timing_factor: f64) -> f64 {
282    assert!(timing_factor >= 0.0);
283    1.0 + 1.0 / timing_factor
284}