Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
fb12863
WIP
tianleq Jul 25, 2025
9ac22bd
minor
tianleq Jul 25, 2025
04fee38
Merge branch 'master' of github.com:tianleq/mmtk-core into concurrent…
tianleq Jul 28, 2025
100c049
minor
tianleq Jul 28, 2025
2bbb200
Add ref/finalizer packets for final pause. Use log instead of println.
qinsoon Jul 31, 2025
90f4518
schedule_concurrent_packets before resuming mutators
qinsoon Jul 31, 2025
521adea
eBPF tracing tools for concurrent Immix
wks Jul 31, 2025
4f14b2f
Wake up workers immediately for concurrent work
wks Jul 31, 2025
865e24c
Fix clippy warnings and formatting
wks Jul 31, 2025
7b13272
Move concurrent_marking_active to the plan. Add a flag allocate_as_live
qinsoon Aug 1, 2025
1db4743
Rename load_reference to load_weak_reference
wks Aug 1, 2025
5c38704
Replace `swap` with `replace` and `take`
wks Aug 1, 2025
a95e94a
Remove schedule_concurrent_collection
qinsoon Aug 5, 2025
eacc959
Rename gc_pause_start. Merge gc_pause_end with end_of_gc
qinsoon Aug 5, 2025
028bd0e
Merge branch 'master' into concurrent-immix
qinsoon Aug 5, 2025
fe29529
Disallow new weak reference before ref enqueue
qinsoon Aug 6, 2025
26c2a54
Introduce ConcurrentPlan. Make ConcurrentTraceObjects trace objects in
qinsoon Aug 13, 2025
80024dd
Merge branch 'master' into concurrent-immix
qinsoon Aug 13, 2025
7487c1b
Fix rayon-core version for MSRV
qinsoon Aug 13, 2025
6bb03c0
More assertions and minor fix.
qinsoon Aug 14, 2025
ad41d7e
Wrong assertions
qinsoon Aug 14, 2025
a938110
Fix style check
qinsoon Aug 25, 2025
321c6e2
Refactor log bits
qinsoon Aug 8, 2025
c9315b9
Merge branch 'master' into concurrent-immix
qinsoon Aug 25, 2025
12112d2
Fix style check
qinsoon Aug 25, 2025
77330fe
Use Concurrent bucket for concurrent work
qinsoon Aug 22, 2025
74572eb
Merge branch 'master' into concurrent-immix
qinsoon Aug 26, 2025
bdcf723
Re-enable weak ref buckets. Calculate allocated pages using used pages.
qinsoon Aug 26, 2025
1368ac5
Remove NUM_CONCURRENT_TRACING_PACKETS. Fix issues about enabling
qinsoon Aug 26, 2025
f906db3
Dont use TRACE_KIND_FAST in ConcurrentTraceObjects
qinsoon Aug 26, 2025
4e5c772
Fix docs
qinsoon Aug 26, 2025
47708b7
Allow defrag for STW full heap collection
qinsoon Aug 26, 2025
35cf25a
Put generated concurrent work to the concurrent bucket. Don't need pr…
qinsoon Aug 26, 2025
2653716
Properly call post_scan_object in ConcurrentTraceObject
qinsoon Aug 26, 2025
9cdcb7a
Remove the use of Pause in StopMutators
qinsoon Aug 26, 2025
d43c9e5
Use normal StopMutators for initial marking
qinsoon Aug 27, 2025
e3163d8
Remove Collection::set_concurrent_marking_state. Introduce active for
qinsoon Aug 27, 2025
4966131
Remove the GCCause type
qinsoon Aug 27, 2025
c70c03f
Cleanup
qinsoon Aug 27, 2025
c79fe8e
Fix style check
qinsoon Aug 27, 2025
a23bfbb
Fix style check
qinsoon Aug 27, 2025
3e076a9
Fix style check
qinsoon Aug 28, 2025
553227a
Remove object_reference_clone_pre
wks Aug 28, 2025
8ad9ed8
Remove the gc_cause field.
wks Aug 28, 2025
acea83a
Postpone full GC after FinalMark
wks Aug 28, 2025
c442dcd
Merge branch 'master' into review/concurrent-immix
wks Sep 9, 2025
70ef1db
Change active to SATBBarrier-specific
wks Sep 9, 2025
6dfde0a
Label SlotIterator for refactoring
wks Sep 10, 2025
a33ba5a
Fix some comments
wks Sep 10, 2025
d7ae8e2
No longer expose concrete barriers to the VM binding.
wks Sep 10, 2025
8acb5af
Comments and formatting
wks Sep 10, 2025
2731449
Remove dead code.
wks Sep 10, 2025
ab75860
Extract method `eager_mark_lines`
wks Sep 10, 2025
d96d2e5
Minor change
wks Sep 10, 2025
4e1db41
Record pause kind in eBPF trace
wks Sep 10, 2025
c9d4971
Move FIXME out of doc comment
wks Sep 10, 2025
ad0d88e
Remove duplicate clear/set_side_log_bits from policies
qinsoon Sep 11, 2025
be33efe
Set and clear side unlog bits in parallel.
wks Sep 11, 2025
34be825
Merge remote-tracking branch 'tianleq/concurrent-immix' into review/c…
wks Sep 11, 2025
2ee86af
Remove visualization of unused work packet
wks Sep 11, 2025
0c3d9f6
Remove unnecessary unlog bit clearing
wks Sep 11, 2025
f76e954
Move `eager_mark_lines` to `Line`.
wks Sep 12, 2025
b7b79e4
Just use bset_metadata
wks Sep 12, 2025
26f74c1
Code style and comments
wks Sep 12, 2025
5b5552b
Extract bulk_set_line_mark_states
wks Sep 12, 2025
815cc17
Make the `-e` option of `capture.py` work for ConcurrentImmix
wks Sep 12, 2025
446ec36
Merge branch 'master' into review/concurrent-immix
wks Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct GlobalState {
pub(crate) malloc_bytes: AtomicUsize,
/// This stores the live bytes and the used bytes (by pages) for each space in last GC. This counter is only updated in the GC release phase.
pub(crate) live_bytes_in_last_gc: AtomicRefCell<HashMap<&'static str, LiveBytesStats>>,
/// The number of used pages at the end of the last GC. This can be used to estimate how many pages we have allocated since last GC.
pub(crate) used_pages_after_last_gc: AtomicUsize,
}

