oxc_allocator/allocator.rs
1use std::{
2 alloc::Layout,
3 ptr::{self, NonNull},
4 slice, str,
5};
6
7#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
8use std::mem::offset_of;
9
10use bumpalo::Bump;
11
12use oxc_data_structures::assert_unchecked;
13
14#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
15use crate::tracking::AllocationStats;
16
17/// A bump-allocated memory arena.
18///
19/// # Anatomy of an Allocator
20///
21/// [`Allocator`] is flexibly sized. It grows as required as you allocate data into it.
22///
23/// To do that, an [`Allocator`] consists of multiple memory chunks.
24///
25/// [`Allocator::new`] creates a new allocator without any chunks. When you first allocate an object
26/// into it, it will lazily create an initial chunk, the size of which is determined by the size of that
27/// first allocation.
28///
29/// As more data is allocated into the [`Allocator`], it will likely run out of capacity. At that point,
30/// a new memory chunk is added, and further allocations will use this new chunk (until it too runs out
31/// of capacity, and *another* chunk is added).
32///
33/// The data from the 1st chunk is not copied into the 2nd one. It stays where it is, which means
34/// `&` or `&mut` references to data in the first chunk remain valid. This is unlike e.g. `Vec` which
35/// copies all existing data when it grows.
36///
37/// Each chunk is at least double the size of the last one, so growth in capacity is exponential.
38///
39/// [`Allocator::reset`] keeps only the last chunk (the biggest one), and discards any other chunks,
40/// returning their memory to the global allocator. The last chunk has its cursor rewound back to
41/// the start, so it's empty, ready to be re-used for allocating more data.
42///
43/// # Recycling allocators
44///
45/// For good performance, it's ideal to create an [`Allocator`], and re-use it over and over, rather than
46/// repeatedly creating and dropping [`Allocator`]s.
47///
48/// ```
49/// // This is good!
50/// use oxc_allocator::Allocator;
51/// let mut allocator = Allocator::new();
52///
53/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
54/// for i in 0..100 {
55/// do_stuff(i, &allocator);
56/// // Reset the allocator, freeing the memory used by `do_stuff`
57/// allocator.reset();
58/// }
59/// ```
60///
61/// ```
62/// // DON'T DO THIS!
63/// # use oxc_allocator::Allocator;
64/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
65/// for i in 0..100 {
66/// let allocator = Allocator::new();
67/// do_stuff(i, &allocator);
68/// }
69/// ```
70///
71/// ```
72/// // DON'T DO THIS EITHER!
73/// # use oxc_allocator::Allocator;
74/// # let allocator = Allocator::new();
75/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
76/// for i in 0..100 {
77/// do_stuff(i, &allocator);
78/// // We haven't reset the allocator, so we haven't freed the memory used by `do_stuff`.
79/// // The allocator will grow and grow, consuming more and more memory.
80/// }
81/// ```
82///
83/// ## Why is re-using an [`Allocator`] good for performance?
84///
85/// 3 reasons:
86///
87/// #### 1. Avoid expensive system calls
88///
89/// Creating an [`Allocator`] is a fairly expensive operation as it involves a call into global allocator,
90/// which in turn will likely make a system call. Ditto when the [`Allocator`] is dropped.
91/// Re-using an existing [`Allocator`] avoids these costs.
92///
93/// #### 2. CPU cache
94///
95/// Re-using an existing allocator means you're re-using the same block of memory. If that memory was
96/// recently accessed, it's likely to be warm in the CPU cache, so memory accesses will be much faster
97/// than accessing "cold" sections of main memory.
98///
99/// This can have a very significant positive impact on performance.
100///
101/// #### 3. Capacity stabilization
102///
103/// The most efficient [`Allocator`] is one with only 1 chunk which has sufficient capacity for
104/// everything you're going to allocate into it.
105///
106/// Why?
107///
108/// 1. Every allocation will occur without the allocator needing to grow.
109///
110/// 2. This makes the "is there sufficient capacity to allocate this?" check in [`alloc`] completely
111/// predictable (the answer is always "yes"). The CPU's branch predictor swiftly learns this,
112/// speeding up operation.
113///
114/// 3. When the [`Allocator`] is reset, there are no excess chunks to discard, so no system calls.
115///
116/// Because [`reset`] keeps only the biggest chunk (see above), re-using the same [`Allocator`]
117/// for multiple similar workloads will result in the [`Allocator`] swiftly stabilizing at a capacity
118/// which is sufficient to service those workloads with a single chunk.
119///
120/// If workload is completely uniform, it reaches stable state on the 3rd round.
121///
122/// ```
123/// # use oxc_allocator::Allocator;
124/// let mut allocator = Allocator::new();
125///
126/// fn workload(allocator: &Allocator) {
127/// // Allocate 4 MB of data in small chunks
128/// for i in 0..1_000_000u32 {
129/// allocator.alloc(i);
130/// }
131/// }
132///
133/// // 1st round
134/// workload(&allocator);
135///
136/// // `allocator` has capacity for 4 MB data, but split into many chunks.
137/// // `reset` throws away all chunks except the last one which will be approx 2 MB.
138/// allocator.reset();
139///
140/// // 2nd round
141/// workload(&allocator);
142///
143/// // `workload` filled the 2 MB chunk, so a 2nd chunk was created of double the size (4 MB).
144/// // `reset` discards the smaller chunk, leaving only a single 4 MB chunk.
145/// allocator.reset();
146///
147/// // 3rd round
148/// // `allocator` now has sufficient capacity for all allocations in a single 4 MB chunk.
149/// workload(&allocator);
150///
151/// // `reset` has no chunks to discard. It keeps the single 4 MB chunk. No system calls.
152/// allocator.reset();
153///
154/// // More rounds
155/// // All serviced without needing to grow the allocator, and with no system calls.
156/// for _ in 0..100 {
157/// workload(&allocator);
158/// allocator.reset();
159/// }
160/// ```
161///
162/// # No `Drop`s
163///
164/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
165/// Memory is released in bulk when the allocator is dropped, without dropping the individual
166/// objects in the arena.
167///
168/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
169/// which own memory allocations outside the arena.
170///
171/// Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
172/// [`HashMap::new_in`], and all other methods which store data in the arena will refuse to compile
173/// if called with a [`Drop`] type.
174///
175/// ```compile_fail
176/// use oxc_allocator::{Allocator, Box};
177/// let allocator = Allocator::new();
178///
179/// struct Foo {
180/// pub a: i32
181/// }
182///
183/// impl std::ops::Drop for Foo {
184/// fn drop(&mut self) {}
185/// }
186///
187/// // This will fail to compile because `Foo` implements `Drop`
188/// let foo = Box::new_in(Foo { a: 0 }, &allocator);
189/// ```
190///
191/// ```compile_fail
192/// use oxc_allocator::{Allocator, Box};
193/// let allocator = Allocator::new();
194///
195/// struct Bar {
196/// v: std::vec::Vec<u8>,
197/// }
198///
199/// // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
200/// let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
201/// ```
202///
203/// # Examples
204///
205/// Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
206/// [`Allocator`] references to other tools.
207///
208/// ```ignore
209/// use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
210///
211/// let allocator = Allocator::default();
212/// let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
213/// assert!(parsed.errors.is_empty());
214/// ```
215///
216/// [`reset`]: Allocator::reset
217/// [`alloc`]: Allocator::alloc
218/// [`Box::new_in`]: crate::Box::new_in
219/// [`Vec::new_in`]: crate::Vec::new_in
220/// [`HashMap::new_in`]: crate::HashMap::new_in
221#[derive(Default)]
222pub struct Allocator {
223 bump: Bump,
224 /// Used to track number of allocations made in this allocator when `track_allocations` feature is enabled
225 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
226 pub(crate) stats: AllocationStats,
227}
228
229/// Offset of `stats` field, relative to `bump` field.
230/// Used in `tracking` module for allocation tracking.
231#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
232#[expect(clippy::cast_possible_wrap)]
233pub const STATS_FIELD_OFFSET: isize =
234 (offset_of!(Allocator, stats) as isize) - (offset_of!(Allocator, bump) as isize);
235
236impl Allocator {
237 /// Create a new [`Allocator`] with no initial capacity.
238 ///
239 /// This method does not reserve any memory to back the allocator. Memory for allocator's initial
240 /// chunk will be reserved lazily, when you make the first allocation into this [`Allocator`]
241 /// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
242 ///
243 /// If you can estimate the amount of memory the allocator will require to fit what you intend to
244 /// allocate into it, it is generally preferable to create that allocator with [`with_capacity`],
245 /// which reserves that amount of memory upfront. This will avoid further system calls to allocate
246 /// further chunks later on. This point is less important if you're re-using the allocator multiple
247 /// times.
248 ///
249 /// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
250 ///
251 /// [`with_capacity`]: Allocator::with_capacity
252 /// [`Box::new_in`]: crate::Box::new_in
253 /// [`Vec::new_in`]: crate::Vec::new_in
254 /// [`HashMap::new_in`]: crate::HashMap::new_in
255 //
256 // `#[inline(always)]` because just delegates to `bumpalo` method
257 #[expect(clippy::inline_always)]
258 #[inline(always)]
259 pub fn new() -> Self {
260 Self {
261 bump: Bump::new(),
262 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
263 stats: AllocationStats::default(),
264 }
265 }
266
267 /// Create a new [`Allocator`] with specified capacity.
268 ///
269 /// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
270 //
271 // `#[inline(always)]` because just delegates to `bumpalo` method
272 #[expect(clippy::inline_always)]
273 #[inline(always)]
274 pub fn with_capacity(capacity: usize) -> Self {
275 Self {
276 bump: Bump::with_capacity(capacity),
277 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
278 stats: AllocationStats::default(),
279 }
280 }
281
282 /// Allocate an object in this [`Allocator`] and return an exclusive reference to it.
283 ///
284 /// # Panics
285 /// Panics if reserving space for `T` fails.
286 ///
287 /// # Examples
288 /// ```
289 /// use oxc_allocator::Allocator;
290 ///
291 /// let allocator = Allocator::default();
292 /// let x = allocator.alloc([1u8; 20]);
293 /// assert_eq!(x, &[1u8; 20]);
294 /// ```
295 //
296 // `#[inline(always)]` because this is a very hot path and `Bump::alloc` is a very small function.
297 // We always want it to be inlined.
298 #[expect(clippy::inline_always)]
299 #[inline(always)]
300 pub fn alloc<T>(&self, val: T) -> &mut T {
301 const { assert!(!std::mem::needs_drop::<T>(), "Cannot allocate Drop type in arena") };
302
303 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
304 self.stats.record_allocation();
305
306 self.bump.alloc(val)
307 }
308
309 /// Copy a string slice into this [`Allocator`] and return a reference to it.
310 ///
311 /// # Panics
312 /// Panics if reserving space for the string fails.
313 ///
314 /// # Examples
315 /// ```
316 /// use oxc_allocator::Allocator;
317 /// let allocator = Allocator::default();
318 /// let hello = allocator.alloc_str("hello world");
319 /// assert_eq!(hello, "hello world");
320 /// ```
321 //
322 // `#[inline(always)]` because this is a hot path and `Bump::alloc_str` is a very small function.
323 // We always want it to be inlined.
324 #[expect(clippy::inline_always)]
325 #[inline(always)]
326 pub fn alloc_str<'alloc>(&'alloc self, src: &str) -> &'alloc str {
327 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
328 self.stats.record_allocation();
329
330 self.bump.alloc_str(src)
331 }
332
333 /// `Copy` a slice into this `Bump` and return an exclusive reference to the copy.
334 ///
335 /// # Panics
336 /// Panics if reserving space for the slice fails.
337 ///
338 /// # Examples
339 /// ```
340 /// use oxc_allocator::Allocator;
341 /// let allocator = Allocator::default();
342 /// let x = allocator.alloc_slice_copy(&[1, 2, 3]);
343 /// assert_eq!(x, &[1, 2, 3]);
344 /// ```
345 // `#[inline(always)]` because this is a hot path and `Bump::alloc_slice_copy` is a very small function.
346 // We always want it to be inlined.
347 #[expect(clippy::inline_always)]
348 #[inline(always)]
349 pub fn alloc_slice_copy<T: Copy>(&self, src: &[T]) -> &mut [T] {
350 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
351 self.stats.record_allocation();
352
353 self.bump.alloc_slice_copy(src)
354 }
355
356 /// Allocate space for an object with the given [`Layout`].
357 ///
358 /// The returned pointer points at uninitialized memory, and should be initialized with
359 /// [`std::ptr::write`].
360 ///
361 /// # Panics
362 ///
363 /// Panics if reserving space matching `layout` fails.
364 pub fn alloc_layout(&self, layout: Layout) -> NonNull<u8> {
365 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
366 self.stats.record_allocation();
367
368 self.bump.alloc_layout(layout)
369 }
370
371 /// Create new `&str` from a fixed-size array of `&str`s concatenated together,
372 /// allocated in the given `allocator`.
373 ///
374 /// # Panics
375 ///
376 /// Panics if the sum of length of all strings exceeds `isize::MAX`.
377 ///
378 /// # Example
379 /// ```
380 /// use oxc_allocator::Allocator;
381 ///
382 /// let allocator = Allocator::new();
383 /// let s = allocator.alloc_concat_strs_array(["hello", " ", "world", "!"]);
384 /// assert_eq!(s, "hello world!");
385 /// ```
386 // `#[inline(always)]` because want compiler to be able to remove checked addition where some of
387 // `strings` are statically known.
388 #[expect(clippy::inline_always)]
389 #[inline(always)]
390 pub fn alloc_concat_strs_array<'a, const N: usize>(&'a self, strings: [&str; N]) -> &'a str {
391 // Calculate total length of all the strings concatenated.
392 //
393 // We have to use `checked_add` here to guard against additions wrapping around
394 // if some of the input `&str`s are very long, or there's many of them.
395 //
396 // However, `&str`s have max length of `isize::MAX`.
397 // https://users.rust-lang.org/t/does-str-reliably-have-length-isize-max/126777
398 // Use `assert_unchecked!` to communicate this invariant to compiler, which allows it to
399 // optimize out the overflow checks where some of `strings` are static, so their size is known.
400 //
401 // e.g. `allocator.from_strs_array_in(["__vite_ssr_import_", str, "__"])`, for example,
402 // requires no checks at all, because the static parts have total length of 20 bytes,
403 // and `str` has max length of `isize::MAX`. `isize::MAX as usize + 20` cannot overflow `usize`.
404 // Compiler can see that, and removes the overflow check.
405 // https://godbolt.org/z/MGh44Yz5d
406 #[expect(clippy::checked_conversions)]
407 let total_len = strings.iter().fold(0usize, |total_len, s| {
408 let len = s.len();
409 // SAFETY: `&str`s have maximum length of `isize::MAX`
410 unsafe { assert_unchecked!(len <= (isize::MAX as usize)) };
411 total_len.checked_add(len).unwrap()
412 });
413 assert!(
414 isize::try_from(total_len).is_ok(),
415 "attempted to create a string longer than `isize::MAX` bytes"
416 );
417
418 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
419 self.stats.record_allocation();
420
421 // Create actual `&str` in a separate function, to ensure that `alloc_concat_strs_array`
422 // is inlined, so that compiler has knowledge to remove the overflow checks above.
423 // When some of `strings` are static, this function is usually only a few instructions.
424 // Compiler can choose whether or not to inline `alloc_concat_strs_array_with_total_len_in`.
425 // SAFETY: `total_len` has been calculated correctly above.
426 // `total_len` is `<= isize::MAX`.
427 unsafe { self.alloc_concat_strs_array_with_total_len_in(strings, total_len) }
428 }
429
430 /// Create a new `&str` from a fixed-size array of `&str`s concatenated together,
431 /// allocated in the given `allocator`, with provided `total_len`.
432 ///
433 /// # SAFETY
434 /// * `total_len` must be the total length of all `strings` concatenated.
435 /// * `total_len` must be `<= isize::MAX`.
436 unsafe fn alloc_concat_strs_array_with_total_len_in<'a, const N: usize>(
437 &'a self,
438 strings: [&str; N],
439 total_len: usize,
440 ) -> &'a str {
441 if total_len == 0 {
442 return "";
443 }
444
445 // Allocate `total_len` bytes.
446 // SAFETY: Caller guarantees `total_len <= isize::MAX`.
447 let layout = unsafe { Layout::from_size_align_unchecked(total_len, 1) };
448 let start_ptr = self.bump().alloc_layout(layout);
449
450 let mut end_ptr = start_ptr;
451 for str in strings {
452 let src_ptr = str.as_ptr();
453 let len = str.len();
454
455 // SAFETY:
456 // `src` is obtained from a `&str` with length `len`, so is valid for reading `len` bytes.
457 // `end_ptr` is within bounds of the allocation. So is `end_ptr + len`.
458 // `u8` has no alignment requirements, so `src_ptr` and `end_ptr` are sufficiently aligned.
459 // No overlapping, because we're copying from an existing `&str` to a newly allocated buffer.
460 unsafe { ptr::copy_nonoverlapping(src_ptr, end_ptr.as_ptr(), len) };
461
462 // SAFETY: We allocated sufficient capacity for all the strings concatenated.
463 // So `end_ptr.add(len)` cannot go out of bounds.
464 end_ptr = unsafe { end_ptr.add(len) };
465 }
466
467 debug_assert_eq!(end_ptr.as_ptr() as usize - start_ptr.as_ptr() as usize, total_len);
468
469 // SAFETY: We have allocated and filled `total_len` bytes starting at `start_ptr`.
470 // Concatenating multiple `&str`s results in a valid UTF-8 string.
471 unsafe {
472 let slice = slice::from_raw_parts(start_ptr.as_ptr(), total_len);
473 str::from_utf8_unchecked(slice)
474 }
475 }
476
477 /// Reset this allocator.
478 ///
479 /// Performs mass deallocation on everything allocated in this arena by resetting the pointer
480 /// into the underlying chunk of memory to the start of the chunk.
481 /// Does not run any `Drop` implementations on deallocated objects.
482 ///
483 /// If this arena has allocated multiple chunks to bump allocate into, then the excess chunks
484 /// are returned to the global allocator.
485 ///
486 /// # Examples
487 /// ```
488 /// use oxc_allocator::Allocator;
489 ///
490 /// let mut allocator = Allocator::default();
491 ///
492 /// // Allocate a bunch of things.
493 /// {
494 /// for i in 0..100 {
495 /// allocator.alloc(i);
496 /// }
497 /// }
498 ///
499 /// // Reset the arena.
500 /// allocator.reset();
501 ///
502 /// // Allocate some new things in the space previously occupied by the
503 /// // original things.
504 /// for j in 200..400 {
505 /// allocator.alloc(j);
506 /// }
507 /// ```
508 //
509 // `#[inline(always)]` because it just delegates to `bumpalo`
510 #[expect(clippy::inline_always)]
511 #[inline(always)]
512 pub fn reset(&mut self) {
513 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
514 self.stats.reset();
515
516 self.bump.reset();
517 }
518
519 /// Calculate the total capacity of this [`Allocator`] including all chunks, in bytes.
520 ///
521 /// Note: This is the total amount of memory the [`Allocator`] owns NOT the total size of data
522 /// that's been allocated in it. If you want the latter, use [`used_bytes`] instead.
523 ///
524 /// # Examples
525 /// ```
526 /// use oxc_allocator::Allocator;
527 ///
528 /// let capacity = 64 * 1024; // 64 KiB
529 /// let mut allocator = Allocator::with_capacity(capacity);
530 /// allocator.alloc(123u64); // 8 bytes
531 ///
532 /// // Result is the capacity (64 KiB), not the size of allocated data (8 bytes).
533 /// // `Allocator::with_capacity` may allocate a bit more than requested.
534 /// assert!(allocator.capacity() >= capacity);
535 /// ```
536 ///
537 /// [`used_bytes`]: Allocator::used_bytes
538 //
539 // `#[inline(always)]` because it just delegates to `bumpalo`
540 #[expect(clippy::inline_always)]
541 #[inline(always)]
542 pub fn capacity(&self) -> usize {
543 self.bump.allocated_bytes()
544 }
545
546 /// Calculate the total size of data used in this [`Allocator`], in bytes.
547 ///
548 /// This is the total amount of memory that has been *used* in the [`Allocator`], NOT the amount of
549 /// memory the [`Allocator`] owns. If you want the latter, use [`capacity`] instead.
550 ///
551 /// The result includes:
552 ///
553 /// 1. Padding bytes between objects which have been allocated to preserve alignment of types
554 /// where they have different alignments or have larger-than-typical alignment.
555 /// 2. Excess capacity in [`Vec`]s, [`StringBuilder`]s and [`HashMap`]s.
556 /// 3. Objects which were allocated but later dropped. [`Allocator`] does not re-use allocations,
557 /// so anything which is allocated into arena continues to take up "dead space", even after it's
558 /// no longer referenced anywhere.
559 /// 4. "Dead space" left over where a [`Vec`], [`StringBuilder`] or [`HashMap`] has grown and had to
560 /// make a new allocation to accommodate its new larger size. Its old allocation continues to
561 /// take up "dead" space in the allocator, unless it was the most recent allocation.
562 ///
563 /// In practice, this almost always means that the result returned from this function will be an
564 /// over-estimate vs the amount of "live" data in the arena.
565 ///
566 /// However, if you are using the result of this method to create a new `Allocator` to clone
567 /// an AST into, it is theoretically possible (though very unlikely) that it may be a slight
568 /// under-estimate of the capacity required in new allocator to clone the AST into, depending
569 /// on the order that `&str`s were allocated into arena in parser vs the order they get allocated
570 /// during cloning. The order allocations are made in affects the amount of padding bytes required.
571 ///
572 /// # Examples
573 /// ```
574 /// use oxc_allocator::{Allocator, Vec};
575 ///
576 /// let capacity = 64 * 1024; // 64 KiB
577 /// let mut allocator = Allocator::with_capacity(capacity);
578 ///
579 /// allocator.alloc(1u8); // 1 byte with alignment 1
580 /// allocator.alloc(2u8); // 1 byte with alignment 1
581 /// allocator.alloc(3u64); // 8 bytes with alignment 8
582 ///
583 /// // Only 10 bytes were allocated, but 16 bytes were used, in order to align `3u64` on 8
584 /// assert_eq!(allocator.used_bytes(), 16);
585 ///
586 /// allocator.reset();
587 ///
588 /// let mut vec = Vec::<u64>::with_capacity_in(2, &allocator);
589 ///
590 /// // Allocate something else, so `vec`'s allocation is not the most recent
591 /// allocator.alloc(123u64);
592 ///
593 /// // `vec` has to grow beyond it's initial capacity
594 /// vec.extend([1, 2, 3, 4]);
595 ///
596 /// // `vec` takes up 32 bytes, and `123u64` takes up 8 bytes = 40 total.
597 /// // But there's an additional 16 bytes consumed for `vec`'s original capacity of 2,
598 /// // which is still using up space
599 /// assert_eq!(allocator.used_bytes(), 56);
600 /// ```
601 ///
602 /// [`capacity`]: Allocator::capacity
603 /// [`Vec`]: crate::Vec
604 /// [`StringBuilder`]: crate::StringBuilder
605 /// [`HashMap`]: crate::HashMap
606 pub fn used_bytes(&self) -> usize {
607 let mut bytes = 0;
608 // SAFETY: No allocations are made while `chunks_iter` is alive. No data is read from the chunks.
609 let chunks_iter = unsafe { self.bump.iter_allocated_chunks_raw() };
610 for (_, size) in chunks_iter {
611 bytes += size;
612 }
613 bytes
614 }
615
616 /// Get inner [`bumpalo::Bump`].
617 ///
618 /// This method is not public. We don't want to expose `Bump` to user.
619 /// The fact that we're using `bumpalo` is an internal implementation detail.
620 //
621 // `#[inline(always)]` because it's a no-op
622 #[expect(clippy::inline_always)]
623 #[inline(always)]
624 pub(crate) fn bump(&self) -> &Bump {
625 &self.bump
626 }
627
628 /// Create [`Allocator`] from a [`bumpalo::Bump`].
629 ///
630 /// This method is not public. Only used by [`Allocator::from_raw_parts`].
631 //
632 // `#[inline(always)]` because it's a no-op
633 #[cfg(feature = "from_raw_parts")]
634 #[expect(clippy::inline_always)]
635 #[inline(always)]
636 pub(crate) fn from_bump(bump: Bump) -> Self {
637 Self {
638 bump,
639 #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
640 stats: AllocationStats::default(),
641 }
642 }
643}
644
645#[cfg(test)]
646mod test {
647 use super::Allocator;
648
649 #[test]
650 fn test_api() {
651 let mut allocator = Allocator::default();
652 {
653 let array = allocator.alloc([123; 10]);
654 assert_eq!(array, &[123; 10]);
655 let str = allocator.alloc_str("hello");
656 assert_eq!(str, "hello");
657 }
658 allocator.reset();
659 }
660
661 #[test]
662 fn string_from_array_len_1() {
663 let allocator = Allocator::default();
664 let s = allocator.alloc_concat_strs_array(["hello"]);
665 assert_eq!(s, "hello");
666 }
667
668 #[test]
669 fn string_from_array_len_2() {
670 let allocator = Allocator::default();
671 let s = allocator.alloc_concat_strs_array(["hello", "world!"]);
672 assert_eq!(s, "helloworld!");
673 }
674
675 #[test]
676 fn string_from_array_len_3() {
677 let hello = "hello";
678 let world = std::string::String::from("world");
679 let allocator = Allocator::default();
680 let s = allocator.alloc_concat_strs_array([hello, &world, "!"]);
681 assert_eq!(s, "helloworld!");
682 }
683
684 #[test]
685 fn string_from_empty_array() {
686 let allocator = Allocator::default();
687 let s = allocator.alloc_concat_strs_array([]);
688 assert_eq!(s, "");
689 }
690
691 #[test]
692 fn string_from_array_of_empty_strs() {
693 let allocator = Allocator::default();
694 let s = allocator.alloc_concat_strs_array(["", "", ""]);
695 assert_eq!(s, "");
696 }
697
698 #[test]
699 fn string_from_array_containing_some_empty_strs() {
700 let allocator = Allocator::default();
701 let s = allocator.alloc_concat_strs_array(["", "hello", ""]);
702 assert_eq!(s, "hello");
703 }
704}