Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/perf-es-parser-hot-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
swc_common: patch
swc_core: patch
swc_ecma_parser: patch
---

perf(es/parser): Optimize parser hot paths
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ swc-core-*.tgz
# samply
profile.json
profile.json.gz

# Local micro-optimization profiler output
optimization-artifacts/
38 changes: 36 additions & 2 deletions crates/swc_common/src/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ use crate::{
/// We use [Option] instead of no-op Comments implementation to avoid allocation
/// unless required.
pub trait Comments {
/// Reserve storage for comments that will be inserted soon.
///
/// Implementations may use this to avoid repeated map growth when a parser
/// has already buffered comments before publishing them.
fn reserve_comments(&self, _leading: usize, _trailing: usize) {}

fn add_leading(&self, pos: BytePos, cmt: Comment);
fn add_leading_comments(&self, pos: BytePos, comments: Vec<Comment>);
fn has_leading(&self, pos: BytePos) -> bool;
Expand Down Expand Up @@ -129,6 +135,10 @@ pub trait Comments {

macro_rules! delegate {
() => {
fn reserve_comments(&self, leading: usize, trailing: usize) {
(**self).reserve_comments(leading, trailing)
}

fn add_leading(&self, pos: BytePos, cmt: Comment) {
(**self).add_leading(pos, cmt)
}
Expand Down Expand Up @@ -282,6 +292,12 @@ impl<C> Comments for Option<C>
where
C: Comments,
{
fn reserve_comments(&self, leading: usize, trailing: usize) {
if let Some(c) = self {
c.reserve_comments(leading, trailing)
}
}

fn add_leading(&self, pos: BytePos, cmt: Comment) {
if let Some(c) = self {
c.add_leading(pos, cmt)
Expand Down Expand Up @@ -419,8 +435,22 @@ pub struct SingleThreadedComments {
}

impl Comments for SingleThreadedComments {
fn reserve_comments(&self, leading: usize, trailing: usize) {
if leading != 0 {
self.leading.borrow_mut().reserve(leading);
}

if trailing != 0 {
self.trailing.borrow_mut().reserve(trailing);
}
}

fn add_leading(&self, pos: BytePos, cmt: Comment) {
self.leading.borrow_mut().entry(pos).or_default().push(cmt);
self.leading
.borrow_mut()
.entry(pos)
.or_insert_with(|| Vec::with_capacity(1))
.push(cmt);
}

fn add_leading_comments(&self, pos: BytePos, comments: Vec<Comment>) {
Expand Down Expand Up @@ -460,7 +490,11 @@ impl Comments for SingleThreadedComments {
}

fn add_trailing(&self, pos: BytePos, cmt: Comment) {
self.trailing.borrow_mut().entry(pos).or_default().push(cmt);
self.trailing
.borrow_mut()
.entry(pos)
.or_insert_with(|| Vec::with_capacity(1))
.push(cmt);
}

fn add_trailing_comments(&self, pos: BytePos, comments: Vec<Comment>) {
Expand Down
28 changes: 14 additions & 14 deletions crates/swc_common/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,29 @@ impl<'a> From<&'a SourceFile> for StringInput<'a> {
}

impl<'a> Input<'a> for StringInput<'a> {
#[inline]
#[inline(always)]
fn cur(&self) -> Option<u8> {
self.remaining.as_bytes().first().copied()
}

#[inline]
#[inline(always)]
fn peek(&self) -> Option<u8> {
self.remaining.as_bytes().get(1).copied()
}

#[inline]
#[inline(always)]
fn peek_ahead(&self) -> Option<u8> {
self.remaining.as_bytes().get(2).copied()
}

#[inline]
#[inline(always)]
unsafe fn bump_bytes(&mut self, n: usize) {
debug_assert!(n <= self.remaining.len());
self.remaining = unsafe { self.remaining.get_unchecked(n..) };
self.last_pos.0 += n as u32;
}

#[inline]
#[inline(always)]
fn cur_as_ascii(&self) -> Option<u8> {
let first_byte = *self.remaining.as_bytes().first()?;
if first_byte <= 0x7f {
Expand All @@ -121,28 +121,28 @@ impl<'a> Input<'a> for StringInput<'a> {
}
}

#[inline]
#[inline(always)]
fn cur_as_char(&self) -> Option<char> {
self.remaining.chars().next()
}

#[inline]
#[inline(always)]
fn is_at_start(&self) -> bool {
self.orig_start == self.last_pos
}

/// TODO(kdy1): Remove this?
#[inline]
#[inline(always)]
fn cur_pos(&self) -> BytePos {
self.last_pos
}

#[inline]
#[inline(always)]
fn last_pos(&self) -> BytePos {
self.last_pos
}

#[inline]
#[inline(always)]
unsafe fn slice(&mut self, start: BytePos, end: BytePos) -> &'a str {
debug_assert!(start <= end, "Cannot slice {start:?}..{end:?}");
let s = self.orig;
Expand Down Expand Up @@ -186,7 +186,7 @@ impl<'a> Input<'a> for StringInput<'a> {
ret
}

#[inline]
#[inline(always)]
unsafe fn reset_to(&mut self, to: BytePos) {
if self.last_pos == to {
// No need to reset.
Expand All @@ -201,7 +201,7 @@ impl<'a> Input<'a> for StringInput<'a> {
self.last_pos = to;
}

#[inline]
#[inline(always)]
fn is_byte(&self, c: u8) -> bool {
self.remaining
.as_bytes()
Expand All @@ -210,12 +210,12 @@ impl<'a> Input<'a> for StringInput<'a> {
.unwrap_or(false)
}

#[inline]
#[inline(always)]
fn is_str(&self, s: &str) -> bool {
self.remaining.starts_with(s)
}

#[inline]
#[inline(always)]
unsafe fn eat_byte(&mut self, c: u8) -> bool {
if self.is_byte(c) {
self.remaining = unsafe { self.remaining.get_unchecked(1..) };
Expand Down
43 changes: 35 additions & 8 deletions crates/swc_ecma_parser/src/lexer/comments_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,49 @@ pub enum BufferedCommentKind {
pub struct CommentsBuffer {
comments: Vec<BufferedComment>,
pending_leading: Vec<Comment>,
leading_count: usize,
trailing_count: usize,
}

#[derive(Debug, Default)]
pub struct CommentsBufferCheckpoint {
comments_pos: usize,
pending_leading: usize,
leading_count: usize,
trailing_count: usize,
}

impl CommentsBuffer {
pub fn new() -> Self {
Self::default()
pub fn with_capacity(comments: usize) -> Self {
Self {
comments: Vec::with_capacity(comments),
pending_leading: Vec::with_capacity(4),
leading_count: 0,
trailing_count: 0,
}
}

pub fn checkpoint_save(&self) -> CommentsBufferCheckpoint {
CommentsBufferCheckpoint {
comments_pos: self.comments.len(),
pending_leading: self.pending_leading.len(),
leading_count: self.leading_count,
trailing_count: self.trailing_count,
}
}

pub fn checkpoint_load(&mut self, checkpoint: CommentsBufferCheckpoint) {
self.comments.truncate(checkpoint.comments_pos);
self.pending_leading.truncate(checkpoint.pending_leading);
self.leading_count = checkpoint.leading_count;
self.trailing_count = checkpoint.trailing_count;
}
}

impl CommentsBuffer {
#[inline(always)]
pub fn push_comment(&mut self, comment: BufferedComment) {
self.increment_count(comment.kind, 1);
self.comments.push(comment);
}

Expand All @@ -54,11 +68,6 @@ impl CommentsBuffer {
self.pending_leading.push(comment);
}

#[inline(always)]
pub fn has_pending(&self) -> bool {
!self.pending_leading.is_empty()
}

#[inline(always)]
pub fn pending_to_comment(&mut self, kind: BufferedCommentKind, pos: BytePos) {
// Most tokens have no pending comments; avoid creating an empty drain on
Expand All @@ -68,10 +77,13 @@ impl CommentsBuffer {
1 => {
let comment = self.pending_leading.pop().unwrap();
let comment = BufferedComment { kind, pos, comment };
self.increment_count(kind, 1);
self.comments.push(comment);
return;
}
_ => {}
len => {
self.increment_count(kind, len);
}
}

for comment in self.pending_leading.drain(..) {
Expand All @@ -80,8 +92,23 @@ impl CommentsBuffer {
}
}

#[inline(always)]
pub fn comment_counts(&self) -> (usize, usize) {
(self.leading_count, self.trailing_count)
}

#[inline(always)]
pub fn take_comments(&mut self) -> impl Iterator<Item = BufferedComment> + '_ {
self.leading_count = 0;
self.trailing_count = 0;
self.comments.drain(..)
}

#[inline(always)]
fn increment_count(&mut self, kind: BufferedCommentKind, count: usize) {
match kind {
BufferedCommentKind::Leading => self.leading_count += count,
BufferedCommentKind::Trailing => self.trailing_count += count,
}
}
}
12 changes: 11 additions & 1 deletion crates/swc_ecma_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,13 @@ impl<'a> Lexer<'a> {
}
}

let comments_buffer = comments
.is_some()
.then(|| CommentsBuffer::with_capacity(input.as_str().len() / 64));

Lexer {
comments,
comments_buffer: comments.is_some().then(CommentsBuffer::new),
comments_buffer,
ctx: Default::default(),
input,
start_pos,
Expand Down Expand Up @@ -791,6 +795,7 @@ impl<'a> Lexer<'a> {

if is_for_next {
self.comments_buffer_mut().unwrap().push_pending(cmt);
self.state.has_pending_comments = true;
} else {
let pos = self.state.prev_hi;
self.comments_buffer_mut().unwrap().push_comment(BufferedComment {
Expand Down Expand Up @@ -822,6 +827,7 @@ impl<'a> Lexer<'a> {

if is_for_next {
self.comments_buffer_mut().unwrap().push_pending(cmt);
self.state.has_pending_comments = true;
} else {
let pos = self.state.prev_hi;
self.comments_buffer_mut()
Expand Down Expand Up @@ -931,6 +937,7 @@ impl<'a> Lexer<'a> {

if is_for_next {
self.comments_buffer_mut().unwrap().push_pending(cmt);
self.state.has_pending_comments = true;
} else {
let pos = self.state.prev_hi;
self.comments_buffer_mut()
Expand Down Expand Up @@ -1349,6 +1356,8 @@ impl<'a> Lexer<'a> {
comments_buffer.pending_to_comment(kind, last);

// now fill the user's passed in comments
let (leading, trailing) = comments_buffer.comment_counts();
comments.reserve_comments(leading, trailing);
for comment in comments_buffer.take_comments() {
match comment.kind {
BufferedCommentKind::Leading => {
Expand All @@ -1359,6 +1368,7 @@ impl<'a> Lexer<'a> {
}
}
}
self.state.has_pending_comments = false;
}
}

Expand Down
Loading
Loading