mmtk/util/heap/
gc_trigger.rs

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