mmtk/util/heap/gc_trigger.rs
1use atomic::Ordering;
2
3use crate::global_state::{GcStatus, GlobalState};
4use crate::plan::Plan;
5use crate::policy::space::Space;
6use crate::scheduler::GCWorkScheduler;
7use crate::util::constants::BYTES_IN_PAGE;
8use crate::util::conversions;
9use crate::util::options::{GCTriggerSelector, Options, DEFAULT_MAX_NURSERY, DEFAULT_MIN_NURSERY};
10use crate::vm::VMBinding;
11use crate::MMTK;
12use std::mem::MaybeUninit;
13use std::sync::atomic::AtomicUsize;
14use std::sync::Arc;
15
16/// GCTrigger is responsible for triggering GCs based on the given policy.
17/// All the decisions about heap limit and GC triggering should be resolved here.
18/// Depending on the actual policy, we may either forward the calls either to the plan
19/// or to the binding/runtime.
20pub struct GCTrigger<VM: VMBinding> {
21 /// The current plan. This is uninitialized when we create it, and later initialized
22 /// once we have a fixed address for the plan.
23 plan: MaybeUninit<&'static dyn Plan<VM = VM>>,
24 /// The triggering policy.
25 pub policy: Box<dyn GCTriggerPolicy<VM>>,
26 scheduler: Arc<GCWorkScheduler<VM>>,
27 options: Arc<Options>,
28 state: Arc<GlobalState>,
29}
30
31impl<VM: VMBinding> GCTrigger<VM> {
32 pub fn new(
33 options: Arc<Options>,
34 scheduler: Arc<GCWorkScheduler<VM>>,
35 state: Arc<GlobalState>,
36 ) -> Self {
37 GCTrigger {
38 plan: MaybeUninit::uninit(),
39 policy: match *options.gc_trigger {
40 GCTriggerSelector::FixedHeapSize(size) => Box::new(FixedHeapSizeTrigger {
41 total_pages: conversions::bytes_to_pages_up(size),
42 }),
43 GCTriggerSelector::DynamicHeapSize(min, max) => 'dynamic_heap_size: {
44 let min_pages = conversions::bytes_to_pages_up(min);
45 let max_pages = conversions::bytes_to_pages_up(max);
46
47 if *options.plan == crate::util::options::PlanSelector::NoGC {
48 warn!("Cannot use dynamic heap size with NoGC. Using fixed heap size trigger instead.");
49 break 'dynamic_heap_size Box::new(FixedHeapSizeTrigger {
50 total_pages: max_pages,
51 });
52 }
53
54 Box::new(MemBalancerTrigger::new(min_pages, max_pages))
55 }
56 GCTriggerSelector::Delegated => {
57 <VM::VMCollection as crate::vm::Collection<VM>>::create_gc_trigger()
58 }
59 },
60 options,
61 scheduler,
62 state,
63 }
64 }
65
66 /// Set the plan. This is called in `create_plan()` after we created a boxed plan.
67 pub fn set_plan(&mut self, plan: &'static dyn Plan<VM = VM>) {
68 self.plan.write(plan);
69 }
70
71 fn plan(&self) -> &dyn Plan<VM = VM> {
72 unsafe { self.plan.assume_init() }
73 }
74
75 /// Request a GC. Called by mutators when polling (during allocation) and when handling user
76 /// GC requests (e.g. `System.gc();` in Java).
77 /// Atomically check that collection is enabled, and if so, request a GC. This makes the
78 /// enabled-check and the request atomic with respect to [`GCTrigger::disable_collection`]
79 /// and [`GCTrigger::enable_collection`], so a GC is never requested after collection has
80 /// been disabled.
81 /// Returns whether a GC was actually requested.
82 fn request(&self) -> bool {
83 // `GCWorkScheduler::request_schedule_collection` needs to hold a mutex to communicate
84 // with GC workers, which is expensive for functions like `poll`. `try_request_pause`
85 // only returns `Ok` to the thread that actually wins the race to transition the status,
86 // so only that thread calls it, instead of every thread that observes the old status:
87 // calling it unconditionally would re-queue a `WorkerGoal::Gc` request that a previous
88 // winner's request already delivered and that the workers may already be acting on,
89 // tripping the `debug_is_requested` assertion in `GCWorkScheduler::on_last_parked`.
90 match self.state.gc_status.try_request_pause() {
91 Ok(cur_status) => {
92 if cur_status == GcStatus::InConcurrentGC {
93 self.plan().concurrent().unwrap().on_concurrent_work_interrupted();
94 }
95 probe!(mmtk, gc_requested);
96 self.state.record_pause_requested_time();
97 self.scheduler.request_schedule_collection();
98 true
99 },
100 Err(GcStatus::Disabled(_)) => false,
101 // A GC is genuinely required (the heap policy has been exceeded), but MMTk has no GC
102 // worker threads to service it. Silently returning `false` here would let allocation
103 // grow the heap without bound instead of respecting the configured limit.
104 Err(GcStatus::Uninitialized) => panic!(
105 "GC is not allowed here: collection is not initialized (did you call initialize_collection()?)."
106 ),
107 Err(GcStatus::PauseRequested) => true,
108 _ => unreachable!(),
109 }
110 }
111
112 /// Disable collection. On success, returns `Ok(true)` if this call actually switched
113 /// collection from enabled to disabled, `Ok(false)` if it only increased the nesting depth of
114 /// an already-disabled status. If MMTk is unable to disable GC right now (possibly a GC is in
115 /// progress, or a GC has been requested), returns `Err` with the status that prevented it;
116 /// users should invoke runtime safepoints or other mechanisms to prepare for a GC pause, and
117 /// then call this function again.
118 ///
119 /// This call is nestable. Each call must be paired with a matching call to
120 /// [`GCTrigger::enable_collection`].
121 pub fn disable_collection(&self) -> Result<bool, GcStatus> {
122 self.state.gc_status.set_disabled()
123 }
124
125 /// Re-enable collection. If collection is not currently disabled (e.g. there was no prior
126 /// matching call to [`GCTrigger::disable_collection`]), this is a no-op.
127 /// Returns `true` if this call actually re-enabled collection (i.e. it was the outermost
128 /// matching call), `false` if it only decremented the nesting depth, or if collection was
129 /// already enabled.
130 pub fn enable_collection(&self) -> bool {
131 self.state.gc_status.set_enabled()
132 }
133
134 /// Return whether collection is currently enabled.
135 pub fn is_collection_enabled(&self) -> bool {
136 !self.state.gc_status.is_disabled()
137 }
138
139 /// This method is called periodically by the allocation subsystem
140 /// (by default, each time a page is consumed), and provides the
141 /// collector with an opportunity to collect.
142 ///
143 /// Arguments:
144 /// * `space_full`: Space request failed, must recover pages within 'space'.
145 /// * `space`: The space that triggered the poll. This could `None` if the poll is not triggered by a space.
146 pub fn poll(&self, space_full: bool, space: Option<&dyn Space<VM>>) -> bool {
147 if !self.is_collection_enabled() {
148 return false;
149 }
150
151 let plan = self.plan();
152 if self
153 .policy
154 .is_gc_required(space_full, space.map(|s| SpaceStats::new(s)), plan)
155 {
156 info!(
157 "[POLL] {}{} ({}/{} pages)",
158 if let Some(space) = space {
159 format!("{}: ", space.get_name())
160 } else {
161 "".to_string()
162 },
163 "Triggering collection",
164 plan.get_reserved_pages(),
165 plan.get_total_pages(),
166 );
167 return self.request();
168 }
169 false
170 }
171
172 /// For [`crate::scheduler::GCWorkScheduler::on_last_parked`]'s use when the last parked GC
173 /// worker is about to go idle with no mutator-requested goal pending: check if we should poll
174 /// from a GC worker.
175 pub(crate) fn poll_from_last_parked_worker(&self) -> bool {
176 if !self.is_collection_enabled() {
177 return false;
178 }
179
180 // Currently only poll if a concurrent GC is in progress, and only if that work has actually drained.
181 let Some(concurrent_plan) = self.plan().concurrent() else {
182 return false;
183 };
184 if !concurrent_plan.concurrent_work_in_progress() {
185 return false;
186 }
187 if !self.scheduler.work_buckets[crate::scheduler::WorkBucketStage::Concurrent].is_drained()
188 {
189 return false;
190 }
191
192 let plan = self.plan();
193 if self.policy.is_gc_required(false, None, plan) {
194 match self.state.gc_status.try_request_pause() {
195 // This call won the race to request a GC. However, we cannot call request() now.
196 // The caller of this function is holding a mutex, and if we do request() here,
197 // we end up with deadlock. So we just return true to the caller, and let the caller do the request.
198 Ok(_) => {
199 probe!(mmtk, gc_requested);
200 self.state.record_pause_requested_time();
201 info!(
202 "[POLL] Requesting a concurrent GC's closing pause from the last parked GC worker"
203 );
204 true
205 }
206 Err(GcStatus::Disabled(_)) | Err(GcStatus::PauseRequested) => false,
207 Err(GcStatus::Uninitialized) => panic!(
208 "GC is not allowed here: collection is not initialized (did you call initialize_collection()?)."
209 ),
210 _ => unreachable!(),
211 }
212 } else {
213 false
214 }
215 }
216
217 /// This method is called when the user manually requests a collection, such as `System.gc()` in Java.
218 /// Returns true if a collection is actually requested.
219 ///
220 /// # Arguments
221 /// * `force`: If true, we force a collection regardless of the settings. If false, we only trigger a collection if the settings allow it.
222 /// * `exhaustive`: If true, we try to make the collection exhaustive (e.g. full heap collection). If false, the collection kind is determined internally.
223 pub fn handle_user_collection_request(&self, force: bool, exhaustive: bool) -> bool {
224 if !self.plan().constraints().collects_garbage {
225 warn!("User attempted a collection request, but the plan can not do GC. The request is ignored.");
226 return false;
227 }
228
229 if force || !*self.options.ignore_system_gc && self.is_collection_enabled() {
230 info!("User triggering collection");
231 // TODO: this may not work reliably. If a GC has been triggered, this will not force it to be a full heap GC.
232 if exhaustive {
233 if let Some(gen) = self.plan().generational() {
234 gen.force_full_heap_collection();
235 }
236 }
237
238 self.state
239 .user_triggered_collection
240 .store(true, Ordering::Relaxed);
241 return self.request();
242 }
243
244 false
245 }
246
247 /// MMTK has requested stop-the-world activity (e.g., stw within a concurrent gc).
248 // TODO: We should use this for concurrent GC. E.g. in concurrent Immix, when the initial mark is done, we
249 // can use this function to immediately trigger the final mark pause. The current implementation uses
250 // normal collection_required check, which may delay the final mark unnecessarily.
251 #[allow(unused)]
252 pub fn trigger_internal_collection_request(&self) {
253 self.state
254 .last_internal_triggered_collection
255 .store(true, Ordering::Relaxed);
256 self.state
257 .internal_triggered_collection
258 .store(true, Ordering::Relaxed);
259 // TODO: The current `request()` is probably incorrect for internally triggered GC.
260 // Consider removing functions related to "internal triggered collection".
261 self.request();
262 // TODO: Make sure this function works correctly for concurrent GC.
263 unimplemented!()
264 }
265
266 pub fn should_do_stress_gc(&self) -> bool {
267 Self::should_do_stress_gc_inner(&self.state, &self.options)
268 }
269
270 /// Check if we should do a stress GC now. If GC is initialized and the allocation bytes exceeds
271 /// the stress factor, we should do a stress GC.
272 pub(crate) fn should_do_stress_gc_inner(state: &GlobalState, options: &Options) -> bool {
273 state.is_initialized()
274 && (state.allocation_bytes.load(Ordering::SeqCst) > *options.stress_factor)
275 }
276
277 /// Check if the heap is full
278 pub fn is_heap_full(&self) -> bool {
279 self.policy.is_heap_full(self.plan())
280 }
281
282 /// Return upper bound of the nursery size (in number of bytes)
283 pub fn get_max_nursery_bytes(&self) -> usize {
284 use crate::util::options::NurserySize;
285 debug_assert!(self.plan().generational().is_some());
286 match *self.options.nursery {
287 NurserySize::Bounded { min: _, max } => max,
288 NurserySize::ProportionalBounded { min: _, max } => {
289 let heap_size_bytes =
290 conversions::pages_to_bytes(self.policy.get_current_heap_size_in_pages());
291 let max_bytes = heap_size_bytes as f64 * max;
292 let max_bytes = conversions::raw_align_up(max_bytes as usize, BYTES_IN_PAGE);
293 if max_bytes > DEFAULT_MAX_NURSERY {
294 warn!("Proportional nursery with max size {} ({}) is larger than DEFAULT_MAX_NURSERY ({}). Use DEFAULT_MAX_NURSERY instead.", max, max_bytes, DEFAULT_MAX_NURSERY);
295 DEFAULT_MAX_NURSERY
296 } else {
297 max_bytes
298 }
299 }
300 NurserySize::Fixed(sz) => sz,
301 }
302 }
303
304 /// Return lower bound of the nursery size (in number of bytes)
305 pub fn get_min_nursery_bytes(&self) -> usize {
306 use crate::util::options::NurserySize;
307 debug_assert!(self.plan().generational().is_some());
308 match *self.options.nursery {
309 NurserySize::Bounded { min, max: _ } => min,
310 NurserySize::ProportionalBounded { min, max: _ } => {
311 let min_bytes =
312 conversions::pages_to_bytes(self.policy.get_current_heap_size_in_pages())
313 as f64
314 * min;
315 let min_bytes = conversions::raw_align_up(min_bytes as usize, BYTES_IN_PAGE);
316 if min_bytes < DEFAULT_MIN_NURSERY {
317 warn!("Proportional nursery with min size {} ({}) is smaller than DEFAULT_MIN_NURSERY ({}). Use DEFAULT_MIN_NURSERY instead.", min, min_bytes, DEFAULT_MIN_NURSERY);
318 DEFAULT_MIN_NURSERY
319 } else {
320 min_bytes
321 }
322 }
323 NurserySize::Fixed(sz) => sz,
324 }
325 }
326
327 /// Return upper bound of the nursery size (in number of pages)
328 pub fn get_max_nursery_pages(&self) -> usize {
329 crate::util::conversions::bytes_to_pages_up(self.get_max_nursery_bytes())
330 }
331
332 /// Return lower bound of the nursery size (in number of pages)
333 pub fn get_min_nursery_pages(&self) -> usize {
334 crate::util::conversions::bytes_to_pages_up(self.get_min_nursery_bytes())
335 }
336
337 /// A check for the obvious out-of-memory case: if the requested size is larger than
338 /// the heap size, it is definitely an OOM. We would like to identify that, and
339 /// allows the binding to deal with OOM. Without this check, we will attempt
340 /// to allocate from the page resource. If the requested size is unrealistically large
341 /// (such as `usize::MAX`), it breaks the assumptions of our implementation of
342 /// page resource, vm map, etc. This check prevents that, and allows us to
343 /// handle the OOM case.
344 /// Each allocator that may request an arbitrary size should call this method before
345 /// acquring memory from the space. For example, bump pointer allocator and large object
346 /// allocator need to call this method. On the other hand, allocators that only allocate
347 /// memory in fixed size blocks do not need to call this method.
348 /// An allocator should call this method before doing any computation on the size to
349 /// avoid arithmatic overflow. If we have to do computation in the allocation fastpath and
350 /// overflow happens there, there is nothing we can do about it.
351 /// Return a boolean to indicate if we will be out of memory, determined by the check.
352 pub fn will_oom_on_alloc(&self, size: usize) -> bool {
353 let max_pages = self.policy.get_max_heap_size_in_pages();
354 let requested_pages = size >> crate::util::constants::LOG_BYTES_IN_PAGE;
355 requested_pages > max_pages
356 }
357}
358
359/// Provides statistics about the space. This is exposed to bindings, as it is used
360/// in both [`crate::plan::Plan`] and [`GCTriggerPolicy`].
361// This type exists so we do not need to expose the `Space` trait to the bindings.
362pub struct SpaceStats<'a, VM: VMBinding>(pub(crate) &'a dyn Space<VM>);
363
364impl<'a, VM: VMBinding> SpaceStats<'a, VM> {
365 /// Create new SpaceStats.
366 fn new(space: &'a dyn Space<VM>) -> Self {
367 Self(space)
368 }
369
370 /// Get the number of reserved pages for the space.
371 pub fn reserved_pages(&self) -> usize {
372 self.0.reserved_pages()
373 }
374
375 // We may expose more methods to bindings if they need more information for implementing GC triggers.
376 // But we should never expose `Space` itself.
377}
378
379/// This trait describes a GC trigger policy. A triggering policy have hooks to be informed about
380/// GC start/end so they can collect some statistics about GC and allocation. The policy needs to
381/// decide the (current) heap limit and decide whether a GC should be performed.
382pub trait GCTriggerPolicy<VM: VMBinding>: Sync + Send {
383 /// Inform the triggering policy that we have pending allocation.
384 /// Any GC trigger policy with dynamic heap size should take this into account when calculating a new heap size.
385 /// Failing to do so may result in unnecessay GCs, or result in an infinite loop if the new heap size
386 /// can never accomodate the pending allocation.
387 fn on_pending_allocation(&self, _pages: usize) {}
388 /// Inform the triggering policy that a GC cycle starts. A GC cycle consists of one or more
389 /// GC pauses (see [`Self::on_pause_start`]) plus any concurrent work in between the pauses.
390 /// For a stop-the-world GC, a GC cycle is just a single pause, and this is called at the same
391 /// time as [`Self::on_pause_start`]. For a concurrent GC that splits a cycle into multiple
392 /// pauses (e.g. an initial mark pause and a final mark pause with concurrent marking in
393 /// between), this is only called once per cycle, for the first pause in the cycle.
394 fn on_gc_start(&self, _mmtk: &'static MMTK<VM>) {}
395 /// Inform the triggering policy that a GC cycle ends. See [`Self::on_gc_start`] for what
396 /// a GC cycle is. This is only called once per GC cycle, for the last pause in the cycle.
397 fn on_gc_end(&self, _mmtk: &'static MMTK<VM>) {}
398 /// Inform the triggering policy that a pause starts. For a concurrent GC, this is called once
399 /// for every STW pause in a GC cycle, not just once per cycle. See
400 /// [`Self::on_gc_start`] for the hook that is only called once per GC cycle.
401 fn on_pause_start(&self, _mmtk: &'static MMTK<VM>) {}
402 /// Inform the triggering policy that a pause ends. For a concurrent GC, this is called once
403 /// for every STW pause in a GC cycle, not just once per cycle. See [`Self::on_gc_end`]
404 /// for the hook that is only called once per GC cycle.
405 fn on_pause_end(&self, _mmtk: &'static MMTK<VM>) {}
406 /// Inform the triggering policy that a GC is about to start the release work. This is called
407 /// in the global Release work packet. This means we assume a plan
408 /// do not schedule any work that reclaims memory before the global `Release` work. The current plans
409 /// satisfy this assumption: they schedule other release work in `plan.release()`.
410 fn on_gc_release(&self, _mmtk: &'static MMTK<VM>) {}
411 /// Is a GC required now? The GC trigger may implement its own heuristics to decide when
412 /// a GC should be performed. However, we recommend the implementation to do its own checks
413 /// first, and always call `plan.collection_required(space_full, space)` at the end as a fallback to see if the plan needs
414 /// to do a GC.
415 ///
416 /// Arguments:
417 /// * `space_full`: Is any space full?
418 /// * `space`: The space that is full. The GC trigger may access some stats of the space.
419 /// * `plan`: The reference to the plan in use.
420 fn is_gc_required(
421 &self,
422 space_full: bool,
423 space: Option<SpaceStats<VM>>,
424 plan: &dyn Plan<VM = VM>,
425 ) -> bool;
426 /// Is current heap full?
427 fn is_heap_full(&self, plan: &dyn Plan<VM = VM>) -> bool;
428 /// Return the current heap size (in pages)
429 fn get_current_heap_size_in_pages(&self) -> usize;
430 /// Return the upper bound of heap size
431 fn get_max_heap_size_in_pages(&self) -> usize;
432 /// Can the heap size grow?
433 fn can_heap_size_grow(&self) -> bool;
434}
435
436/// A simple GC trigger that uses a fixed heap size.
437pub struct FixedHeapSizeTrigger {
438 total_pages: usize,
439}
440impl<VM: VMBinding> GCTriggerPolicy<VM> for FixedHeapSizeTrigger {
441 fn is_gc_required(
442 &self,
443 space_full: bool,
444 space: Option<SpaceStats<VM>>,
445 plan: &dyn Plan<VM = VM>,
446 ) -> bool {
447 // Let the plan decide
448 plan.collection_required(space_full, space)
449 }
450
451 fn is_heap_full(&self, plan: &dyn Plan<VM = VM>) -> bool {
452 // If reserved pages is larger than the total pages, the heap is full.
453 plan.get_reserved_pages() > self.total_pages
454 }
455
456 fn get_current_heap_size_in_pages(&self) -> usize {
457 self.total_pages
458 }
459
460 fn get_max_heap_size_in_pages(&self) -> usize {
461 self.total_pages
462 }
463
464 fn can_heap_size_grow(&self) -> bool {
465 false
466 }
467}
468
469use atomic_refcell::AtomicRefCell;
470use std::time::Instant;
471
472/// An implementation of MemBalancer (Optimal heap limits for reducing browser memory use, <https://dl.acm.org/doi/10.1145/3563323>)
473/// We use MemBalancer to decide a heap limit between the min heap and the max heap.
474/// The current implementation is a simplified version of mem balancer and it does not take collection/allocation speed into account,
475/// and uses a fixed constant instead.
476// TODO: implement a complete mem balancer.
477pub struct MemBalancerTrigger {
478 /// The min heap size
479 min_heap_pages: usize,
480 /// The max heap size
481 max_heap_pages: usize,
482 /// The current heap size
483 current_heap_pages: AtomicUsize,
484 /// The number of pending allocation pages. The allocation requests for them have failed, and a GC is triggered.
485 /// We will need to take them into consideration so that the new heap size can accomodate those allocations.
486 pending_pages: AtomicUsize,
487 /// Statistics
488 stats: AtomicRefCell<MemBalancerStats>,
489}
490
491#[derive(Copy, Clone, Debug)]
492struct MemBalancerStats {
493 // Allocation/collection stats in the previous estimation. We keep this so we can use them to smooth the current value
494 /// Previous allocated memory in pages.
495 allocation_pages_prev: Option<f64>,
496 /// Previous allocation duration in secs
497 allocation_time_prev: Option<f64>,
498 /// Previous collected memory in pages
499 collection_pages_prev: Option<f64>,
500 /// Previous colleciton duration in secs
501 collection_time_prev: Option<f64>,
502
503 // Allocation/collection stats in this estimation.
504 /// Allocated memory in pages
505 allocation_pages: f64,
506 /// Allocation duration in secs
507 allocation_time: f64,
508 /// Collected memory in pages (memory traversed during collection)
509 collection_pages: f64,
510 /// Collection duration in secs
511 collection_time: f64,
512
513 /// The time when this GC starts
514 gc_start_time: Instant,
515 /// The time when this GC ends
516 gc_end_time: Instant,
517
518 /// The live pages before we release memory.
519 gc_release_live_pages: usize,
520 /// The live pages at the GC end
521 gc_end_live_pages: usize,
522}
523
524impl std::default::Default for MemBalancerStats {
525 fn default() -> Self {
526 let now = Instant::now();
527 Self {
528 allocation_pages_prev: None,
529 allocation_time_prev: None,
530 collection_pages_prev: None,
531 collection_time_prev: None,
532 allocation_pages: 0f64,
533 allocation_time: 0f64,
534 collection_pages: 0f64,
535 collection_time: 0f64,
536 gc_start_time: now,
537 gc_end_time: now,
538 gc_release_live_pages: 0,
539 gc_end_live_pages: 0,
540 }
541 }
542}
543
544use crate::plan::GenerationalPlan;
545
546impl MemBalancerStats {
547 // Collect mem stats for generational plans:
548 // * We ignore nursery GCs.
549 // * allocation = objects in mature space = promoted + pretentured = live pages in mature space before release - live pages at the end of last mature GC
550 // * collection = live pages in mature space at the end of GC - live pages in mature space before release
551
552 fn generational_mem_stats_on_gc_start<VM: VMBinding>(
553 &mut self,
554 _plan: &dyn GenerationalPlan<VM = VM>,
555 ) {
556 // We don't need to do anything
557 }
558 fn generational_mem_stats_on_gc_release<VM: VMBinding>(
559 &mut self,
560 plan: &dyn GenerationalPlan<VM = VM>,
561 ) {
562 if !plan.is_current_gc_nursery() {
563 self.gc_release_live_pages = plan.get_mature_reserved_pages();
564
565 // Calculate the promoted pages (including pre tentured objects)
566 let promoted = self
567 .gc_release_live_pages
568 .saturating_sub(self.gc_end_live_pages);
569 self.allocation_pages = promoted as f64;
570 trace!(
571 "promoted = mature live before release {} - mature live at prev gc end {} = {}",
572 self.gc_release_live_pages,
573 self.gc_end_live_pages,
574 promoted
575 );
576 trace!(
577 "allocated pages (accumulated to) = {}",
578 self.allocation_pages
579 );
580 }
581 }
582 /// Return true if we should compute a new heap limit. Only do so at the end of a mature GC
583 fn generational_mem_stats_on_gc_end<VM: VMBinding>(
584 &mut self,
585 plan: &dyn GenerationalPlan<VM = VM>,
586 ) -> bool {
587 if !plan.is_current_gc_nursery() {
588 self.gc_end_live_pages = plan.get_mature_reserved_pages();
589 // Use live pages as an estimate for pages traversed during GC
590 self.collection_pages = self.gc_end_live_pages as f64;
591 trace!(
592 "collected pages = mature live at gc end {} - mature live at gc release {} = {}",
593 self.gc_release_live_pages,
594 self.gc_end_live_pages,
595 self.collection_pages
596 );
597 true
598 } else {
599 false
600 }
601 }
602
603 // Collect mem stats for non generational plans
604 // * allocation = live pages at the start of GC - live pages at the end of last GC
605 // * collection = live pages at the end of GC - live pages before release
606
607 fn non_generational_mem_stats_on_gc_start<VM: VMBinding>(&mut self, mmtk: &'static MMTK<VM>) {
608 self.allocation_pages = mmtk
609 .get_plan()
610 .get_reserved_pages()
611 .saturating_sub(self.gc_end_live_pages) as f64;
612 trace!(
613 "allocated pages = used {} - live in last gc {} = {}",
614 mmtk.get_plan().get_reserved_pages(),
615 self.gc_end_live_pages,
616 self.allocation_pages
617 );
618 }
619 fn non_generational_mem_stats_on_gc_release<VM: VMBinding>(&mut self, mmtk: &'static MMTK<VM>) {
620 self.gc_release_live_pages = mmtk.get_plan().get_reserved_pages();
621 trace!("live before release = {}", self.gc_release_live_pages);
622 }
623 fn non_generational_mem_stats_on_gc_end<VM: VMBinding>(&mut self, mmtk: &'static MMTK<VM>) {
624 self.gc_end_live_pages = mmtk.get_plan().get_reserved_pages();
625 trace!("live pages = {}", self.gc_end_live_pages);
626 // Use live pages as an estimate for pages traversed during GC
627 self.collection_pages = self.gc_end_live_pages as f64;
628 trace!(
629 "collected pages = live at gc end {} - live at gc release {} = {}",
630 self.gc_release_live_pages,
631 self.gc_end_live_pages,
632 self.collection_pages
633 );
634 }
635}
636
637impl<VM: VMBinding> GCTriggerPolicy<VM> for MemBalancerTrigger {
638 fn is_gc_required(
639 &self,
640 space_full: bool,
641 space: Option<SpaceStats<VM>>,
642 plan: &dyn Plan<VM = VM>,
643 ) -> bool {
644 // Let the plan decide
645 plan.collection_required(space_full, space)
646 }
647
648 fn on_pending_allocation(&self, pages: usize) {
649 self.pending_pages.fetch_add(pages, Ordering::SeqCst);
650 }
651
652 fn on_gc_start(&self, mmtk: &'static MMTK<VM>) {
653 trace!("=== on_gc_start ===");
654 self.access_stats(|stats| {
655 stats.gc_start_time = Instant::now();
656 stats.allocation_time += (stats.gc_start_time - stats.gc_end_time).as_secs_f64();
657 trace!(
658 "gc_start = {:?}, allocation_time = {}",
659 stats.gc_start_time,
660 stats.allocation_time
661 );
662
663 if let Some(plan) = mmtk.get_plan().generational() {
664 stats.generational_mem_stats_on_gc_start(plan);
665 } else {
666 stats.non_generational_mem_stats_on_gc_start(mmtk);
667 }
668 });
669 }
670
671 fn on_gc_release(&self, mmtk: &'static MMTK<VM>) {
672 trace!("=== on_gc_release ===");
673 self.access_stats(|stats| {
674 if let Some(plan) = mmtk.get_plan().generational() {
675 stats.generational_mem_stats_on_gc_release(plan);
676 } else {
677 stats.non_generational_mem_stats_on_gc_release(mmtk);
678 }
679 });
680 }
681
682 fn on_gc_end(&self, mmtk: &'static MMTK<VM>) {
683 trace!("=== on_gc_end ===");
684 self.access_stats(|stats| {
685 stats.gc_end_time = Instant::now();
686 stats.collection_time += (stats.gc_end_time - stats.gc_start_time).as_secs_f64();
687 trace!(
688 "gc_end = {:?}, collection_time = {}",
689 stats.gc_end_time,
690 stats.collection_time
691 );
692
693 if let Some(plan) = mmtk.get_plan().generational() {
694 if stats.generational_mem_stats_on_gc_end(plan) {
695 self.compute_new_heap_limit(
696 mmtk.get_plan().get_reserved_pages(),
697 // We reserve an extra of min nursery. This ensures that we will not trigger
698 // a full heap GC in the next GC (if available pages is smaller than min nursery, we will force a full heap GC)
699 mmtk.get_plan().get_collection_reserved_pages()
700 + mmtk.gc_trigger.get_min_nursery_pages(),
701 stats,
702 );
703 }
704 } else {
705 stats.non_generational_mem_stats_on_gc_end(mmtk);
706 self.compute_new_heap_limit(
707 mmtk.get_plan().get_reserved_pages(),
708 mmtk.get_plan().get_collection_reserved_pages(),
709 stats,
710 );
711 }
712 });
713 // Clear pending allocation pages at the end of GC, no matter we used it or not.
714 self.pending_pages.store(0, Ordering::SeqCst);
715 }
716
717 fn is_heap_full(&self, plan: &dyn Plan<VM = VM>) -> bool {
718 // If reserved pages is larger than the current heap size, the heap is full.
719 plan.get_reserved_pages() > self.current_heap_pages.load(Ordering::Relaxed)
720 }
721
722 fn get_current_heap_size_in_pages(&self) -> usize {
723 self.current_heap_pages.load(Ordering::Relaxed)
724 }
725
726 fn get_max_heap_size_in_pages(&self) -> usize {
727 self.max_heap_pages
728 }
729
730 fn can_heap_size_grow(&self) -> bool {
731 self.current_heap_pages.load(Ordering::Relaxed) < self.max_heap_pages
732 }
733}
734impl MemBalancerTrigger {
735 fn new(min_heap_pages: usize, max_heap_pages: usize) -> Self {
736 Self {
737 min_heap_pages,
738 max_heap_pages,
739 pending_pages: AtomicUsize::new(0),
740 // start with min heap
741 current_heap_pages: AtomicUsize::new(min_heap_pages),
742 stats: AtomicRefCell::new(Default::default()),
743 }
744 }
745
746 fn access_stats<F>(&self, mut f: F)
747 where
748 F: FnMut(&mut MemBalancerStats),
749 {
750 let mut stats = self.stats.borrow_mut();
751 f(&mut stats);
752 }
753
754 fn compute_new_heap_limit(
755 &self,
756 live: usize,
757 extra_reserve: usize,
758 stats: &mut MemBalancerStats,
759 ) {
760 trace!("compute new heap limit: {:?}", stats);
761
762 // Constants from the original paper
763 const ALLOCATION_SMOOTH_FACTOR: f64 = 0.95;
764 const COLLECTION_SMOOTH_FACTOR: f64 = 0.5;
765 const TUNING_FACTOR: f64 = 0.2;
766
767 // Smooth memory/time for allocation/collection
768 let smooth = |prev: Option<f64>, cur, factor| {
769 prev.map(|p| p * factor + cur * (1.0f64 - factor))
770 .unwrap_or(cur)
771 };
772 let alloc_mem = smooth(
773 stats.allocation_pages_prev,
774 stats.allocation_pages,
775 ALLOCATION_SMOOTH_FACTOR,
776 );
777 let alloc_time = smooth(
778 stats.allocation_time_prev,
779 stats.allocation_time,
780 ALLOCATION_SMOOTH_FACTOR,
781 );
782 let gc_mem = smooth(
783 stats.collection_pages_prev,
784 stats.collection_pages,
785 COLLECTION_SMOOTH_FACTOR,
786 );
787 let gc_time = smooth(
788 stats.collection_time_prev,
789 stats.collection_time,
790 COLLECTION_SMOOTH_FACTOR,
791 );
792 trace!(
793 "after smoothing, alloc mem = {}, alloc_time = {}",
794 alloc_mem,
795 alloc_time
796 );
797 trace!(
798 "after smoothing, gc mem = {}, gc_time = {}",
799 gc_mem,
800 gc_time
801 );
802
803 // We got the smoothed stats. Now save the current stats as previous stats
804 stats.allocation_pages_prev = Some(stats.allocation_pages);
805 stats.allocation_pages = 0f64;
806 stats.allocation_time_prev = Some(stats.allocation_time);
807 stats.allocation_time = 0f64;
808 stats.collection_pages_prev = Some(stats.collection_pages);
809 stats.collection_pages = 0f64;
810 stats.collection_time_prev = Some(stats.collection_time);
811 stats.collection_time = 0f64;
812
813 // Calculate the square root
814 let e: f64 = if alloc_mem != 0f64 && gc_mem != 0f64 && alloc_time != 0f64 && gc_time != 0f64
815 {
816 let mut e = live as f64;
817 e *= alloc_mem / alloc_time;
818 e /= TUNING_FACTOR;
819 e /= gc_mem / gc_time;
820 e.sqrt()
821 } else {
822 // If any collected stat is abnormal, we use the fallback heuristics.
823 (live as f64 * 4096f64).sqrt()
824 };
825
826 // Get pending allocations
827 let pending_pages = self.pending_pages.load(Ordering::SeqCst);
828
829 // This is the optimal heap limit due to mem balancer. We will need to clamp the value to the defined min/max range.
830 let optimal_heap = live + e as usize + extra_reserve + pending_pages;
831 trace!(
832 "optimal = live {} + sqrt(live) {} + extra {}",
833 live,
834 e,
835 extra_reserve
836 );
837
838 // The new heap size must be within min/max.
839 let new_heap = optimal_heap.clamp(self.min_heap_pages, self.max_heap_pages);
840 debug!(
841 "MemBalander: new heap limit = {} pages (optimal = {}, clamped to [{}, {}])",
842 new_heap, optimal_heap, self.min_heap_pages, self.max_heap_pages
843 );
844 self.current_heap_pages.store(new_heap, Ordering::Relaxed);
845 }
846}