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}