mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(formatter): stage assignment-like left hand side on the heap (#24613)
The last section where arena stranding was taking place. --- There was a suggestion to use a dedicated temporary-arena as a temporary data storage; however, as with the previous JSX PR and this PR has become clear that this does not meet the current requirements.
This commit is contained in:
1 parent
94de05f153
commit
a5f7b155f7
11 files changed
+172
-154
No files matched your search
@@ -47,8 +47,8 @@ pub mod buffer {
|
||||
|
||||
pub use oxc_formatter_core::{
|
||||
Argument, Arguments, Buffer, BufferExtensions, Format, FormatElement, FormatOptions,
|
||||
FormatState, Formatted, Formatter, GroupId, HeapVecBuffer, MemoizeFormat, Memoized,
|
||||
ScratchBuffer, SourceText, UniqueGroupIdBuilder, VecBuffer,
|
||||
FormatState, Formatted, Formatter, GroupId, MemoizeFormat, Memoized, ScratchBuffer, SourceText,
|
||||
UniqueGroupIdBuilder, VecBuffer,
|
||||
};
|
||||
|
||||
pub use self::builders::JoinBuilderJsExt;
|
||||
|
||||
@@ -7,13 +7,13 @@ use crate::{
|
||||
ast_nodes::{AstNode, AstNodes},
|
||||
format_args,
|
||||
formatter::{
|
||||
Comments, FormatElement, HeapVecBuffer, JoinBuilderJsExt as _, JsFormatContext,
|
||||
JsFormatter, JsFormatterExt as _, SourceText,
|
||||
Comments, FormatElement, JoinBuilderJsExt as _, JsFormatContext, JsFormatter,
|
||||
JsFormatterExt as _, SourceText,
|
||||
buffer::RemoveSoftLinesBuffer,
|
||||
format_element,
|
||||
prelude::{
|
||||
FormatElements, Tag, empty_line, expand_parent, format_once, format_with, group,
|
||||
soft_block_indent, soft_line_break_or_space, space,
|
||||
FormatElements, best_fitting_variant, empty_line, expand_parent, format_once,
|
||||
format_with, group, soft_block_indent, soft_line_break_or_space, space,
|
||||
},
|
||||
trivia::format_dangling_comments,
|
||||
},
|
||||
@@ -736,16 +736,9 @@ fn write_grouped_arguments<'a>(
|
||||
}
|
||||
|
||||
// First write the most expanded variant because it needs `arguments`.
|
||||
let most_expanded = {
|
||||
let mut buffer = HeapVecBuffer::new(f.state_mut());
|
||||
buffer.write_element(FormatElement::Tag(Tag::StartEntry));
|
||||
|
||||
format_all_elements_broken_out(node, elements.iter().cloned(), true, &mut buffer);
|
||||
|
||||
buffer.write_element(FormatElement::Tag(Tag::EndEntry));
|
||||
|
||||
buffer.take_into_arena_slice()
|
||||
};
|
||||
let most_expanded = best_fitting_variant(f.state_mut(), |buffer| {
|
||||
format_all_elements_broken_out(node, elements.iter().cloned(), true, buffer);
|
||||
});
|
||||
|
||||
// Now reformat the first or last argument if they happen to be a function or arrow function expression.
|
||||
// Function and arrow function expression apply a custom formatting that removes soft line breaks from the parameters,
|
||||
@@ -824,11 +817,7 @@ fn write_grouped_arguments<'a>(
|
||||
}
|
||||
|
||||
// Write the second variant that forces the group of the first/last argument to expand.
|
||||
let middle_variant = {
|
||||
let mut buffer = HeapVecBuffer::new(f.state_mut());
|
||||
|
||||
buffer.write_element(FormatElement::Tag(Tag::StartEntry));
|
||||
|
||||
let middle_variant = best_fitting_variant(f.state_mut(), |buffer| {
|
||||
write!(
|
||||
buffer,
|
||||
[
|
||||
@@ -860,11 +849,7 @@ fn write_grouped_arguments<'a>(
|
||||
")"
|
||||
]
|
||||
);
|
||||
|
||||
buffer.write_element(FormatElement::Tag(Tag::EndEntry));
|
||||
|
||||
buffer.take_into_arena_slice()
|
||||
};
|
||||
});
|
||||
|
||||
// If the grouped content breaks, then we can skip the most_flat variant,
|
||||
// since we already know that it won't be fitting on a single line.
|
||||
@@ -873,10 +858,7 @@ fn write_grouped_arguments<'a>(
|
||||
ArenaVec::from_array_in([middle_variant, most_expanded], f)
|
||||
} else {
|
||||
// Write the most flat variant with the first or last argument grouped.
|
||||
let most_flat = {
|
||||
let mut buffer = HeapVecBuffer::new(f.state_mut());
|
||||
buffer.write_element(FormatElement::Tag(Tag::StartEntry));
|
||||
|
||||
let most_flat = best_fitting_variant(f.state_mut(), |buffer| {
|
||||
write!(
|
||||
buffer,
|
||||
[
|
||||
@@ -895,11 +877,7 @@ fn write_grouped_arguments<'a>(
|
||||
")",
|
||||
]
|
||||
);
|
||||
|
||||
buffer.write_element(FormatElement::Tag(Tag::EndEntry));
|
||||
|
||||
buffer.take_into_arena_slice()
|
||||
};
|
||||
});
|
||||
|
||||
ArenaVec::from_array_in([most_flat, middle_variant, most_expanded], f)
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
ast_nodes::AstNode,
|
||||
format_args,
|
||||
formatter::{
|
||||
Buffer, Comments, FormatElement, FormatState, JsFormatContext, ScratchBuffer,
|
||||
Buffer, Comments, FormatElement, JsFormatContext, ScratchBuffer,
|
||||
prelude::{tag::GroupMode, *},
|
||||
},
|
||||
utils::{
|
||||
@@ -20,41 +20,6 @@ use crate::{
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Heap accumulator [`Buffer`] for the child-list builders.
|
||||
///
|
||||
/// The builders accumulate elements across separate `write()` calls while other content is formatted in between,
|
||||
/// so their buffers can't stage in the arena (every growth would strand the old allocation, see `HeapVecBuffer`)
|
||||
/// nor share the format run's scratch vector (the flat and multiline builders write alternately, breaking its LIFO discipline).
|
||||
///
|
||||
/// Instead each builder checks its own [`ScratchBuffer`] out of the thread-local cache and writes through this adapter;
|
||||
/// the arena receives one exactly-sized copy when the finished result is interned ([`Formatter::intern_elements`]).
|
||||
struct ChildListBuffer<'buf, 'a> {
|
||||
state: &'buf mut FormatState<'a, JsFormatContext<'a>>,
|
||||
elements: &'buf mut Vec<FormatElement<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Buffer<'a, JsFormatContext<'a>> for ChildListBuffer<'_, 'a> {
|
||||
fn write_element(&mut self, element: FormatElement<'a>) {
|
||||
self.elements.push(element);
|
||||
}
|
||||
|
||||
fn elements(&self) -> &[FormatElement<'a>] {
|
||||
self.elements
|
||||
}
|
||||
|
||||
fn state(&self) -> &FormatState<'a, JsFormatContext<'a>> {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn state_mut(&mut self) -> &mut FormatState<'a, JsFormatContext<'a>> {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'a>]) {
|
||||
self.elements.splice(start.., replacement.iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FormatJsxChildList {
|
||||
layout: JsxChildListLayout,
|
||||
@@ -82,8 +47,6 @@ impl FormatJsxChildList {
|
||||
};
|
||||
|
||||
let mut force_multiline = layout.is_multiline();
|
||||
let mut flat = FlatBuilder::new(force_multiline);
|
||||
let mut multiline = MultilineBuilder::new(multiline_layout);
|
||||
|
||||
let mut children = jsx_split_children(children, f.context().comments());
|
||||
|
||||
@@ -99,6 +62,9 @@ impl FormatJsxChildList {
|
||||
});
|
||||
}
|
||||
|
||||
let mut flat = FlatBuilder::new(force_multiline);
|
||||
let mut multiline = MultilineBuilder::new(multiline_layout);
|
||||
|
||||
let mut is_next_child_suppressed = false;
|
||||
let mut last: Option<&JsxChild> = None;
|
||||
let mut children_iter = JsxChildrenIterator::new(children.iter());
|
||||
@@ -588,6 +554,8 @@ enum MultilineLayout {
|
||||
#[derive(Debug)]
|
||||
struct MultilineBuilder<'a> {
|
||||
layout: MultilineLayout,
|
||||
/// Heap accumulator; the flat and multiline builders write alternately,
|
||||
/// so each checks out its own (see [`ScratchBuffer::writer`]).
|
||||
result: ScratchBuffer<'a>,
|
||||
}
|
||||
|
||||
@@ -631,7 +599,7 @@ impl<'a> MultilineBuilder<'a> {
|
||||
separator: Option<&dyn Format<'a, JsFormatContext<'a>>>,
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
) {
|
||||
let mut buffer = ChildListBuffer { state: f.state_mut(), elements: &mut self.result };
|
||||
let mut buffer = self.result.writer(f.state_mut());
|
||||
match self.layout {
|
||||
MultilineLayout::Fill => {
|
||||
// Make sure that the separator and content only ever write a single element
|
||||
@@ -725,13 +693,16 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatMultilineChildren<'a> {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FlatBuilder<'a> {
|
||||
/// Heap accumulator; see [`MultilineBuilder::result`].
|
||||
result: ScratchBuffer<'a>,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl<'a> FlatBuilder<'a> {
|
||||
fn new(disabled: bool) -> Self {
|
||||
Self { result: ScratchBuffer::checkout(), disabled }
|
||||
// A builder born disabled never writes; skip the pool checkout
|
||||
let result = if disabled { ScratchBuffer::empty() } else { ScratchBuffer::checkout() };
|
||||
Self { result, disabled }
|
||||
}
|
||||
|
||||
fn write(
|
||||
@@ -743,7 +714,7 @@ impl<'a> FlatBuilder<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffer = ChildListBuffer { state: f.state_mut(), elements: &mut self.result };
|
||||
let mut buffer = self.result.writer(f.state_mut());
|
||||
write!(buffer, [content]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use oxc_span::GetSpan;
|
||||
use crate::{
|
||||
ast_nodes::{AstNode, AstNodes},
|
||||
formatter::{
|
||||
Buffer, BufferExtensions, Format, JsFormatter, VecBuffer,
|
||||
Buffer, BufferExtensions, Format, JsFormatter, ScratchBuffer,
|
||||
prelude::{FormatElements, format_once, line_suffix_boundary, *},
|
||||
trivia::FormatTrailingComments,
|
||||
},
|
||||
@@ -831,17 +831,18 @@ impl<'a> Format<'a, JsFormatContext<'a>> for AssignmentLike<'a, '_> {
|
||||
// can can be known only when it's formatted (it can incur in some transformation,
|
||||
// like removing some escapes, etc.).
|
||||
//
|
||||
// 1. we crate a temporary buffer
|
||||
// 1. we check a scratch vector out as a temporary heap buffer
|
||||
// (see `AccumulatorBuffer` for why neither the arena nor the shared scratch fits)
|
||||
// 2. we write the left hand side into the buffer and retrieve the `is_left_short` info
|
||||
// which is computed only when we format it
|
||||
// which is computed only when we format it
|
||||
// 3. we compute the layout
|
||||
// 4. we write the left node inside the main buffer based on the layout
|
||||
let mut buffer = VecBuffer::new(f.state_mut());
|
||||
let is_left_short = self.write_left(&mut Formatter::new(&mut buffer));
|
||||
let formatted_left = buffer.into_vec();
|
||||
let mut formatted_left = ScratchBuffer::checkout();
|
||||
let is_left_short =
|
||||
self.write_left(&mut Formatter::new(&mut formatted_left.writer(f.state_mut())));
|
||||
let left_may_break = formatted_left.may_directly_break();
|
||||
|
||||
let left = format_once(|f| f.write_elements(formatted_left));
|
||||
let left = format_once(move |f| f.write_elements(formatted_left.drain(..)));
|
||||
|
||||
// Compare name only if we are in a position of computing it.
|
||||
// If not (for example, left is not an identifier), then let's fallback to false,
|
||||
|
||||
@@ -35,8 +35,9 @@ Pick by what you're building:
|
||||
- it moves into the `Document` for free, and heap-staging it costs an extra copy for no benefit
|
||||
- Unknown-length staging that ends interned/sliced: `HeapVecBuffer`
|
||||
- a watermarked view over a shared, thread-cached scratch vector; the arena receives one exactly-sized copy (see its rustdoc for the full rationale)
|
||||
- Accumulating across interleaved `write()` calls (multiple builders open at once): check a `ScratchBuffer` out per builder behind a private `Buffer` adapter, finish via `Formatter::intern_elements`
|
||||
- the shared scratch's LIFO rule rules out `HeapVecBuffer` there (see `ChildListBuffer` in `oxc_formatter`)
|
||||
- Accumulating across interleaved `write()` calls (multiple builders open at once), or staging that must release the state between writes and consumption: check a `ScratchBuffer` out per accumulator and write through `ScratchBuffer::writer`, finish via `Formatter::intern_elements` (or re-emit into the destination buffer)
|
||||
- the shared scratch's LIFO rule and its exclusive state borrow rule out `HeapVecBuffer` there (see the JSX child-list builders and `AssignmentLike` in `oxc_formatter`)
|
||||
- `BestFitting` variants: `best_fitting_variant` (wraps the entry tags and stages on the heap in one place)
|
||||
- Known-length sequences: build exact-sized directly (e.g. `ArenaVec::from_iter_in`)
|
||||
|
||||
`Formatter::intern` and `BestFitting` already stage on the heap; consumer crates get this for free.
|
||||
|
||||
@@ -46,7 +46,7 @@ pub trait Buffer<'ast, C> {
|
||||
///
|
||||
/// Used by streaming IR transforms (currently `SortImportsTransform`)
|
||||
/// to splice a reordered chunk back into the buffer.
|
||||
/// Only the vector-backed buffers (`VecBuffer`, `HeapVecBuffer`) support this;
|
||||
/// Only the vector-backed buffers (`VecBuffer`, `HeapVecBuffer`, `AccumulatorBuffer`) support this;
|
||||
/// the wrapper buffers (`PreambleBuffer`, `Inspect`, `RemoveSoftLinesBuffer`) are only ever active
|
||||
/// inside inner-expression contexts, never on the call stack while a streaming chunk is being flushed,
|
||||
/// so they implement this as `unreachable!()`.
|
||||
@@ -189,10 +189,8 @@ impl<'buf, 'ast, C> HeapVecBuffer<'buf, 'ast, C> {
|
||||
pub fn take_into_arena_vec(&mut self) -> ArenaVec<'ast, FormatElement<'ast>> {
|
||||
let allocator = self.state.allocator();
|
||||
let watermark = self.watermark;
|
||||
let scratch = self.state.scratch_mut();
|
||||
let vec = move_elements_into_arena(allocator, &scratch[watermark..]);
|
||||
scratch.truncate(watermark);
|
||||
vec
|
||||
// `Drain`'s exact size hint makes `from_iter_in` allocate exactly-sized
|
||||
ArenaVec::from_iter_in(self.state.scratch_mut().drain(watermark..), &allocator)
|
||||
}
|
||||
|
||||
/// Moves the buffered elements into an exactly-sized arena slice, leaving the buffer empty.
|
||||
@@ -209,27 +207,6 @@ impl<C> Drop for HeapVecBuffer<'_, '_, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bitwise-moves `elements` into an exactly-sized arena vector.
|
||||
///
|
||||
/// The caller must treat the source elements as moved-out and forget them
|
||||
/// (truncate the vector, or let it drop, `FormatElement` has no drop glue).
|
||||
pub(crate) fn move_elements_into_arena<'ast>(
|
||||
allocator: &'ast Allocator,
|
||||
elements: &[FormatElement<'ast>],
|
||||
) -> ArenaVec<'ast, FormatElement<'ast>> {
|
||||
let len = elements.len();
|
||||
let mut vec = ArenaVec::with_capacity_in(len, &allocator);
|
||||
// SAFETY: `vec` has capacity for `len` elements,
|
||||
// and the source (heap) and destination (arena) buffers cannot overlap.
|
||||
// `FormatElement` is not `Drop` (`ArenaVec` const-asserts `!needs_drop` on construction),
|
||||
// so the bitwise move duplicates no owning state and the caller forgetting the source elements merely drops the stale copies.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(elements.as_ptr(), vec.as_mut_ptr(), len);
|
||||
vec.set_len(len);
|
||||
}
|
||||
vec
|
||||
}
|
||||
|
||||
impl<'ast, C> Deref for HeapVecBuffer<'_, 'ast, C> {
|
||||
type Target = [FormatElement<'ast>];
|
||||
|
||||
@@ -261,6 +238,56 @@ impl<'ast, C> Buffer<'ast, C> for HeapVecBuffer<'_, 'ast, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Heap accumulator [`Buffer`] over a caller-owned element vector.
|
||||
///
|
||||
/// For element sequences that outlive a single exclusive borrow of the [`FormatState`]:
|
||||
/// builders that accumulate across separate `write()` calls while other content is formatted in between
|
||||
/// (multiple builders open at once, see the JSX child-list builders in `oxc_formatter`),
|
||||
/// or staging that must release the state between being written and being consumed
|
||||
/// (the assignment-like left hand side, whose layout is computed with the formatter in between).
|
||||
/// Those can't stage in the arena (every growth would strand the grown-out-of allocation, see [`HeapVecBuffer`])
|
||||
/// nor share the format run's scratch vector (suspended or interleaved use breaks its LIFO discipline).
|
||||
///
|
||||
/// Instead the caller checks its own [`crate::ScratchBuffer`] out of the thread-local cache
|
||||
/// and writes through this adapter (constructed via [`crate::ScratchBuffer::writer`]);
|
||||
/// the arena receives one exactly-sized copy when the finished result is interned ([`crate::Formatter::intern_elements`]),
|
||||
/// or nothing at all when the elements are re-emitted into a downstream buffer.
|
||||
pub struct AccumulatorBuffer<'buf, 'ast, C> {
|
||||
state: &'buf mut FormatState<'ast, C>,
|
||||
elements: &'buf mut Vec<FormatElement<'ast>>,
|
||||
}
|
||||
|
||||
impl<'buf, 'ast, C> AccumulatorBuffer<'buf, 'ast, C> {
|
||||
pub(crate) fn new(
|
||||
state: &'buf mut FormatState<'ast, C>,
|
||||
elements: &'buf mut Vec<FormatElement<'ast>>,
|
||||
) -> Self {
|
||||
Self { state, elements }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast, C> Buffer<'ast, C> for AccumulatorBuffer<'_, 'ast, C> {
|
||||
fn write_element(&mut self, element: FormatElement<'ast>) {
|
||||
self.elements.push(element);
|
||||
}
|
||||
|
||||
fn elements(&self) -> &[FormatElement<'ast>] {
|
||||
self.elements
|
||||
}
|
||||
|
||||
fn state(&self) -> &FormatState<'ast, C> {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn state_mut(&mut self) -> &mut FormatState<'ast, C> {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn replace_end(&mut self, start: usize, replacement: &[FormatElement<'ast>]) {
|
||||
self.elements.splice(start.., replacement.iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct wraps an existing buffer and emits a preamble text when the first text is written.
|
||||
///
|
||||
/// This can be useful if you, for example, want to write some content if what gets written next isn't empty.
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::num::NonZeroU8;
|
||||
use oxc_allocator::ArenaVec;
|
||||
|
||||
use crate::{
|
||||
Argument, Arguments, Buffer, Format, FormatContext, FormatElement, FormatOptions, Formatter,
|
||||
GroupId, HeapVecBuffer,
|
||||
Argument, Arguments, Buffer, Format, FormatContext, FormatElement, FormatOptions, FormatState,
|
||||
Formatter, GroupId, HeapVecBuffer,
|
||||
format::write,
|
||||
format_element::{
|
||||
self, LineMode, PrintMode, TextWidth,
|
||||
@@ -881,23 +881,33 @@ impl<'fmt, 'ast, C> BestFitting<'fmt, 'ast, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stages one [`BestFitting`] variant and moves it into the arena exactly-sized.
|
||||
///
|
||||
/// This is the single place encoding the variant shape the printer's [`format_element::BestFittingElement`] relies on:
|
||||
/// an entry-tag-wrapped, exactly-sized arena slice.
|
||||
/// The content is staged on the heap so only that final slice lands in the arena (see [`HeapVecBuffer`]).
|
||||
pub fn best_fitting_variant<'ast, C>(
|
||||
state: &mut FormatState<'ast, C>,
|
||||
content: impl FnOnce(&mut HeapVecBuffer<'_, 'ast, C>),
|
||||
) -> &'ast [FormatElement<'ast>] {
|
||||
let mut buffer = HeapVecBuffer::new(state);
|
||||
buffer.write_element(FormatElement::Tag(StartEntry));
|
||||
content(&mut buffer);
|
||||
buffer.write_element(FormatElement::Tag(EndEntry));
|
||||
buffer.take_into_arena_slice()
|
||||
}
|
||||
|
||||
impl<'ast, C> Format<'ast, C> for BestFitting<'_, 'ast, C> {
|
||||
fn fmt(&self, f: &mut Formatter<'_, 'ast, C>) {
|
||||
// Stage each variant on the heap,
|
||||
// so only the exactly-sized variant slices land in the arena (see `HeapVecBuffer`).
|
||||
let mut buffer = HeapVecBuffer::new(f.state_mut());
|
||||
let variants = self.variants.items();
|
||||
|
||||
let mut formatted_variants = Vec::with_capacity(variants.len());
|
||||
|
||||
for variant in variants {
|
||||
buffer.write_element(FormatElement::Tag(StartEntry));
|
||||
buffer.write_fmt(Arguments::from(variant));
|
||||
buffer.write_element(FormatElement::Tag(EndEntry));
|
||||
|
||||
formatted_variants.push(buffer.take_into_arena_slice());
|
||||
formatted_variants.push(best_fitting_variant(f.state_mut(), |buffer| {
|
||||
buffer.write_fmt(Arguments::from(variant));
|
||||
}));
|
||||
}
|
||||
drop(buffer);
|
||||
|
||||
let formatted_variants = ArenaVec::from_iter_in(formatted_variants, f);
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use oxc_allocator::{Allocator, GetAllocator};
|
||||
use oxc_allocator::{Allocator, ArenaVec, GetAllocator};
|
||||
|
||||
use crate::{
|
||||
Argument, Arguments, Buffer, FormatContext, FormatElement, FormatState,
|
||||
buffer::{HeapVecBuffer, move_elements_into_arena},
|
||||
buffer::HeapVecBuffer,
|
||||
builders::{FillBuilder, JoinBuilder},
|
||||
format::{Format, write},
|
||||
format_element::Interned,
|
||||
@@ -89,8 +89,8 @@ impl<'buf, 'ast, C> Formatter<'buf, 'ast, C> {
|
||||
// Doesn't get cheaper than calling clone, use the element directly
|
||||
1 => elements.pop(),
|
||||
_ => {
|
||||
let vec = move_elements_into_arena(self.allocator(), elements);
|
||||
elements.clear();
|
||||
// `Drain`'s exact size hint makes `from_iter_in` allocate exactly-sized
|
||||
let vec = ArenaVec::from_iter_in(elements.drain(..), &self.allocator());
|
||||
Some(FormatElement::Interned(Interned::new(vec)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ pub mod test_support;
|
||||
|
||||
pub use arguments::{Argument, Arguments};
|
||||
pub use buffer::{
|
||||
Buffer, BufferExtensions, HeapVecBuffer, Inspect, PreambleBuffer, Recorded, Recording,
|
||||
RemoveSoftLinesBuffer, VecBuffer,
|
||||
AccumulatorBuffer, Buffer, BufferExtensions, HeapVecBuffer, Inspect, PreambleBuffer, Recorded,
|
||||
Recording, RemoveSoftLinesBuffer, VecBuffer,
|
||||
};
|
||||
pub use diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError};
|
||||
pub use embedded::{
|
||||
|
||||
@@ -3,12 +3,25 @@ use std::{cell::RefCell, mem};
|
||||
use oxc_allocator::{Allocator, GetAllocator};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{FormatElement, GroupId, UniqueGroupIdBuilder, format_element::Interned};
|
||||
use crate::{
|
||||
FormatElement, GroupId, UniqueGroupIdBuilder, buffer::AccumulatorBuffer,
|
||||
format_element::Interned,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
/// Cache of heap staging vectors, keeping their high-water capacity alive across format runs on the same thread.
|
||||
/// once a thread is warm, staging performs no heap allocation at all.
|
||||
/// Once a thread is warm, staging performs no heap allocation at all.
|
||||
/// A stack because format runs nest (embedded-language formatting creates a child [`FormatState`] on the same thread).
|
||||
///
|
||||
/// INVARIANT: the blind LIFO pop hands each vector back to the role that grew it
|
||||
/// only because returns and takes pair up in reverse order today:
|
||||
/// a run's shared scratch, the largest vector by far on pathological inputs
|
||||
/// (a bundled file can stage most of its document in one `intern`, tens of MB),
|
||||
/// is returned last on [`FormatState`] drop and taken first by the next [`FormatState::new`],
|
||||
/// while the accumulator-sized vectors cycle among themselves in between.
|
||||
/// Anything that changes the return order (e.g. a second [`ScratchBuffer`] field on [`FormatState`] dropping after `scratch`)
|
||||
/// silently mismatches sizes to roles and re-grows the big vector from nothing every run (only visible in the allocation snapshots).
|
||||
/// If you need another long-lived holder, make [`ScratchBuffer::checkout`] pick by capacity instead of relying on this ordering.
|
||||
static SCRATCH_CACHE: RefCell<Vec<Vec<FormatElement<'static>>>> =
|
||||
const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
@@ -20,7 +33,7 @@ thread_local! {
|
||||
/// - [`FormatState`] holds one per format run, shared by all [`crate::HeapVecBuffer`]s like a stack via watermarks:
|
||||
/// each buffer records the length at creation, pushes its elements, and drains its own tail on completion.
|
||||
/// Sound because IR staging is strictly LIFO (an inner buffer always completes before its enclosing one resumes)
|
||||
/// - Accumulators that outlive a single staging scope (interleaved builders, see `ChildListBuffer` in `oxc_formatter`) check out their own.
|
||||
/// - Accumulators that outlive a single staging scope (interleaved or suspended use, written through [`crate::AccumulatorBuffer`]) check out their own.
|
||||
/// The cache itself is an unordered pool with no LIFO requirement
|
||||
#[derive(Debug)]
|
||||
pub struct ScratchBuffer<'ast>(Vec<FormatElement<'ast>>);
|
||||
@@ -34,6 +47,23 @@ impl<'ast> ScratchBuffer<'ast> {
|
||||
mem::transmute::<Vec<FormatElement<'static>>, Vec<FormatElement<'ast>>>(vec)
|
||||
})
|
||||
}
|
||||
|
||||
/// An unpooled, empty scratch buffer, for accumulators that may never be written (e.g. a builder born disabled):
|
||||
/// costs no thread-local access on creation, nor on drop while still empty.
|
||||
pub const fn empty() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
/// Adapts this scratch vector into a [`Buffer`](crate::Buffer) writing into it.
|
||||
///
|
||||
/// This is the only way to construct an [`AccumulatorBuffer`],
|
||||
/// so an accumulator always writes into a pooled vector guarded by the drain-before-drop assertion below.
|
||||
pub fn writer<'buf, C>(
|
||||
&'buf mut self,
|
||||
state: &'buf mut FormatState<'ast, C>,
|
||||
) -> AccumulatorBuffer<'buf, 'ast, C> {
|
||||
AccumulatorBuffer::new(state, &mut self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> std::ops::Deref for ScratchBuffer<'ast> {
|
||||
|
||||
@@ -5,9 +5,9 @@ checker.ts:
|
||||
sys deallocs: 11012
|
||||
sys alloc bytes: 5918285 # 5.92 MB
|
||||
sys peak growth: 2990058 # 2.99 MB
|
||||
arena allocs: 1079319
|
||||
arena reallocs: 3951
|
||||
arena size: 116637808 # 116.64 MB
|
||||
arena allocs: 1070955
|
||||
arena reallocs: 927
|
||||
arena size: 115790968 # 115.79 MB
|
||||
|
||||
App.tsx:
|
||||
file size: 415342 # 415.34 kB
|
||||
@@ -16,20 +16,20 @@ App.tsx:
|
||||
sys deallocs: 2531
|
||||
sys alloc bytes: 985182 # 985.18 kB
|
||||
sys peak growth: 443550 # 443.55 kB
|
||||
arena allocs: 211998
|
||||
arena reallocs: 1645
|
||||
arena size: 22023480 # 22.02 MB
|
||||
arena allocs: 209344
|
||||
arena reallocs: 364
|
||||
arena size: 21592480 # 21.59 MB
|
||||
|
||||
RadixUIAdoptionSection.jsx:
|
||||
file size: 2520 # 2.52 kB
|
||||
sys allocs: 54
|
||||
sys reallocs: 28
|
||||
sys reallocs: 26
|
||||
sys deallocs: 53
|
||||
sys alloc bytes: 17796 # 17.80 kB
|
||||
sys peak growth: 10536 # 10.54 kB
|
||||
arena allocs: 1155
|
||||
sys alloc bytes: 10116 # 10.12 kB
|
||||
sys peak growth: 2856 # 2.86 kB
|
||||
arena allocs: 1134
|
||||
arena reallocs: 0
|
||||
arena size: 155608 # 155.61 kB
|
||||
arena size: 154768 # 154.77 kB
|
||||
|
||||
pdf.mjs:
|
||||
file size: 567296 # 567.30 kB
|
||||
@@ -38,9 +38,9 @@ pdf.mjs:
|
||||
sys deallocs: 4018
|
||||
sys alloc bytes: 2050032 # 2.05 MB
|
||||
sys peak growth: 1164624 # 1.16 MB
|
||||
arena allocs: 434612
|
||||
arena reallocs: 5696
|
||||
arena size: 36857064 # 36.86 MB
|
||||
arena allocs: 428857
|
||||
arena reallocs: 428
|
||||
arena size: 35654864 # 35.65 MB
|
||||
|
||||
antd.js:
|
||||
file size: 6686316 # 6.69 MB
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 34120
|
||||
sys alloc bytes: 20850396 # 20.85 MB
|
||||
sys peak growth: 13841112 # 13.84 MB
|
||||
arena allocs: 2559844
|
||||
arena reallocs: 14941
|
||||
arena size: 332361480 # 332.36 MB
|
||||
arena allocs: 2503924
|
||||
arena reallocs: 1889
|
||||
arena size: 327824560 # 327.82 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -60,9 +60,9 @@ binder.ts:
|
||||
sys deallocs: 456
|
||||
sys alloc bytes: 291781 # 291.78 kB
|
||||
sys peak growth: 195661 # 195.66 kB
|
||||
arena allocs: 64023
|
||||
arena reallocs: 244
|
||||
arena size: 7370072 # 7.37 MB
|
||||
arena allocs: 63509
|
||||
arena reallocs: 38
|
||||
arena size: 7297672 # 7.30 MB
|
||||
|
||||
kitchen-sink.tsx:
|
||||
file size: 732898 # 732.90 kB
|
||||
@@ -71,7 +71,7 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 9564
|
||||
sys alloc bytes: 3180244 # 3.18 MB
|
||||
sys peak growth: 1499988 # 1.50 MB
|
||||
arena allocs: 567769
|
||||
arena reallocs: 5195
|
||||
arena size: 48002616 # 48.00 MB
|
||||
arena allocs: 560401
|
||||
arena reallocs: 1041
|
||||
arena size: 46408496 # 46.41 MB
|
||||
|
||||
Reference in new issue
Block a user