Skip to content

Commit cacb8f6

Browse files
authored
Collect live bytes during GC (mmtk#768)
This PR adds a feature `count_live_bytes_in_gc`. When enabled, each worker will maintain a counter for live bytes when scanning an object, and in `Release`, we get live bytes from all the workers, and sum it up. We also provide `memory_manager::live_bytes_in_last_gc()` for users to query this value. This is usually used to debug fragmentation.
1 parent 4c1e58a commit cacb8f6

File tree

7 files changed

+101
-3
lines changed

7 files changed

+101
-3
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ work_packet_stats = []
136136
# Count the malloc'd memory into the heap size
137137
malloc_counted_size = []
138138

139+
# Count the size of all live objects in GC
140+
count_live_bytes_in_gc = []
141+
139142
# Do not modify the following line - ci-common.sh matches it
140143
# -- Mutally exclusive features --
141144
# Only one feature from each group can be provided. Otherwise build will fail.

src/memory_manager.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,22 +534,38 @@ pub fn process_bulk(builder: &mut MMTKBuilder, options: &str) -> bool {
534534
builder.set_options_bulk_by_str(options)
535535
}
536536

537-
/// Return used memory in bytes.
537+
/// Return used memory in bytes. MMTk accounts for memory in pages, thus this method always returns a value in
538+
/// page granularity.
538539
///
539540
/// Arguments:
540541
/// * `mmtk`: A reference to an MMTk instance.
541542
pub fn used_bytes<VM: VMBinding>(mmtk: &MMTK<VM>) -> usize {
542543
mmtk.plan.get_used_pages() << LOG_BYTES_IN_PAGE
543544
}
544545

545-
/// Return free memory in bytes.
546+
/// Return free memory in bytes. MMTk accounts for memory in pages, thus this method always returns a value in
547+
/// page granularity.
546548
///
547549
/// Arguments:
548550
/// * `mmtk`: A reference to an MMTk instance.
549551
pub fn free_bytes<VM: VMBinding>(mmtk: &MMTK<VM>) -> usize {
550552
mmtk.plan.get_free_pages() << LOG_BYTES_IN_PAGE
551553
}
552554

555+
/// Return the size of all the live objects in bytes in the last GC. MMTk usually accounts for memory in pages.
556+
/// This is a special method that we count the size of every live object in a GC, and sum up the total bytes.
557+
/// We provide this method so users can compare with `used_bytes` (which does page accounting), and know if
558+
/// the heap is fragmented.
559+
/// The value returned by this method is only updated when we finish tracing in a GC. A recommended timing
560+
/// to call this method is at the end of a GC (e.g. when the runtime is about to resume threads).
561+
#[cfg(feature = "count_live_bytes_in_gc")]
562+
pub fn live_bytes_in_last_gc<VM: VMBinding>(mmtk: &MMTK<VM>) -> usize {
563+
mmtk.plan
564+
.base()
565+
.live_bytes_in_last_gc
566+
.load(Ordering::SeqCst)
567+
}
568+
553569
/// Return the starting address of the heap. *Note that currently MMTk uses
554570
/// a fixed address range as heap.*
555571
pub fn starting_heap_address() -> Address {

src/plan/global.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ pub struct BasePlan<VM: VMBinding> {
417417
/// A counteer that keeps tracks of the number of bytes allocated by malloc
418418
#[cfg(feature = "malloc_counted_size")]
419419
malloc_bytes: AtomicUsize,
420+
/// This stores the size in bytes for all the live objects in last GC. This counter is only updated in the GC release phase.
421+
#[cfg(feature = "count_live_bytes_in_gc")]
422+
pub live_bytes_in_last_gc: AtomicUsize,
420423
/// Wrapper around analysis counters
421424
#[cfg(feature = "analysis")]
422425
pub analysis_manager: AnalysisManager<VM>,
@@ -547,6 +550,8 @@ impl<VM: VMBinding> BasePlan<VM> {
547550
allocation_bytes: AtomicUsize::new(0),
548551
#[cfg(feature = "malloc_counted_size")]
549552
malloc_bytes: AtomicUsize::new(0),
553+
#[cfg(feature = "count_live_bytes_in_gc")]
554+
live_bytes_in_last_gc: AtomicUsize::new(0),
550555
#[cfg(feature = "analysis")]
551556
analysis_manager,
552557
}

src/plan/markcompact/gc_work.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ impl<VM: VMBinding> GCWork<VM> for UpdateReferences<VM> {
4343
#[cfg(feature = "extreme_assertions")]
4444
mmtk.edge_logger.reset();
4545

46+
// We do two passes of transitive closures. We clear the live bytes from the first pass.
47+
#[cfg(feature = "count_live_bytes_in_gc")]
48+
mmtk.scheduler
49+
.worker_group
50+
.get_and_clear_worker_live_bytes();
51+
4652
// TODO investigate why the following will create duplicate edges
4753
// scheduler.work_buckets[WorkBucketStage::RefForwarding]
4854
// .add(ScanStackRoots::<ForwardingProcessEdges<VM>>::new());

src/plan/tracing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl ObjectQueue for VectorQueue<ObjectReference> {
7474
/// A transitive closure visitor to collect all the edges of an object.
7575
pub struct ObjectsClosure<'a, E: ProcessEdgesWork> {
7676
buffer: VectorQueue<EdgeOf<E>>,
77-
worker: &'a mut GCWorker<E::VM>,
77+
pub(crate) worker: &'a mut GCWorker<E::VM>,
7878
}
7979

8080
impl<'a, E: ProcessEdgesWork> ObjectsClosure<'a, E> {

src/scheduler/gc_work.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ impl<C: GCWorkContext + 'static> GCWork<C::VM> for Release<C> {
129129
let result = w.designated_work.push(Box::new(ReleaseCollector));
130130
debug_assert!(result.is_ok());
131131
}
132+
133+
#[cfg(feature = "count_live_bytes_in_gc")]
134+
{
135+
let live_bytes = mmtk
136+
.scheduler
137+
.worker_group
138+
.get_and_clear_worker_live_bytes();
139+
self.plan
140+
.base()
141+
.live_bytes_in_last_gc
142+
.store(live_bytes, std::sync::atomic::Ordering::SeqCst);
143+
}
132144
}
133145
}
134146

@@ -227,6 +239,28 @@ impl<VM: VMBinding> GCWork<VM> for EndOfGC {
227239
self.elapsed.as_millis()
228240
);
229241

242+
#[cfg(feature = "count_live_bytes_in_gc")]
243+
{
244+
let live_bytes = mmtk
245+
.plan
246+
.base()
247+
.live_bytes_in_last_gc
248+
.load(std::sync::atomic::Ordering::SeqCst);
249+
let used_bytes =
250+
mmtk.plan.get_used_pages() << crate::util::constants::LOG_BYTES_IN_PAGE;
251+
debug_assert!(
252+
live_bytes <= used_bytes,
253+
"Live bytes of all live objects ({} bytes) is larger than used pages ({} bytes), something is wrong.",
254+
live_bytes, used_bytes
255+
);
256+
info!(
257+
"Live objects = {} bytes ({:04.1}% of {} used pages)",
258+
live_bytes,
259+
live_bytes as f64 * 100.0 / used_bytes as f64,
260+
mmtk.plan.get_used_pages()
261+
);
262+
}
263+
230264
// We assume this is the only running work packet that accesses plan at the point of execution
231265
#[allow(clippy::cast_ref_to_mut)]
232266
let plan_mut: &mut dyn Plan<VM = VM> = unsafe { &mut *(&*mmtk.plan as *const _ as *mut _) };
@@ -815,6 +849,13 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized {
815849
{
816850
let mut closure = ObjectsClosure::<Self::E>::new(worker);
817851
for object in objects_to_scan.iter().copied() {
852+
// For any object we need to scan, we count its liv bytes
853+
#[cfg(feature = "count_live_bytes_in_gc")]
854+
closure
855+
.worker
856+
.shared
857+
.increase_live_bytes(VM::VMObjectModel::get_current_size(object));
858+
818859
if <VM as VMBinding>::VMScanning::support_edge_enqueuing(tls, object) {
819860
trace!("Scan object (edge) {}", object);
820861
// If an object supports edge-enqueuing, we enqueue its edges.

src/scheduler/worker.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use atomic::Atomic;
99
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
1010
use crossbeam::deque::{self, Stealer};
1111
use crossbeam::queue::ArrayQueue;
12+
#[cfg(feature = "count_live_bytes_in_gc")]
13+
use std::sync::atomic::AtomicUsize;
1214
use std::sync::atomic::Ordering;
1315
use std::sync::{Arc, Condvar, Mutex};
1416

@@ -30,6 +32,11 @@ pub fn current_worker_ordinal() -> Option<ThreadId> {
3032
pub struct GCWorkerShared<VM: VMBinding> {
3133
/// Worker-local statistics data.
3234
stat: AtomicRefCell<WorkerLocalStat<VM>>,
35+
/// Accumulated bytes for live objects in this GC. When each worker scans
36+
/// objects, we increase the live bytes. We get this value from each worker
37+
/// at the end of a GC, and reset this counter.
38+
#[cfg(feature = "count_live_bytes_in_gc")]
39+
live_bytes: AtomicUsize,
3340
/// A queue of GCWork that can only be processed by the owned thread.
3441
///
3542
/// Note: Currently, designated work cannot be added from the GC controller thread, or
@@ -44,10 +51,22 @@ impl<VM: VMBinding> GCWorkerShared<VM> {
4451
pub fn new(stealer: Option<Stealer<Box<dyn GCWork<VM>>>>) -> Self {
4552
Self {
4653
stat: Default::default(),
54+
#[cfg(feature = "count_live_bytes_in_gc")]
55+
live_bytes: AtomicUsize::new(0),
4756
designated_work: ArrayQueue::new(16),
4857
stealer,
4958
}
5059
}
60+
61+
#[cfg(feature = "count_live_bytes_in_gc")]
62+
pub(crate) fn increase_live_bytes(&self, bytes: usize) {
63+
self.live_bytes.fetch_add(bytes, Ordering::Relaxed);
64+
}
65+
66+
#[cfg(feature = "count_live_bytes_in_gc")]
67+
pub(crate) fn get_and_clear_live_bytes(&self) -> usize {
68+
self.live_bytes.swap(0, Ordering::SeqCst)
69+
}
5170
}
5271

5372
/// Used to synchronize mutually exclusive operations between workers and controller,
@@ -427,4 +446,12 @@ impl<VM: VMBinding> WorkerGroup<VM> {
427446
.iter()
428447
.any(|w| !w.designated_work.is_empty())
429448
}
449+
450+
#[cfg(feature = "count_live_bytes_in_gc")]
451+
pub fn get_and_clear_worker_live_bytes(&self) -> usize {
452+
self.workers_shared
453+
.iter()
454+
.map(|w| w.get_and_clear_live_bytes())
455+
.sum()
456+
}
430457
}

0 commit comments

Comments
 (0)