Skip to content

Commit 2349f15

Browse files
committed
refactor: object pool
1 parent c317241 commit 2349f15

File tree

8 files changed

+89
-46
lines changed

8 files changed

+89
-46
lines changed

benches/bench_complex_replace_source.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ pub use criterion::*;
99
pub use codspeed_criterion_compat::*;
1010

1111
use rspack_sources::{
12-
BoxSource, MapOptions, OriginalSource, ReplaceSource, SourceExt,
12+
using_object_pool, BoxSource, MapOptions, OriginalSource, ReplaceSource,
13+
SourceExt,
1314
};
1415

1516
static LARGE_REPLACE_SOURCE: LazyLock<BoxSource> = LazyLock::new(|| {
@@ -36723,8 +36724,10 @@ static LARGE_REPLACE_SOURCE: LazyLock<BoxSource> = LazyLock::new(|| {
3672336724
pub fn benchmark_complex_replace_source_map(b: &mut Bencher) {
3672436725
let source = LARGE_REPLACE_SOURCE.clone();
3672536726

36726-
b.iter(|| {
36727-
black_box(source.map(&MapOptions::default()));
36727+
using_object_pool(|| {
36728+
b.iter(|| {
36729+
black_box(source.map(&MapOptions::default()));
36730+
});
3672836731
});
3672936732
}
3673036733

benches/benchmark_repetitive_react_components.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ pub use criterion::*;
99
pub use codspeed_criterion_compat::*;
1010

1111
use rspack_sources::{
12-
BoxSource, ConcatSource, MapOptions, OriginalSource, RawStringSource,
13-
ReplaceSource, ReplacementEnforce, Source, SourceExt, SourceMap,
14-
SourceMapSource, SourceMapSourceOptions,
12+
using_object_pool, BoxSource, ConcatSource, MapOptions, OriginalSource,
13+
RawStringSource, ReplaceSource, ReplacementEnforce, Source, SourceExt,
14+
SourceMap, SourceMapSource, SourceMapSourceOptions,
1515
};
1616

1717
static REPETITIVE_1K_REACT_COMPONENTS_SOURCE: LazyLock<BoxSource> =
@@ -3504,8 +3504,10 @@ static REPETITIVE_1K_REACT_COMPONENTS_SOURCE: LazyLock<BoxSource> =
35043504
pub fn benchmark_repetitive_react_components_map(b: &mut Bencher) {
35053505
let source = REPETITIVE_1K_REACT_COMPONENTS_SOURCE.clone();
35063506

3507-
b.iter(|| {
3508-
black_box(source.map(&MapOptions::default()));
3507+
using_object_pool(|| {
3508+
b.iter(|| {
3509+
black_box(source.map(&MapOptions::default()));
3510+
});
35093511
});
35103512
}
35113513

src/helpers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ where
833833
match inner_source_contents.get(&inner_source_index) {
834834
Some(Some(source_content)) => Some(
835835
split_into_lines(source_content)
836-
.map(|line| WithIndices::new(line))
836+
.map(WithIndices::new)
837837
.collect(),
838838
),
839839
_ => None,
@@ -933,7 +933,7 @@ where
933933
match inner_source_contents.get(&inner_source_index) {
934934
Some(Some(source_content)) => Some(
935935
split_into_lines(source_content)
936-
.map(|line| WithIndices::new(line))
936+
.map(WithIndices::new)
937937
.collect(),
938938
),
939939
_ => None,

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod encoder;
77
mod error;
88
mod helpers;
99
mod linear_map;
10-
mod memory_pool;
10+
mod object_pool;
1111
mod original_source;
1212
mod raw_source;
1313
mod replace_source;
@@ -40,3 +40,5 @@ pub mod stream_chunks {
4040
}
4141

4242
pub use helpers::{decode_mappings, encode_mappings};
43+
44+
pub use object_pool::using_object_pool;

src/memory_pool.rs renamed to src/object_pool.rs

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::{
2-
cell::RefCell, collections::BTreeMap, rc::Rc, sync::atomic::AtomicBool,
3-
};
1+
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
42

53
// Vector pooling minimum capacity threshold
64
// Recommended threshold: 64
@@ -11,7 +9,7 @@ use std::{
119
// 4. Empirical value: 64 is a proven balance point in real projects
1210
const MIN_POOL_CAPACITY: usize = 64;
1311

14-
trait Poolable {
12+
pub trait Poolable {
1513
fn with_capacity(capacity: usize) -> Self;
1614
fn capacity(&self) -> usize;
1715
fn clear(&mut self);
@@ -34,50 +32,64 @@ impl<T> Poolable for Vec<T> {
3432
/// A memory pool for reusing `T` allocations to reduce memory allocation overhead.
3533
#[derive(Default, Debug)]
3634
pub struct ObjectPool<T> {
37-
usize_vec_pool: RefCell<BTreeMap<usize, Vec<T>>>,
35+
objects: Rc<RefCell<BTreeMap<usize, Vec<T>>>>,
36+
}
37+
38+
impl<T> Clone for ObjectPool<T> {
39+
fn clone(&self) -> Self {
40+
Self {
41+
objects: self.objects.clone(),
42+
}
43+
}
3844
}
3945

4046
impl<T: Poolable> ObjectPool<T> {
4147
/// Retrieves a reusable `T` from the pool with at least the requested capacity.
4248
pub fn pull(&self, requested_capacity: usize) -> T {
4349
if requested_capacity < MIN_POOL_CAPACITY
44-
|| self.usize_vec_pool.borrow().is_empty()
50+
|| self.objects.borrow().is_empty()
4551
{
4652
return T::with_capacity(requested_capacity);
4753
}
48-
let mut usize_vec_pool = self.usize_vec_pool.borrow_mut();
49-
if let Some((_, bucket)) =
50-
usize_vec_pool.range_mut(requested_capacity..).next()
51-
{
52-
if let Some(mut v) = bucket.pop() {
53-
v.clear();
54-
return v;
54+
let mut objects = self.objects.borrow_mut();
55+
if let Some((_, bucket)) = objects.range_mut(requested_capacity..).next() {
56+
if let Some(mut object) = bucket.pop() {
57+
object.clear();
58+
return object;
5559
}
5660
}
5761
T::with_capacity(requested_capacity)
5862
}
5963

6064
/// Returns a `T` to the pool for future reuse.
61-
pub fn ret(&self, object: T) {
65+
pub fn return_to_pool(&self, object: T) {
6266
if object.capacity() < MIN_POOL_CAPACITY {
6367
return;
6468
}
65-
let mut usize_vec_pool = self.usize_vec_pool.borrow_mut();
69+
let mut objects = self.objects.borrow_mut();
6670
let cap = object.capacity();
67-
let bucket = usize_vec_pool.entry(cap).or_default();
71+
let bucket = objects.entry(cap).or_default();
6872
bucket.push(object);
6973
}
7074
}
7175

76+
/// A smart pointer that holds a pooled object and automatically returns it to the pool when dropped.
77+
///
78+
/// `Pooled<T>` implements RAII (Resource Acquisition Is Initialization) pattern to manage
79+
/// pooled objects lifecycle. When the `Pooled` instance is dropped, the contained object
80+
/// is automatically returned to its associated pool for future reuse.
7281
#[derive(Debug)]
7382
pub struct Pooled<T: Poolable> {
7483
object: Option<T>,
75-
pool: Rc<ObjectPool<T>>,
84+
pool: Option<ObjectPool<T>>,
7685
}
7786

7887
impl<T: Poolable> Pooled<T> {
79-
fn new(pool: Rc<ObjectPool<T>>, requested_capacity: usize) -> Self {
80-
let object = pool.pull(requested_capacity);
88+
pub fn new(pool: Option<ObjectPool<T>>, requested_capacity: usize) -> Self {
89+
let object = match &pool {
90+
Some(pool) => pool.pull(requested_capacity),
91+
None => T::with_capacity(requested_capacity),
92+
};
8193
Self {
8294
object: Some(object),
8395
pool,
@@ -96,7 +108,9 @@ impl<T: Poolable> Pooled<T> {
96108
impl<T: Poolable> Drop for Pooled<T> {
97109
fn drop(&mut self) {
98110
if let Some(object) = self.object.take() {
99-
self.pool.ret(object);
111+
if let Some(pool) = &self.pool {
112+
pool.return_to_pool(object);
113+
}
100114
}
101115
}
102116
}
@@ -115,14 +129,33 @@ impl<T: Poolable> std::ops::DerefMut for Pooled<T> {
115129
}
116130
}
117131

118-
pub(crate) const USING_OBJECT_POOL: AtomicBool = AtomicBool::new(false);
132+
thread_local! {
133+
pub static USIZE_VEC_POOL: RefCell<Option<ObjectPool<Vec<usize>>>> = RefCell::default();
134+
}
119135

136+
/// Executes a function with object pooling enabled for the current thread.
137+
///
138+
/// This function temporarily enables a thread-local object pool for `Vec<usize>` allocations,
139+
/// executes the provided closure, and then cleans up the pool to prevent memory leaks.
120140
pub fn using_object_pool<F, R>(f: F) -> R
121141
where
122142
F: FnOnce() -> R,
123143
{
124-
USING_OBJECT_POOL.store(true, std::sync::atomic::Ordering::SeqCst);
144+
// Initialize the thread-local pool if needed
145+
USIZE_VEC_POOL.with(|pool| {
146+
let mut pool_ref = pool.borrow_mut();
147+
if pool_ref.is_none() {
148+
*pool_ref = Some(ObjectPool::default());
149+
}
150+
});
151+
125152
let result = f();
126-
USING_OBJECT_POOL.store(false, std::sync::atomic::Ordering::SeqCst);
153+
154+
// Clean up the pool to prevent memory retention
155+
// This ensures no memory is held between different pooling sessions
156+
USIZE_VEC_POOL.with(|pool| {
157+
pool.borrow_mut().take();
158+
});
159+
127160
result
128161
}

src/replace_source.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ impl StreamChunks for ReplaceSource {
391391
match source_content {
392392
SourceContent::Raw(source) => {
393393
let lines = split_into_lines(source)
394-
.map(|line| WithIndices::new(line))
394+
.map(WithIndices::new)
395395
.collect::<Vec<_>>();
396396
let matched =
397397
check_content_at_position(&lines, line, column, expected_chunk);

src/with_indices.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::{cell::OnceCell, marker::PhantomData};
22

3-
use crate::helpers::SourceText;
3+
use crate::{
4+
helpers::SourceText,
5+
object_pool::{Pooled, USIZE_VEC_POOL},
6+
};
47

58
#[derive(Debug)]
69
pub struct WithIndices<'text, S>
@@ -10,7 +13,7 @@ where
1013
/// line is a string reference
1114
pub line: S,
1215
/// the byte position of each `char` in `line` string slice .
13-
pub indices_indexes: OnceCell<PooledUsizeVec<'context>>,
16+
pub char_byte_indices: OnceCell<Pooled<Vec<usize>>>,
1417
data: PhantomData<&'text S>,
1518
}
1619

@@ -20,7 +23,7 @@ where
2023
{
2124
pub fn new(line: S) -> Self {
2225
Self {
23-
indices_indexes: OnceCell::new(),
26+
char_byte_indices: OnceCell::new(),
2427
line,
2528
data: PhantomData,
2629
}
@@ -32,15 +35,17 @@ where
3235
return S::default();
3336
}
3437

35-
let indices_indexes = self.indices_indexes.get_or_init(|| {
36-
let mut vec = PooledUsizeVec::new(self.line.len());
38+
let char_byte_indices = self.char_byte_indices.get_or_init(|| {
39+
let mut vec = USIZE_VEC_POOL.with(|pool| {
40+
Pooled::new(pool.borrow().as_ref().cloned(), self.line.len())
41+
});
3742
vec.extend(self.line.char_indices().map(|(i, _)| i));
3843
vec
3944
});
4045

4146
let str_len = self.line.len();
42-
let start = *indices_indexes.get(start_index).unwrap_or(&str_len);
43-
let end = *indices_indexes.get(end_index).unwrap_or(&str_len);
47+
let start = *char_byte_indices.get(start_index).unwrap_or(&str_len);
48+
let end = *char_byte_indices.get(end_index).unwrap_or(&str_len);
4449

4550
#[allow(unsafe_code)]
4651
unsafe {

tests/compat_source.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use rspack_sources::stream_chunks::{
66
stream_chunks_default, GeneratedInfo, OnChunk, OnName, OnSource, StreamChunks,
77
};
88
use rspack_sources::{
9-
ConcatSource, MapOptions, MemoryPool, RawStringSource, Rope, Source,
10-
SourceExt, SourceMap, SourceValue,
9+
ConcatSource, MapOptions, RawStringSource, Rope, Source, SourceExt,
10+
SourceMap, SourceValue,
1111
};
1212

1313
#[derive(Debug, Eq)]
@@ -42,14 +42,12 @@ impl Source for CompatSource {
4242
impl StreamChunks for CompatSource {
4343
fn stream_chunks<'a>(
4444
&'a self,
45-
memory_pool: &'a MemoryPool,
4645
options: &MapOptions,
4746
on_chunk: OnChunk<'_, 'a>,
4847
on_source: OnSource<'_, 'a>,
4948
on_name: OnName<'_, 'a>,
5049
) -> GeneratedInfo {
5150
stream_chunks_default(
52-
memory_pool,
5351
self.0,
5452
self.1.as_ref(),
5553
options,

0 commit comments

Comments
 (0)