impl GlobalState {
Expand Down Expand Up @@ -184,6 +186,15 @@ impl GlobalState {
pub(crate) fn decrease_malloc_bytes_by(&self, size: usize) {
self.malloc_bytes.fetch_sub(size, Ordering::SeqCst);
}

pub(crate) fn set_used_pages_after_last_gc(&self, pages: usize) {
self.used_pages_after_last_gc
.store(pages, Ordering::Relaxed);
}

pub(crate) fn get_used_pages_after_last_gc(&self) -> usize {
self.used_pages_after_last_gc.load(Ordering::Relaxed)
}
}

impl Default for GlobalState {
Expand All @@ -206,6 +217,7 @@ impl Default for GlobalState {
#[cfg(feature = "malloc_counted_size")]
malloc_bytes: AtomicUsize::new(0),
live_bytes_in_last_gc: AtomicRefCell::new(HashMap::new()),
used_pages_after_last_gc: AtomicUsize::new(0),
}
}
}
Expand Down
111 changes: 110 additions & 1 deletion src/plan/barriers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ use downcast_rs::Downcast;
pub enum BarrierSelector {
/// No barrier is used.
NoBarrier,
/// Object remembering barrier is used.
/// Object remembering post-write barrier is used.
ObjectBarrier,
/// Object remembering pre-write barrier with weak reference loading barrier.
// TODO: We might be able to generalize this to object remembering pre-write barrier.
SATBBarrier,
}

