Skip to content

perf(parser): failed try_parse allocates a throwaway diagnostic on every rewind #21540

@Boshen

Description

@Boshen

Root cause

ParserImpl::try_parse in crates/oxc_parser/src/cursor.rs signals "rewind me" by setting self.fatal_error = Some(...) via self.unexpected() / set_unexpected() (error_handler.rs:38, 81).

Building that signal costs 2–3 global allocations that are immediately dropped when try_parse rewinds:

// error_handler.rs: set_unexpected → set_fatal_error
let error = diagnostics::unexpected_token(self.cur_token().span());

// diagnostics.rs
OxcDiagnostic::error("Unexpected token").with_label(span)

// oxc_diagnostics/src/lib.rs: OxcDiagnostic::error
Self { inner: Box::new(OxcDiagnosticInner {}) }   // 1 sys alloc
// .with_label(span) pushes to a Vec<LabeledSpan>    // +1-2 sys allocs

On rewind the FatalError is restored to the checkpoint's value (usually None), so the Box<OxcDiagnosticInner> and the labels Vec are dropped. The entire heap allocation exists only as a "rewind me" flag.

Evidence

#21532 converted 5 try_parse sites where the discriminator is a cheap peek, eliminating the failure path altogether. Allocation snapshot (tasks/track_memory_allocations/allocs_parser.snap):

File Sys allocs (before → after) Δ
checker.ts 9672 → 7080 −2592
binder.ts 530 → 394 −136
cal.com.tsx 1083 → 833 −250
antd.js 7132 → 7102 −30
pdf.mjs 703 → 695 −8

TS-heavy files dominate the win because the paths that took the failure branch (infer T extends …, primitive-keyword-vs-reference, x is Foo predicate, soft-keyword modifiers, constructor string key) only fire on TypeScript input.

Suggested fix

Make the throwaway-diagnostic path lazy. set_unexpected could set a cheap sentinel (e.g. self.fatal_error = Some(FatalError::Sentinel)) and only materialize the OxcDiagnostic when fatal_error survives to the top-level parser exit — i.e. when it will actually be reported. That removes the heap cost from every try_parse failure, including the cases where try_parse is genuinely the right tool and can't be converted to lookahead.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions