Skip to content

Commit 4fe3aac

Browse files
committed
perf(formatter): use ArenaVec and ArenaBox (#15420)
This PR aims to replace the Formatter's standard `Vec` and `Box` usages with `ArenaVec` and `ArenaBox` to gain performance, which could reduce the `drop` overhead. There are three key changes: 1. Change the Document's element from `Vec<FormatElement<'a>>` ```diff pub struct Document<'a> { - elements: Vec<FormatElement<'a>>, + elements: &'a [FormatElement<'a>], } ``` 2. Change the `FormatElement::interned`'s argument from `Rc<[FormatElement<'a>]>` to `&'a [FormatElement<'a>]` ```diff - pub struct Interned<'a>(Rc<[FormatElement<'a>]>); + pub struct Interned<'a>(&'a [FormatElement<'a>]); ``` 3. Change the `FormatElement::variants`'s variants from `Box<[Box<[FormatElement<'a>]>]>` to `&'a [&'a [FormatElement<'a>]]` ```diff pub struct BestFittingElement<'a> { - variants: Box<[Box<[FormatElement<'a>]>]>, + variants: &'a [&'a [FormatElement<'a>]], } ``` All other changes stem from those mentioned above. Replacing the `Rc` with `&a` reference approach was suggested and guided by @overlookmotel. Thank you!
1 parent 7d1ebad commit 4fe3aac

File tree

10 files changed

+105
-75
lines changed

10 files changed

+105
-75
lines changed

crates/oxc_formatter/src/formatter/buffer.rs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::{
77

88
use rustc_hash::FxHashMap;
99

10+
use oxc_allocator::{Allocator, TakeIn, Vec as ArenaVec};
11+
1012
use super::{
1113
Arguments, Format, FormatElement, FormatResult, FormatState,
1214
format_element::Interned,
@@ -175,34 +177,35 @@ impl<'ast, W: Buffer<'ast> + ?Sized> Buffer<'ast> for &mut W {
175177
#[derive(Debug)]
176178
pub struct VecBuffer<'buf, 'ast> {
177179
state: &'buf mut FormatState<'ast>,
178-
elements: Vec<FormatElement<'ast>>,
180+
elements: ArenaVec<'ast, FormatElement<'ast>>,
179181
}
180182

181183
impl<'buf, 'ast> VecBuffer<'buf, 'ast> {
182184
pub fn new(state: &'buf mut FormatState<'ast>) -> Self {
183-
Self::new_with_vec(state, Vec::new())
185+
Self::new_with_vec(state, ArenaVec::new_in(state.context().allocator()))
184186
}
185187

186188
pub fn new_with_vec(
187189
state: &'buf mut FormatState<'ast>,
188-
elements: Vec<FormatElement<'ast>>,
190+
elements: ArenaVec<'ast, FormatElement<'ast>>,
189191
) -> Self {
190192
Self { state, elements }
191193
}
192194

193195
/// Creates a buffer with the specified capacity
194196
pub fn with_capacity(capacity: usize, state: &'buf mut FormatState<'ast>) -> Self {
195-
Self { state, elements: Vec::with_capacity(capacity) }
197+
let elements = ArenaVec::with_capacity_in(capacity, state.context().allocator());
198+
Self { state, elements }
196199
}
197200

198201
/// Consumes the buffer and returns the written [`FormatElement]`s as a vector.
199-
pub fn into_vec(self) -> Vec<FormatElement<'ast>> {
202+
pub fn into_vec(self) -> ArenaVec<'ast, FormatElement<'ast>> {
200203
self.elements
201204
}
202205

203206
/// Takes the elements without consuming self
204-
pub fn take_vec(&mut self) -> Vec<FormatElement<'ast>> {
205-
std::mem::take(&mut self.elements)
207+
pub fn take_vec(&mut self) -> ArenaVec<'ast, FormatElement<'ast>> {
208+
self.elements.take_in(self.state.context().allocator())
206209
}
207210
}
208211

@@ -493,8 +496,13 @@ impl<'buf, 'ast> RemoveSoftLinesBuffer<'buf, 'ast> {
493496
}
494497

495498
/// Removes the soft line breaks from an interned element.
496-
fn clean_interned(&mut self, interned: &Interned<'ast>) -> Interned<'ast> {
497-
clean_interned(interned, &mut self.interned_cache, &mut self.conditional_content_stack)
499+
fn clean_interned(&mut self, interned: Interned<'ast>) -> Interned<'ast> {
500+
clean_interned(
501+
interned,
502+
&mut self.interned_cache,
503+
&mut self.conditional_content_stack,
504+
self.inner.state().context().allocator(),
505+
)
498506
}
499507

500508
/// Marker for whether a `StartConditionalContent(mode: Expanded)` has been
@@ -509,11 +517,12 @@ impl<'buf, 'ast> RemoveSoftLinesBuffer<'buf, 'ast> {
509517

510518
// Extracted to function to avoid monomorphization
511519
fn clean_interned<'ast>(
512-
interned: &Interned<'ast>,
520+
interned: Interned<'ast>,
513521
interned_cache: &mut FxHashMap<Interned<'ast>, Interned<'ast>>,
514522
condition_content_stack: &mut Vec<Condition>,
523+
allocator: &'ast Allocator,
515524
) -> Interned<'ast> {
516-
if let Some(cleaned) = interned_cache.get(interned) {
525+
if let Some(cleaned) = interned_cache.get(&interned) {
517526
cleaned.clone()
518527
} else {
519528
// Find the first soft line break element, interned element, or conditional expanded
@@ -522,18 +531,23 @@ fn clean_interned<'ast>(
522531
FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace)
523532
| FormatElement::Tag(Tag::StartConditionalContent(_) | Tag::EndConditionalContent)
524533
| FormatElement::BestFitting(_) => {
525-
let mut cleaned = Vec::new();
526-
cleaned.extend_from_slice(&interned[..index]);
534+
let mut cleaned =
535+
ArenaVec::from_iter_in(interned[..index].iter().cloned(), allocator);
527536
Some((cleaned, &interned[index..]))
528537
}
529538
FormatElement::Interned(inner) => {
530-
let cleaned_inner = clean_interned(inner, interned_cache, condition_content_stack);
539+
let cleaned_inner = clean_interned(
540+
inner.clone(),
541+
interned_cache,
542+
condition_content_stack,
543+
allocator,
544+
);
531545

532546
if &cleaned_inner == inner {
533547
None
534548
} else {
535-
let mut cleaned = Vec::with_capacity(interned.len());
536-
cleaned.extend_from_slice(&interned[..index]);
549+
let mut cleaned = ArenaVec::with_capacity_in(interned.len(), allocator);
550+
cleaned.extend(interned[..index].iter().cloned());
537551
cleaned.push(FormatElement::Interned(cleaned_inner));
538552
Some((cleaned, &interned[index + 1..]))
539553
}
@@ -567,9 +581,10 @@ fn clean_interned<'ast>(
567581

568582
FormatElement::Interned(interned) => {
569583
cleaned.push(FormatElement::Interned(clean_interned(
570-
interned,
584+
interned.clone(),
571585
interned_cache,
572586
condition_content_stack,
587+
allocator,
573588
)));
574589
}
575590
// Since this buffer aims to simulate infinite print width, we don't need to retain the best fitting.
@@ -588,15 +603,14 @@ fn clean_interned<'ast>(
588603
None => interned.clone(),
589604
};
590605

591-
interned_cache.insert(interned.clone(), result.clone());
606+
interned_cache.insert(interned, result.clone());
592607
result
593608
}
594609
}
595610

596611
impl<'ast> Buffer<'ast> for RemoveSoftLinesBuffer<'_, 'ast> {
597612
fn write_element(&mut self, element: FormatElement<'ast>) -> FormatResult<()> {
598-
let mut element_stack = Vec::new();
599-
element_stack.push(element);
613+
let mut element_stack = Vec::from_iter([element]);
600614
while let Some(element) = element_stack.pop() {
601615
match element {
602616
FormatElement::Tag(Tag::StartConditionalContent(condition)) => {
@@ -614,14 +628,14 @@ impl<'ast> Buffer<'ast> for RemoveSoftLinesBuffer<'_, 'ast> {
614628
self.inner.write_element(FormatElement::Space)?;
615629
}
616630
FormatElement::Interned(interned) => {
617-
let cleaned = self.clean_interned(&interned);
631+
let cleaned = self.clean_interned(interned);
618632
self.inner.write_element(FormatElement::Interned(cleaned))?;
619633
}
620634
// Since this buffer aims to simulate infinite print width, we don't need to retain the best fitting.
621635
// Just extract the flattest variant and then handle elements within it.
622636
FormatElement::BestFitting(best_fitting) => {
623637
let most_flat = best_fitting.most_flat();
624-
most_flat.iter().rev().for_each(|element| element_stack.push(element.clone()));
638+
element_stack.extend(most_flat.iter().rev().cloned());
625639
}
626640
element => self.inner.write_element(element)?,
627641
}

crates/oxc_formatter/src/formatter/builders.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use Tag::{
66
StartDedent, StartEntry, StartFill, StartGroup, StartIndent, StartIndentIfGroupBreaks,
77
StartLabelled, StartLineSuffix,
88
};
9+
use oxc_allocator::Vec as ArenaVec;
910
use oxc_span::{GetSpan, Span};
1011
use oxc_syntax::identifier::{is_identifier_name, is_line_terminator, is_white_space_single_line};
1112

@@ -2549,9 +2550,12 @@ impl<'ast> Format<'ast> for BestFitting<'_, 'ast> {
25492550
buffer.write_fmt(Arguments::from(variant))?;
25502551
buffer.write_element(FormatElement::Tag(EndEntry))?;
25512552

2552-
formatted_variants.push(buffer.take_vec().into_boxed_slice());
2553+
formatted_variants.push(buffer.take_vec().into_bump_slice());
25532554
}
25542555

2556+
let formatted_variants =
2557+
ArenaVec::from_iter_in(formatted_variants, f.context().allocator());
2558+
25552559
// SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore,
25562560
// safe to call into the unsafe `from_vec_unchecked` function
25572561
let element = unsafe {

crates/oxc_formatter/src/formatter/format_element/document.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use cow_utils::CowUtils;
33
use std::ops::Deref;
44

5-
use oxc_allocator::Allocator;
5+
use oxc_allocator::{Allocator, Vec as ArenaVec};
66
use oxc_ast::Comment;
77
use oxc_span::SourceType;
88
use rustc_hash::FxHashMap;
@@ -21,7 +21,7 @@ use crate::{format, write};
2121
/// A formatted document.
2222
#[derive(Debug, Clone, Eq, PartialEq, Default)]
2323
pub struct Document<'a> {
24-
elements: Vec<FormatElement<'a>>,
24+
elements: &'a [FormatElement<'a>],
2525
}
2626

2727
impl Document<'_> {
@@ -140,26 +140,26 @@ impl Document<'_> {
140140
}
141141
}
142142

143-
impl<'a> From<Vec<FormatElement<'a>>> for Document<'a> {
144-
fn from(elements: Vec<FormatElement<'a>>) -> Self {
145-
Self { elements }
143+
impl<'a> From<ArenaVec<'a, FormatElement<'a>>> for Document<'a> {
144+
fn from(elements: ArenaVec<'a, FormatElement<'a>>) -> Self {
145+
Self { elements: elements.into_bump_slice() }
146146
}
147147
}
148148

149149
impl<'a> Deref for Document<'a> {
150150
type Target = [FormatElement<'a>];
151151

152152
fn deref(&self) -> &Self::Target {
153-
self.elements.as_slice()
153+
self.elements
154154
}
155155
}
156156

157157
impl std::fmt::Display for Document<'_> {
158158
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159159
let allocator = Allocator::default();
160160
let context = FormatContext::dummy(&allocator);
161-
let formatted = format!(context, [self.elements.as_slice()])
162-
.expect("Formatting not to throw any FormatErrors");
161+
let formatted =
162+
format!(context, [self.elements]).expect("Formatting not to throw any FormatErrors");
163163

164164
f.write_str(formatted.print().expect("Expected a valid document").as_code())
165165
}

crates/oxc_formatter/src/formatter/format_element/mod.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ pub mod tag;
55
// use biome_rowan::static_assert;
66
use std::hash::{Hash, Hasher};
77
use std::num::NonZeroU32;
8+
use std::ptr;
89
use std::{borrow::Cow, ops::Deref, rc::Rc};
910

1011
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
1112

13+
use oxc_allocator::{Address, Box as ArenaBox, Vec as ArenaVec};
14+
1215
use crate::{IndentWidth, TabWidth};
1316

1417
use super::{
@@ -146,28 +149,25 @@ impl PrintMode {
146149
}
147150

148151
#[derive(Clone)]
149-
pub struct Interned<'a>(Rc<[FormatElement<'a>]>);
152+
pub struct Interned<'a>(&'a [FormatElement<'a>]);
150153

151154
impl<'a> Interned<'a> {
152-
pub(super) fn new(content: Vec<FormatElement<'a>>) -> Self {
153-
Self(content.into())
155+
pub(super) fn new(content: ArenaVec<'a, FormatElement<'a>>) -> Self {
156+
Self(content.into_bump_slice())
154157
}
155158
}
156159

157160
impl PartialEq for Interned<'_> {
158161
fn eq(&self, other: &Interned<'_>) -> bool {
159-
Rc::ptr_eq(&self.0, &other.0)
162+
ptr::eq(self.0, other.0)
160163
}
161164
}
162165

163166
impl Eq for Interned<'_> {}
164167

165168
impl Hash for Interned<'_> {
166-
fn hash<H>(&self, hasher: &mut H)
167-
where
168-
H: Hasher,
169-
{
170-
Rc::as_ptr(&self.0).hash(hasher);
169+
fn hash<H: Hasher>(&self, state: &mut H) {
170+
self.0.as_ptr().addr().hash(state);
171171
}
172172
}
173173

@@ -181,7 +181,7 @@ impl<'a> Deref for Interned<'a> {
181181
type Target = [FormatElement<'a>];
182182

183183
fn deref(&self) -> &Self::Target {
184-
&self.0
184+
self.0
185185
}
186186
}
187187

@@ -305,7 +305,7 @@ pub struct BestFittingElement<'a> {
305305
/// The different variants for this element.
306306
/// The first element is the one that takes up the most space horizontally (the most flat),
307307
/// The last element takes up the least space horizontally (but most horizontal space).
308-
variants: Box<[Box<[FormatElement<'a>]>]>,
308+
variants: &'a [&'a [FormatElement<'a>]],
309309
}
310310

311311
impl<'a> BestFittingElement<'a> {
@@ -318,13 +318,13 @@ impl<'a> BestFittingElement<'a> {
318318
/// ## Safety
319319
/// The slice must contain at least two variants.
320320
#[doc(hidden)]
321-
pub unsafe fn from_vec_unchecked(variants: Vec<Box<[FormatElement<'a>]>>) -> Self {
321+
pub unsafe fn from_vec_unchecked(variants: ArenaVec<'a, &'a [FormatElement<'a>]>) -> Self {
322322
debug_assert!(
323323
variants.len() >= 2,
324324
"Requires at least the least expanded and most expanded variants"
325325
);
326326

327-
Self { variants: variants.into_boxed_slice() }
327+
Self { variants: variants.into_bump_slice() }
328328
}
329329

330330
/// Returns the most expanded variant
@@ -334,8 +334,8 @@ impl<'a> BestFittingElement<'a> {
334334
)
335335
}
336336

337-
pub fn variants(&self) -> &[Box<[FormatElement<'a>]>] {
338-
&self.variants
337+
pub fn variants(&self) -> &[&'a [FormatElement<'a>]] {
338+
self.variants
339339
}
340340

341341
/// Returns the least expanded variant
@@ -348,7 +348,7 @@ impl<'a> BestFittingElement<'a> {
348348

349349
impl std::fmt::Debug for BestFittingElement<'_> {
350350
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351-
f.debug_list().entries(&*self.variants).finish()
351+
f.debug_list().entries(self.variants).finish()
352352
}
353353
}
354354

crates/oxc_formatter/src/formatter/formatter.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![allow(clippy::module_inception)]
22

3-
use oxc_allocator::{Address, Allocator};
3+
use oxc_allocator::{Address, Allocator, Vec as ArenaVec};
44
use oxc_ast::AstKind;
55

66
use crate::options::FormatOptions;
@@ -27,7 +27,7 @@ impl<'buf, 'ast> Formatter<'buf, 'ast> {
2727
Self { buffer }
2828
}
2929

30-
pub fn allocator(&self) -> &Allocator {
30+
pub fn allocator(&self) -> &'ast Allocator {
3131
self.context().allocator()
3232
}
3333

@@ -244,7 +244,7 @@ impl<'buf, 'ast> Formatter<'buf, 'ast> {
244244

245245
pub fn intern_vec(
246246
&mut self,
247-
mut elements: Vec<FormatElement<'ast>>,
247+
mut elements: ArenaVec<'ast, FormatElement<'ast>>,
248248
) -> Option<FormatElement<'ast>> {
249249
match elements.len() {
250250
0 => None,

crates/oxc_formatter/src/ir_transform/sort_imports/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ mod import_unit;
22
mod partitioned_chunk;
33
mod source_line;
44

5+
use std::mem;
6+
7+
use oxc_allocator::{Allocator, Vec as ArenaVec};
8+
59
use crate::{
610
formatter::format_element::{FormatElement, LineMode, document::Document},
711
options,
@@ -26,7 +30,7 @@ impl SortImportsTransform {
2630
// It means that:
2731
// - There is no redundant spaces, no consecutive line breaks, etc...
2832
// - Last element is always `FormatElement::Line(Hard)`.
29-
pub fn transform<'a>(&self, document: &Document<'a>) -> Document<'a> {
33+
pub fn transform<'a>(&self, document: &Document<'a>, allocator: &'a Allocator) -> Document<'a> {
3034
// Early return for empty files
3135
if document.len() == 1 && matches!(document[0], FormatElement::Line(LineMode::Hard)) {
3236
return document.clone();
@@ -136,7 +140,7 @@ impl SortImportsTransform {
136140

137141
// Finally, sort import lines within each chunk.
138142
// After sorting, flatten everything back to `FormatElement`s.
139-
let mut next_elements = Vec::with_capacity(prev_elements.len());
143+
let mut next_elements = ArenaVec::with_capacity_in(prev_elements.len(), allocator);
140144

141145
let mut chunks_iter = chunks.into_iter().enumerate().peekable();
142146
while let Some((idx, chunk)) = chunks_iter.next() {

0 commit comments

Comments
 (0)