impl BarrierSelector {
Expand All @@ -43,8 +46,22 @@ impl BarrierSelector {
/// As a performance optimization, the binding may also choose to port the fast-path to the VM side,
/// and call the slow-path (`object_reference_write_slow`) only if necessary.
pub trait Barrier<VM: VMBinding>: 'static + Send + Downcast {
/// Flush thread-local states like buffers or remembered sets.
fn flush(&mut self) {}

/// Weak reference loading barrier. A mutator should call this when loading from a weak
/// reference field, for example, when executing `java.lang.ref.Reference.get()` in JVM, or
/// loading from a global weak table in CRuby.
///
/// Note: Merely loading from a field holding weak reference into a local variable will create a
/// strong reference from the stack to the referent, changing its reachablilty from weakly
/// reachable to strongly reachable. Concurrent garbage collectors may need to handle such
/// events specially. See [SATBBarrier::load_weak_reference] for a concrete example.
///
/// Arguments:
/// * `referent`: The referent object which the weak reference is pointing to.
fn load_weak_reference(&mut self, _referent: ObjectReference) {}

/// Subsuming barrier for object reference write
fn object_reference_write(
&mut self,
Expand Down Expand Up @@ -159,6 +176,9 @@ pub trait BarrierSemantics: 'static + Send {

/// Object will probably be modified
fn object_probable_write_slow(&mut self, _obj: ObjectReference) {}

/// Loading from a weak reference field
fn load_weak_reference(&mut self, _o: ObjectReference) {}
}

/// Generic object barrier with a type argument defining it's slow-path behaviour.
Expand All @@ -167,6 +187,7 @@ pub struct ObjectBarrier<S: BarrierSemantics> {
}

impl<S: BarrierSemantics> ObjectBarrier<S> {
/// Create a new ObjectBarrier with the given semantics.
pub fn new(semantics: S) -> Self {
Self { semantics }
}
Expand Down Expand Up @@ -250,3 +271,91 @@ impl<S: BarrierSemantics> Barrier<S::VM> for ObjectBarrier<S> {
}
}
}

/// A SATB (Snapshot-At-The-Beginning) barrier implementation.
/// This barrier is basically a pre-write object barrier with a weak reference loading barrier.
pub struct SATBBarrier<S: BarrierSemantics> {
weak_ref_barrier_enabled: bool,
semantics: S,
}

impl<S: BarrierSemantics> SATBBarrier<S> {
/// Create a new SATBBarrier with the given semantics.
pub fn new(semantics: S) -> Self {
Self {
weak_ref_barrier_enabled: false,
semantics,
}
}

pub(crate) fn set_weak_ref_barrier_enabled(&mut self, value: bool) {
self.weak_ref_barrier_enabled = value;
}

fn object_is_unlogged(&self, object: ObjectReference) -> bool {
S::UNLOG_BIT_SPEC.load_atomic::<S::VM, u8>(object, None, Ordering::SeqCst) != 0
}
}

impl<S: BarrierSemantics> Barrier<S::VM> for SATBBarrier<S> {
fn flush(&mut self) {
self.semantics.flush();
}

fn load_weak_reference(&mut self, o: ObjectReference) {
if self.weak_ref_barrier_enabled {
self.semantics.load_weak_reference(o)
}
}

fn object_probable_write(&mut self, obj: ObjectReference) {
self.semantics.object_probable_write_slow(obj);
}

fn object_reference_write_pre(
&mut self,
src: ObjectReference,
slot: <S::VM as VMBinding>::VMSlot,
target: Option<ObjectReference>,
) {
if self.object_is_unlogged(src) {
self.semantics
.object_reference_write_slow(src, slot, target);
}
}

fn object_reference_write_post(
&mut self,
_src: ObjectReference,
_slot: <S::VM as VMBinding>::VMSlot,
_target: Option<ObjectReference>,
) {
unimplemented!()
}

fn object_reference_write_slow(
&mut self,
src: ObjectReference,
slot: <S::VM as VMBinding>::VMSlot,
target: Option<ObjectReference>,
) {
self.semantics
.object_reference_write_slow(src, slot, target);
}

fn memory_region_copy_pre(
&mut self,
src: <S::VM as VMBinding>::VMMemorySlice,
dst: <S::VM as VMBinding>::VMMemorySlice,
) {
self.semantics.memory_region_copy_slow(src, dst);
}

fn memory_region_copy_post(
&mut self,
_src: <S::VM as VMBinding>::VMMemorySlice,
_dst: <S::VM as VMBinding>::VMMemorySlice,
) {
unimplemented!()
}
}
163 changes: 163 additions & 0 deletions src/plan/concurrent/barrier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use std::sync::atomic::Ordering;

use super::{concurrent_marking_work::ProcessModBufSATB, Pause};
use crate::plan::global::PlanTraceObject;
use crate::policy::gc_work::TraceKind;
use crate::util::VMMutatorThread;
use crate::{
plan::{barriers::BarrierSemantics, concurrent::global::ConcurrentPlan, VectorQueue},
scheduler::WorkBucketStage,
util::ObjectReference,
vm::{
slot::{MemorySlice, Slot},
VMBinding,
},
MMTK,
};

pub struct SATBBarrierSemantics<
VM: VMBinding,
P: ConcurrentPlan<VM = VM> + PlanTraceObject<VM>,
const KIND: TraceKind,
> {
mmtk: &'static MMTK<VM>,
tls: VMMutatorThread,
satb: VectorQueue<ObjectReference>,
refs: VectorQueue<ObjectReference>,
plan: &'static P,
}

impl<VM: VMBinding, P: ConcurrentPlan<VM = VM> + PlanTraceObject<VM>, const KIND: TraceKind>
SATBBarrierSemantics<VM, P, KIND>
{
pub fn new(mmtk: &'static MMTK<VM>, tls: VMMutatorThread) -> Self {
Self {
mmtk,
tls,
satb: VectorQueue::default(),
refs: VectorQueue::default(),
plan: mmtk.get_plan().downcast_ref::<P>().unwrap(),
}
}

fn slow(&mut self, _src: Option<ObjectReference>, _slot: VM::VMSlot, old: ObjectReference) {
self.satb.push(old);
if self.satb.is_full() {
self.flush_satb();
}
}

fn enqueue_node(
&mut self,
src: Option<ObjectReference>,
slot: VM::VMSlot,
_new: Option<ObjectReference>,
) -> bool {
if let Some(old) = slot.load() {
self.slow(src, slot, old);
}
true
}

/// Attempt to atomically log an object.
/// Returns true if the object is not logged previously.
fn log_object(&self, object: ObjectReference) -> bool {
Self::UNLOG_BIT_SPEC.store_atomic::<VM, u8>(object, 0, None, Ordering::SeqCst);
true
}

fn flush_satb(&mut self) {
if !self.satb.is_empty() {
if self.should_create_satb_packets() {
let satb = self.satb.take();
let bucket = if self.plan.concurrent_work_in_progress() {
WorkBucketStage::Concurrent
} else {
debug_assert_ne!(self.plan.current_pause(), Some(Pause::InitialMark));
WorkBucketStage::Closure
};
self.mmtk.scheduler.work_buckets[bucket]
.add(ProcessModBufSATB::<VM, P, KIND>::new(satb));
} else {
let _ = self.satb.take();
};
}
}

#[cold]
fn flush_weak_refs(&mut self) {
if !self.refs.is_empty() {
let nodes = self.refs.take();
let bucket = if self.plan.concurrent_work_in_progress() {
WorkBucketStage::Concurrent
} else {
debug_assert_ne!(self.plan.current_pause(), Some(Pause::InitialMark));
WorkBucketStage::Closure
};
self.mmtk.scheduler.work_buckets[bucket]
.add(ProcessModBufSATB::<VM, P, KIND>::new(nodes));
}
}

fn should_create_satb_packets(&self) -> bool {
self.plan.concurrent_work_in_progress()
|| self.plan.current_pause() == Some(Pause::FinalMark)
}
}

impl<VM: VMBinding, P: ConcurrentPlan<VM = VM> + PlanTraceObject<VM>, const KIND: TraceKind>
BarrierSemantics for SATBBarrierSemantics<VM, P, KIND>
{
type VM = VM;

#[cold]
fn flush(&mut self) {
self.flush_satb();
self.flush_weak_refs();
}

fn object_reference_write_slow(
&mut self,
src: ObjectReference,
_slot: <Self::VM as VMBinding>::VMSlot,
_target: Option<ObjectReference>,
) {
self.object_probable_write_slow(src);
self.log_object(src);
}

fn memory_region_copy_slow(
&mut self,
_src: <Self::VM as VMBinding>::VMMemorySlice,
dst: <Self::VM as VMBinding>::VMMemorySlice,
) {
for s in dst.iter_slots() {
self.enqueue_node(None, s, None);
}
}

/// Enqueue the referent during concurrent marking.
///
/// Note: During concurrent marking, a collector based on snapshot-at-the-beginning (SATB) will
/// not reach objects that were weakly reachable at the time of `InitialMark`. But if a mutator
/// loads from a weak reference field during concurrent marking, it will make the referent
/// strongly reachable, yet the referent is still not part of the SATB. We must conservatively
/// enqueue the referent even though its reachability has not yet been established, otherwise it
/// (and its children) may be treated as garbage if it happened to be weakly reachable at the
/// time of `InitialMark`.
fn load_weak_reference(&mut self, o: ObjectReference) {
if !self.plan.concurrent_work_in_progress() {
return;
}
self.refs.push(o);
if self.refs.is_full() {
self.flush_weak_refs();
}
}

fn object_probable_write_slow(&mut self, obj: ObjectReference) {
crate::plan::tracing::SlotIterator::<VM>::iterate_fields(obj, self.tls.0, |s| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to enqueue obj not it's fields. If I remember this correctly, at the time this code is called, all fields are uninitialized.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably I'm wrong, but I guess object_probable_write is not necessary at all. This obj should already be in the root set of the InitialMark pause, and will be marked eventually.

Copy link
Collaborator

@wks wks Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right. The semantics of the MMTk-side API Barrier::object_probable_write says

    /// A pre-barrier indicating that some fields of the object will probably be modified soon.
    /// Specifically, the caller should ensure that:
    ///     * The barrier must called before any field modification.
    ///     * Some fields (unknown at the time of calling this barrier) might be modified soon, without a write barrier.
    ///     * There are no safepoints between the barrier call and the field writes.

If the fields are assigned during concurrent marking, the (new) values will either come from the snapshot at the beginning, or be a new object allocated during concurrent marking. In either case, they will be kept alive. (Update: But the old children of the fields that are overwritten by the assignments will not be kept alive.)

To put it another way, the SATB barrier is a deletion barrier. As long as no objects are disconnected from other objects, there is no need to apply barrier. In the case of OpenJDK, it is assigning objects to fields that are not initialized, so no objects are disconnected.

I'll remove object_probable_write for SATBBarrier.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. After a second thought, I think if MMTk implements the semantics described in the doc comment of Barrier::object_probable_write, it should still remember its old field values. If another VM calls object_probable_write on an object that already has children, it will still need to remember the old children because they may probably be overwritten. So Barrier::object_probable_write still has to be implemented as it currently is.

The OpenJDK binding can do VM-specific optimization by eliding the invocation of mmtk_object_probable_write in MMTkSATBBarrierSetRuntime::object_probable_write, making use of the knowledge of the SATBBarrier. Currently only the OpenJDK binding uses Barrier::object_probable_write. So it will not be disruptive to other bindings if we change the semantics of Barrier::object_probable_write to make it more specific to OpenJDK's use case and do more optimizations. But we need to change the semantics first.

Copy link
Collaborator

@k-sareen k-sareen Sep 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove it or change semantics arbitrarily. I (kind of) depend on it in ART as well. ART has a barrier WriteBarrier::ForEveryFieldWrite whose semantics are essentially object_probable_write. Currently since I only have a generational post-write object remembering barrier implemented, I just call the normal post-write function, but this will be fixed in the future when I add concurrent GC support.

self.enqueue_node(Some(obj), s, None);
});
}
}
Loading
Loading