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}