Skip to content

Commit c9dba3c

Browse files
committed
feat(allocator): block, waiting for new allocator if none are available
1 parent 477d0fd commit c9dba3c

File tree

1 file changed

+90
-25
lines changed

1 file changed

+90
-25
lines changed

crates/oxc_allocator/src/pool/fixed_size.rs

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
mem::{self, ManuallyDrop},
66
ptr::NonNull,
77
sync::{
8-
Mutex,
8+
Condvar, Mutex,
99
atomic::{AtomicBool, AtomicU32, Ordering},
1010
},
1111
};
@@ -47,45 +47,109 @@ pub struct FixedSizeAllocatorPool {
4747
allocators: Mutex<Vec<FixedSizeAllocator>>,
4848
/// ID to assign to next `Allocator` that's created
4949
next_id: AtomicU32,
50+
/// Maximum number of allocators this pool will create.
51+
/// Once this limit is reached, [`get`](Self::get) will block until an allocator
52+
/// is returned to the pool via [`add`](Self::add).
53+
capacity_limit: usize,
54+
/// Condition variable used to signal when an allocator is returned to the pool.
55+
/// Threads blocked in [`get`](Self::get) waiting for an allocator will be woken
56+
/// when [`add`](Self::add) returns one to the pool.
57+
available: Condvar,
58+
/// `true` if allocator creation has failed.
59+
/// Once set, no further attempts to create allocators will be made, since they would
60+
/// also fail.
61+
allocation_failed: AtomicBool,
5062
}
5163

5264
impl FixedSizeAllocatorPool {
5365
/// Create a new [`FixedSizeAllocatorPool`] for use across the specified number of threads.
66+
///
67+
/// The pool eagerly creates one allocator during construction. In total, it will create
68+
/// at most `max(thread_count, 1)` allocators (including this initial allocator). If all
69+
/// allocators are in use when [`get`](Self::get) is called, it will block until one is
70+
/// returned to the pool via [`add`](Self::add).
71+
///
72+
/// Passing `thread_count == 0` is equivalent to a pool that may use exactly one allocator
73+
/// in total (the one created during construction).
5474
pub fn new(thread_count: usize) -> FixedSizeAllocatorPool {
55-
// Each allocator consumes a large block of memory, so create them on demand instead of upfront,
56-
// in case not all threads end up being used (e.g. language server without `import` plugin)
57-
let allocators = Vec::with_capacity(thread_count);
58-
FixedSizeAllocatorPool { allocators: Mutex::new(allocators), next_id: AtomicU32::new(0) }
75+
let max_allocator_count = thread_count.max(1);
76+
let mut allocators = Vec::with_capacity(max_allocator_count);
77+
let Ok(allocator) = FixedSizeAllocator::try_new(0) else {
78+
panic!("Failed to create initial fixed-size allocator for the pool");
79+
};
80+
allocators.push(allocator);
81+
82+
FixedSizeAllocatorPool {
83+
allocators: Mutex::new(allocators),
84+
next_id: AtomicU32::new(1),
85+
available: Condvar::new(),
86+
capacity_limit: max_allocator_count,
87+
allocation_failed: AtomicBool::new(false),
88+
}
5989
}
6090

6191
/// Retrieve an [`Allocator`] from the pool, or create a new one if the pool is empty.
6292
///
6393
/// # Panics
64-
/// Panics if the underlying mutex is poisoned.
94+
/// * Panics if the underlying mutex is poisoned.
6595
pub fn get(&self) -> Allocator {
66-
let fixed_size_allocator = {
67-
let mut allocators = self.allocators.lock().unwrap();
68-
allocators.pop()
69-
};
96+
fn into_allocator(allocator: FixedSizeAllocator) -> Allocator {
97+
// SAFETY: `FixedSizeAllocator` is just a wrapper around `ManuallyDrop<Allocator>`,
98+
// and is `#[repr(transparent)]`, so the 2 are equivalent.
99+
let allocator =
100+
unsafe { mem::transmute::<FixedSizeAllocator, ManuallyDrop<Allocator>>(allocator) };
101+
ManuallyDrop::into_inner(allocator)
102+
}
103+
104+
{
105+
let maybe_allocator = self.allocators.lock().unwrap().pop();
106+
if let Some(allocator) = maybe_allocator {
107+
return into_allocator(allocator);
108+
}
109+
}
110+
111+
if let Some(Ok(allocator)) = self.create_new_allocator() {
112+
return into_allocator(allocator);
113+
}
114+
115+
loop {
116+
let mut maybe_allocator = self.available.wait(self.allocators.lock().unwrap()).unwrap();
117+
if let Some(allocator) = maybe_allocator.pop() {
118+
return into_allocator(allocator);
119+
}
120+
}
121+
}
122+
123+
fn create_new_allocator(&self) -> Option<Result<FixedSizeAllocator, AllocError>> {
124+
// If a previous allocation attempt failed, don't try again - it will also fail.
125+
if self.allocation_failed.load(Ordering::Relaxed) {
126+
return None;
127+
}
128+
129+
loop {
130+
let id = self.next_id.load(Ordering::Relaxed);
131+
132+
if id as usize >= self.capacity_limit {
133+
return None;
134+
}
70135

71-
let fixed_size_allocator = fixed_size_allocator.unwrap_or_else(|| {
72-
// Each allocator needs to have a unique ID, but the order those IDs are assigned in
73-
// doesn't matter, so `Ordering::Relaxed` is fine
74-
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
75136
// Protect against IDs wrapping around.
76137
// TODO: Does this work? Do we need it anyway?
77138
assert!(id < u32::MAX, "Created too many allocators");
78-
FixedSizeAllocator::try_new(id).unwrap()
79-
});
80-
81-
// Unwrap `FixedSizeAllocator`.
82-
// `add` method will wrap it again, before returning it to pool, ensuring it gets dropped properly.
83-
// SAFETY: `FixedSizeAllocator` is just a wrapper around `ManuallyDrop<Allocator>`,
84-
// and is `#[repr(transparent)]`, so the 2 are equivalent.
85-
let allocator = unsafe {
86-
mem::transmute::<FixedSizeAllocator, ManuallyDrop<Allocator>>(fixed_size_allocator)
87-
};
88-
ManuallyDrop::into_inner(allocator)
139+
140+
// Try to claim this ID. If another thread got there first, retry with new ID.
141+
if self
142+
.next_id
143+
.compare_exchange_weak(id, id + 1, Ordering::Relaxed, Ordering::Relaxed)
144+
.is_ok()
145+
{
146+
let result = FixedSizeAllocator::try_new(id);
147+
if result.is_err() {
148+
self.allocation_failed.store(true, Ordering::Relaxed);
149+
}
150+
return Some(result);
151+
}
152+
}
89153
}
90154

91155
/// Add an [`Allocator`] to the pool.
@@ -104,6 +168,7 @@ impl FixedSizeAllocatorPool {
104168

105169
let mut allocators = self.allocators.lock().unwrap();
106170
allocators.push(fixed_size_allocator);
171+
self.available.notify_one();
107172
}
108173
}
109174

0 commit comments

Comments
 (0)