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:
leaysgur committed 2026-07-22 06:32:03 +00:00
1 parent 94de05f153
commit a5f7b155f7
11 files changed
+172 -154

No files matched your search

+2 -2
View File
@@ -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,
+3 -2
View File
@@ -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.
+53 -26
View File
@@ -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.
+21 -11
View File
@@ -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);
+4 -4
View File
@@ -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)))
}
}
+2 -2
View File
@@ -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::{
+33 -3
View File
@@ -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