Skip to content

Commit c38365b

Browse files
authored
Update OOM semantics and expectations (#535)
Note: API breaking change! Adds `AllocationError` to `Collection::out_of_memory` to differentiate between the two (main) types of OOM: * Critical OOM: This is the case where the OS is unable to mmap or acquire more memory. MMTk expects the VM to abort immediately if such an error is thrown. * Heap OOM: This is the case where the specified heap size is insufficient to execute the application. MMTk expects the binding to notify the VM about this OOM. MMTk makes no assumptions about whether the VM will continue executing or abort immediately. MMTk now returns null in case of a Heap OOM error.
1 parent efc6b99 commit c38365b

File tree

8 files changed

+141
-56
lines changed

8 files changed

+141
-56
lines changed

src/util/alloc/allocator.rs

Lines changed: 102 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ use crate::vm::VMBinding;
99
use crate::vm::{ActivePlan, Collection};
1010
use downcast_rs::Downcast;
1111

12+
#[repr(C)]
13+
#[derive(Debug)]
14+
/// A list of errors that MMTk can encounter during allocation.
15+
pub enum AllocationError {
16+
/// The specified heap size is too small for the given program to continue.
17+
HeapOutOfMemory,
18+
/// The OS is unable to mmap or acquire more memory. Critical error. MMTk expects the VM to
19+
/// abort if such an error is thrown.
20+
MmapOutOfMemory,
21+
}
22+
1223
#[inline(always)]
1324
pub fn align_allocation_no_fill<VM: VMBinding>(
1425
region: Address,
@@ -102,32 +113,73 @@ pub fn get_maximum_aligned_size<VM: VMBinding>(
102113
}
103114
}
104115

116+
/// A trait which implements allocation routines. Every allocator needs to implements this trait.
105117
pub trait Allocator<VM: VMBinding>: Downcast {
118+
/// Return the [`VMThread`] associated with this allocator instance.
106119
fn get_tls(&self) -> VMThread;
107120

121+
/// Return the [`Space`] instance associated with this allocator instance.
108122
fn get_space(&self) -> &'static dyn Space<VM>;
123+
124+
/// Return the [`Plan`] instance that this allocator instance is associated with.
109125
fn get_plan(&self) -> &'static dyn Plan<VM = VM>;
110126

111-
/// Does this allocator do thread local allocation? If an allocator does not do thread local allocation,
112-
/// each allocation will go to slowpath and will have a check for GC polls.
127+
/// Return if this allocator can do thread local allocation. If an allocator does not do thread
128+
/// local allocation, each allocation will go to slowpath and will have a check for GC polls.
113129
fn does_thread_local_allocation(&self) -> bool;
114130

115-
/// At which granularity the allocator acquires memory from the global space and use them as thread local buffer.
116-
/// For example, bump pointer allocator acquire memory at 32KB blocks. Depending on the actual size for the current object,
117-
/// they always acquire memory of N*32KB (N>=1). Thus bump pointer allocator returns 32KB for this method.
118-
/// Only allocators that do thread local allocation need to implement this method.
131+
/// Return at which granularity the allocator acquires memory from the global space and use
132+
/// them as thread local buffer. For example, the [`BumpAllocator`] acquires memory at 32KB
133+
/// blocks. Depending on the actual size for the current object, they always acquire memory of
134+
/// N*32KB (N>=1). Thus the [`BumpAllocator`] returns 32KB for this method. Only allocators
135+
/// that do thread local allocation need to implement this method.
119136
fn get_thread_local_buffer_granularity(&self) -> usize {
120137
assert!(self.does_thread_local_allocation(), "An allocator that does not thread local allocation does not have a buffer granularity.");
121138
unimplemented!()
122139
}
123140

141+
/// An allocation attempt. The implementation of this function depends on the allocator used.
142+
/// If an allocator supports thread local allocations, then the allocation will be serviced
143+
/// from its TLAB, otherwise it will default to using the slowpath, i.e. [`alloc_slow`].
144+
///
145+
/// Note that in the case where the VM is out of memory, we invoke
146+
/// [`Collection::out_of_memory`] to inform the binding and then return a null pointer back to
147+
/// it. We have no assumptions on whether the VM will continue executing or abort immediately.
148+
///
149+
/// Arguments:
150+
/// * `size`: the allocation size in bytes.
151+
/// * `align`: the required alignment in bytes.
152+
/// * `offset` the required offset in bytes.
124153
fn alloc(&mut self, size: usize, align: usize, offset: isize) -> Address;
125154

155+
/// Slowpath allocation attempt. This function is explicitly not inlined for performance
156+
/// considerations.
157+
///
158+
/// Arguments:
159+
/// * `size`: the allocation size in bytes.
160+
/// * `align`: the required alignment in bytes.
161+
/// * `offset` the required offset in bytes.
126162
#[inline(never)]
127163
fn alloc_slow(&mut self, size: usize, align: usize, offset: isize) -> Address {
128164
self.alloc_slow_inline(size, align, offset)
129165
}
130166

167+
/// Slowpath allocation attempt. This function executes the actual slowpath allocation. A
168+
/// slowpath allocation in MMTk attempts to allocate the object using the per-allocator
169+
/// definition of [`alloc_slow_once`]. This function also accounts for increasing the
170+
/// allocation bytes in order to support stress testing. In case precise stress testing is
171+
/// being used, the [`alloc_slow_once_precise_stress`] function is used instead.
172+
///
173+
/// Note that in the case where the VM is out of memory, we invoke
174+
/// [`Collection::out_of_memory`] with a [`AllocationError::HeapOutOfMemory`] error to inform
175+
/// the binding and then return a null pointer back to it. We have no assumptions on whether
176+
/// the VM will continue executing or abort immediately on a
177+
/// [`AllocationError::HeapOutOfMemory`] error.
178+
///
179+
/// Arguments:
180+
/// * `size`: the allocation size in bytes.
181+
/// * `align`: the required alignment in bytes.
182+
/// * `offset` the required offset in bytes.
131183
#[inline(always)]
132184
fn alloc_slow_inline(&mut self, size: usize, align: usize, offset: isize) -> Address {
133185
let tls = self.get_tls();
@@ -207,10 +259,10 @@ pub trait Allocator<VM: VMBinding>: Downcast {
207259
}
208260

209261
// It is possible to have cases where a thread is blocked for another GC (non emergency)
210-
// immediately after being blocked for a GC (emergency) (e.g. in stress test), that is saying the thread does not
211-
// leave this loop between the two GCs. The local var 'emergency_collection' was set to true
212-
// after the first GC. But when we execute this check below, we just finished the second GC,
213-
// which is not emergency. In such case, we will give a false OOM.
262+
// immediately after being blocked for a GC (emergency) (e.g. in stress test), that is saying
263+
// the thread does not leave this loop between the two GCs. The local var 'emergency_collection'
264+
// was set to true after the first GC. But when we execute this check below, we just finished
265+
// the second GC, which is not emergency. In such case, we will give a false OOM.
214266
// We cannot just rely on the local var. Instead, we get the emergency collection value again,
215267
// and check both.
216268
if emergency_collection && self.get_plan().is_emergency_collection() {
@@ -220,8 +272,11 @@ pub trait Allocator<VM: VMBinding>: Downcast {
220272
let fail_with_oom = !plan.allocation_success.swap(true, Ordering::SeqCst);
221273
trace!("fail with oom={}", fail_with_oom);
222274
if fail_with_oom {
223-
VM::VMCollection::out_of_memory(tls);
224-
trace!("Not reached");
275+
// Note that we throw a `HeapOutOfMemory` error here and return a null ptr back to the VM
276+
trace!("Throw HeapOutOfMemory!");
277+
VM::VMCollection::out_of_memory(tls, AllocationError::HeapOutOfMemory);
278+
plan.allocation_success.swap(false, Ordering::SeqCst);
279+
return result;
225280
}
226281
}
227282

@@ -235,53 +290,63 @@ pub trait Allocator<VM: VMBinding>: Downcast {
235290
// VMActivePlan::mutator(tls).get_allocator_from_space(space)
236291
//};
237292

238-
/*
239-
* Record whether last collection was an Emergency collection.
240-
* If so, we make one more attempt to allocate before we signal
241-
* an OOM.
242-
*/
293+
// Record whether last collection was an Emergency collection. If so, we make one more
294+
// attempt to allocate before we signal an OOM.
243295
emergency_collection = self.get_plan().is_emergency_collection();
244296
trace!("Got emergency collection as {}", emergency_collection);
245297
previous_result_zero = true;
246298
}
247299
}
248300

249-
/// Single slow path allocation attempt. This is called by allocSlow.
301+
/// Single slow path allocation attempt. This is called by [`alloc_slow_inline`]. The
302+
/// implementation of this function depends on the allocator used. Generally, if an allocator
303+
/// supports thread local allocations, it will try to allocate more TLAB space here. If it
304+
/// doesn't, then (generally) the allocator simply allocates enough space for the current
305+
/// object.
306+
///
307+
/// Arguments:
308+
/// * `size`: the allocation size in bytes.
309+
/// * `align`: the required alignment in bytes.
310+
/// * `offset` the required offset in bytes.
250311
fn alloc_slow_once(&mut self, size: usize, align: usize, offset: isize) -> Address;
251312

252-
/// Single slowpath allocation attempt for stress test. When the stress factor is set (e.g. to N),
253-
/// we would expect for every N bytes allocated, we will trigger a stress GC.
254-
/// However, for allocators that do thread local allocation, they may allocate from their thread local buffer
255-
/// which does not have a GC poll check, and they may even allocate with the JIT generated allocation
256-
/// fastpath which is unaware of stress test GC. For both cases, we are not able to guarantee
257-
/// a stress GC is triggered every N bytes. To solve this, when the stress factor is set, we
258-
/// will call this method instead of the normal alloc_slow_once(). We expect the implementation of this slow allocation
259-
/// will trick the fastpath so every allocation will fail in the fastpath, jump to the slow path and eventually
260-
/// call this method again for the actual allocation.
313+
/// Single slowpath allocation attempt for stress test. When the stress factor is set (e.g. to
314+
/// N), we would expect for every N bytes allocated, we will trigger a stress GC. However, for
315+
/// allocators that do thread local allocation, they may allocate from their thread local
316+
/// buffer which does not have a GC poll check, and they may even allocate with the JIT
317+
/// generated allocation fastpath which is unaware of stress test GC. For both cases, we are
318+
/// not able to guarantee a stress GC is triggered every N bytes. To solve this, when the
319+
/// stress factor is set, we will call this method instead of the normal alloc_slow_once(). We
320+
/// expect the implementation of this slow allocation will trick the fastpath so every
321+
/// allocation will fail in the fastpath, jump to the slow path and eventually call this method
322+
/// again for the actual allocation.
261323
///
262-
/// The actual implementation about how to trick the fastpath may vary. For example, our bump pointer allocator will
263-
/// set the thread local buffer limit to the buffer size instead of the buffer end address. In this case, every fastpath
264-
/// check (cursor + size < limit) will fail, and jump to this slowpath. In the slowpath, we still allocate from the thread
265-
/// local buffer, and recompute the limit (remaining buffer size).
324+
/// The actual implementation about how to trick the fastpath may vary. For example, our bump
325+
/// pointer allocator will set the thread local buffer limit to the buffer size instead of the
326+
/// buffer end address. In this case, every fastpath check (cursor + size < limit) will fail,
327+
/// and jump to this slowpath. In the slowpath, we still allocate from the thread local buffer,
328+
/// and recompute the limit (remaining buffer size).
266329
///
267-
/// If an allocator does not do thread local allocation (which returns false for does_thread_local_allocation()), it does
268-
/// not need to override this method. The default implementation will simply call allow_slow_once() and it will work fine
269-
/// for allocators that do not have thread local allocation.
330+
/// If an allocator does not do thread local allocation (which returns false for
331+
/// does_thread_local_allocation()), it does not need to override this method. The default
332+
/// implementation will simply call allow_slow_once() and it will work fine for allocators that
333+
/// do not have thread local allocation.
270334
///
271335
/// Arguments:
272336
/// * `size`: the allocation size in bytes.
273337
/// * `align`: the required alignment in bytes.
274338
/// * `offset` the required offset in bytes.
275-
/// * `need_poll`: if this is true, the implementation must poll for a GC, rather than attempting to allocate from the local buffer.
339+
/// * `need_poll`: if this is true, the implementation must poll for a GC, rather than
340+
/// attempting to allocate from the local buffer.
276341
fn alloc_slow_once_precise_stress(
277342
&mut self,
278343
size: usize,
279344
align: usize,
280345
offset: isize,
281346
need_poll: bool,
282347
) -> Address {
283-
// If an allocator does thread local allocation but does not override this method to provide a correct implementation,
284-
// we will log a warning.
348+
// If an allocator does thread local allocation but does not override this method to
349+
// provide a correct implementation, we will log a warning.
285350
if self.does_thread_local_allocation() && need_poll {
286351
warn!("{} does not support stress GC (An allocator that does thread local allocation needs to implement allow_slow_once_stress_test()).", std::any::type_name::<Self>());
287352
}

src/util/alloc/large_object_allocator.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ impl<VM: VMBinding> Allocator<VM> for LargeObjectAllocator<VM> {
1717
fn get_tls(&self) -> VMThread {
1818
self.tls
1919
}
20+
2021
fn get_plan(&self) -> &'static dyn Plan<VM = VM> {
2122
self.plan
2223
}
@@ -32,11 +33,12 @@ impl<VM: VMBinding> Allocator<VM> for LargeObjectAllocator<VM> {
3233

3334
fn alloc(&mut self, size: usize, align: usize, offset: isize) -> Address {
3435
let cell: Address = self.alloc_slow(size, align, offset);
35-
allocator::align_allocation::<VM>(cell, align, offset, VM::MIN_ALIGNMENT, true)
36-
}
37-
38-
fn alloc_slow(&mut self, size: usize, align: usize, offset: isize) -> Address {
39-
self.alloc_slow_inline(size, align, offset)
36+
// We may get a null ptr from alloc due to the VM being OOM
37+
if !cell.is_zero() {
38+
allocator::align_allocation::<VM>(cell, align, offset, VM::MIN_ALIGNMENT, true)
39+
} else {
40+
cell
41+
}
4042
}
4143

4244
fn alloc_slow_once(&mut self, size: usize, align: usize, _offset: isize) -> Address {

src/util/alloc/malloc_allocator.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ impl<VM: VMBinding> Allocator<VM> for MallocAllocator<VM> {
3333
}
3434

3535
fn alloc_slow_once(&mut self, size: usize, align: usize, offset: isize) -> Address {
36-
// TODO: We currently ignore the offset field. This is wrong.
37-
// assert!(offset == 0);
38-
// assert!(align <= 16);
3936
assert!(offset >= 0);
4037

4138
let ret = self.space.alloc(self.tls, size, align, offset);

src/util/alloc/markcompact_allocator.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ impl<VM: VMBinding> Allocator<VM> for MarkCompactAllocator<VM> {
5252
let rtn = self
5353
.bump_allocator
5454
.alloc(size + Self::HEADER_RESERVED_IN_BYTES, align, offset);
55-
// return the actual object start address
56-
rtn + Self::HEADER_RESERVED_IN_BYTES
55+
// Check if the result is valid and return the actual object start address
56+
// Note that `rtn` can be null in the case of OOM
57+
if !rtn.is_zero() {
58+
rtn + Self::HEADER_RESERVED_IN_BYTES
59+
} else {
60+
rtn
61+
}
5762
}
5863

5964
fn alloc_slow_once(&mut self, size: usize, align: usize, offset: isize) -> Address {

src/util/alloc/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub(crate) mod allocator;
22
pub use allocator::fill_alignment_gap;
3+
pub use allocator::AllocationError;
34
pub use allocator::Allocator;
45

56
pub(crate) mod allocators;

src/util/memory.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::util::alloc::AllocationError;
12
use crate::util::opaque_pointer::*;
23
use crate::util::Address;
34
use crate::vm::{Collection, VMBinding};
@@ -82,14 +83,17 @@ pub fn munmap(start: Address, size: usize) -> Result<()> {
8283
wrap_libc_call(&|| unsafe { libc::munmap(start.to_mut_ptr(), size) }, 0)
8384
}
8485

85-
/// Properly handle errors from a mmap Result, including invoking the binding code for an OOM error.
86+
/// Properly handle errors from a mmap Result, including invoking the binding code in the case of
87+
/// an OOM error.
8688
pub fn handle_mmap_error<VM: VMBinding>(error: Error, tls: VMThread) -> ! {
8789
use std::io::ErrorKind;
8890

8991
match error.kind() {
9092
// From Rust nightly 2021-05-12, we started to see Rust added this ErrorKind.
9193
ErrorKind::OutOfMemory => {
92-
VM::VMCollection::out_of_memory(tls);
94+
// Signal `MmapOutOfMemory`. Expect the VM to abort immediately.
95+
trace!("Signal MmapOutOfMemory!");
96+
VM::VMCollection::out_of_memory(tls, AllocationError::MmapOutOfMemory);
9397
unreachable!()
9498
}
9599
// Before Rust had ErrorKind::OutOfMemory, this is how we capture OOM from OS calls.
@@ -99,7 +103,9 @@ pub fn handle_mmap_error<VM: VMBinding>(error: Error, tls: VMThread) -> ! {
99103
if let Some(os_errno) = error.raw_os_error() {
100104
// If it is OOM, we invoke out_of_memory() through the VM interface.
101105
if os_errno == libc::ENOMEM {
102-
VM::VMCollection::out_of_memory(tls);
106+
// Signal `MmapOutOfMemory`. Expect the VM to abort immediately.
107+
trace!("Signal MmapOutOfMemory!");
108+
VM::VMCollection::out_of_memory(tls, AllocationError::MmapOutOfMemory);
103109
unreachable!()
104110
}
105111
}

src/vm/collection.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::plan::MutatorContext;
22
use crate::scheduler::gc_work::ProcessEdgesWork;
33
use crate::scheduler::*;
4+
use crate::util::alloc::AllocationError;
45
use crate::util::opaque_pointer::*;
56
use crate::vm::VMBinding;
67

@@ -73,13 +74,21 @@ pub trait Collection<VM: VMBinding> {
7374
m: &T,
7475
);
7576

76-
/// Inform the VM for an out-of-memory error. The VM can implement its own error routine for OOM.
77-
/// Note the VM needs to fail in this call. We do not expect the VM to resume in any way.
77+
/// Inform the VM of an out-of-memory error. The binding should hook into the VM's error
78+
/// routine for OOM. Note that there are two different categories of OOM:
79+
/// * Critical OOM: This is the case where the OS is unable to mmap or acquire more memory.
80+
/// MMTk expects the VM to abort immediately if such an error is thrown.
81+
/// * Heap OOM: This is the case where the specified heap size is insufficient to execute the
82+
/// application. MMTk expects the binding to notify the VM about this OOM. MMTk makes no
83+
/// assumptions about whether the VM will continue executing or abort immediately.
84+
///
85+
/// See [`AllocationError`] for more information.
7886
///
7987
/// Arguments:
8088
/// * `tls`: The thread pointer for the mutator which failed the allocation and triggered the OOM.
81-
fn out_of_memory(_tls: VMThread) {
82-
panic!("Out of memory!");
89+
/// * `err_kind`: The type of OOM error that was encountered.
90+
fn out_of_memory(_tls: VMThread, err_kind: AllocationError) {
91+
panic!("Out of memory with {:?}!", err_kind);
8392
}
8493

8594
/// Inform the VM to schedule finalization threads.

vmbindings/dummyvm/src/tests/handle_mmap_oom.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ pub fn test_handle_mmap_oom() {
1818

1919
// The error should match the default implementation of Collection::out_of_memory()
2020
let err = panic_res.err().unwrap();
21-
assert!(err.is::<&str>());
22-
assert_eq!(err.downcast_ref::<&str>().unwrap(), &"Out of memory!");
21+
assert!(err.is::<String>());
22+
assert_eq!(err.downcast_ref::<String>().unwrap(), &"Out of memory with MmapOutOfMemory!");
2323
}

0 commit comments

Comments
 (0)