Skip to content

Commit

Permalink
perf(es/codegen): Remove needless allocations (#9978)
Browse files Browse the repository at this point in the history
**Description:**

We don't need to allocate from `get_quoted_utf16` in most cases.
  • Loading branch information
kdy1 authored Feb 5, 2025
1 parent 83f24af commit 9c89d57
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 21 deletions.
6 changes: 6 additions & 0 deletions .changeset/dull-yaks-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
swc_core: patch
swc_ecma_codegen: patch
---

perf(es/codegen): Remove neelees allocations
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ resolver = "2"
anyhow = "1.0.81"
arbitrary = "1"
arrayvec = "0.7.4"
ascii = "1.1.0"
assert_cmd = "2.0.12"
assert_fs = "1.0.13"
auto_impl = "1.2.0"
Expand Down
1 change: 1 addition & 0 deletions crates/swc_ecma_codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ serde-impl = ["swc_ecma_ast/serde"]
bench = false

[dependencies]
ascii = { workspace = true }
memchr = { workspace = true }
num-bigint = { workspace = true, features = ["serde"] }
once_cell = { workspace = true }
Expand Down
81 changes: 66 additions & 15 deletions crates/swc_ecma_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
#![allow(clippy::nonminimal_bool)]
#![allow(non_local_definitions)]

use std::{borrow::Cow, fmt::Write, io};
use std::{borrow::Cow, fmt::Write, io, str};

use ascii::AsciiChar;
use memchr::memmem::Finder;
use once_cell::sync::Lazy;
use swc_allocator::maybe::vec::Vec;
Expand Down Expand Up @@ -680,15 +681,25 @@ where
}
}

let mut value = get_quoted_utf16(&node.value, self.cfg.ascii_only, target);
let (quote_char, mut value) = get_quoted_utf16(&node.value, self.cfg.ascii_only, target);

if self.cfg.inline_script {
value = replace_close_inline_script(&value)
.replace("\x3c!--", "\\x3c!--")
.replace("--\x3e", "--\\x3e");
value = Cow::Owned(
replace_close_inline_script(&value)
.replace("\x3c!--", "\\x3c!--")
.replace("--\x3e", "--\\x3e"),
);
}

let quote_str = [quote_char.as_byte()];
let quote_str = unsafe {
// Safety: quote_char is valid ascii
str::from_utf8_unchecked(&quote_str)
};

self.wr.write_str(quote_str)?;
self.wr.write_str_lit(DUMMY_SP, &value)?;
self.wr.write_str(quote_str)?;

// srcmap!(node, false);
}
Expand Down Expand Up @@ -4125,7 +4136,45 @@ fn get_ascii_only_ident(sym: &str, may_need_quote: bool, target: EsVersion) -> C
}
}

fn get_quoted_utf16(v: &str, ascii_only: bool, target: EsVersion) -> String {
/// Returns `(quote_char, value)`
fn get_quoted_utf16(v: &str, ascii_only: bool, target: EsVersion) -> (AsciiChar, Cow<str>) {
// Fast path: If the string is ASCII and doesn't need escaping, we can avoid
// allocation
if v.is_ascii() {
let mut needs_escaping = false;
let mut single_quote_count = 0;
let mut double_quote_count = 0;

for &b in v.as_bytes() {
match b {
b'\'' => single_quote_count += 1,
b'"' => double_quote_count += 1,
// Control characters and backslash need escaping
0..=0x1f | b'\\' => {
needs_escaping = true;
break;
}
_ => {}
}
}

if !needs_escaping {
let quote_char = if double_quote_count > single_quote_count {
AsciiChar::Apostrophe
} else {
AsciiChar::Quotation
};

// If there are no quotes to escape, we can return the original string
if (quote_char == AsciiChar::Apostrophe && single_quote_count == 0)
|| (quote_char == AsciiChar::Quotation && double_quote_count == 0)
{
return (quote_char, Cow::Borrowed(v));
}
}
}

// Slow path: Original implementation for strings that need processing
// Count quotes first to determine which quote character to use
let (mut single_quote_count, mut double_quote_count) = (0, 0);
for c in v.chars() {
Expand All @@ -4138,21 +4187,24 @@ fn get_quoted_utf16(v: &str, ascii_only: bool, target: EsVersion) -> String {

// Pre-calculate capacity to avoid reallocations
let quote_char = if double_quote_count > single_quote_count {
'\''
AsciiChar::Apostrophe
} else {
AsciiChar::Quotation
};
let escape_char = if quote_char == AsciiChar::Apostrophe {
AsciiChar::Apostrophe
} else {
'"'
AsciiChar::Quotation
};
let escape_char = if quote_char == '\'' { '\'' } else { '"' };
let escape_count = if quote_char == '\'' {
let escape_count = if quote_char == AsciiChar::Apostrophe {
single_quote_count
} else {
double_quote_count
};

// Add 2 for quotes, and 1 for each escaped quote
let capacity = v.len() + 2 + escape_count;
// Add 1 for each escaped quote
let capacity = v.len() + escape_count;
let mut buf = String::with_capacity(capacity);
buf.push(quote_char);

let mut iter = v.chars().peekable();
while let Some(c) = iter.next() {
Expand Down Expand Up @@ -4299,8 +4351,7 @@ fn get_quoted_utf16(v: &str, ascii_only: bool, target: EsVersion) -> String {
}
}

buf.push(quote_char);
buf
(quote_char, Cow::Owned(buf))
}

fn handle_invalid_unicodes(s: &str) -> Cow<str> {
Expand Down
21 changes: 16 additions & 5 deletions crates/swc_ecma_codegen/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,25 +584,36 @@ CONTENT\r

#[test]
fn test_get_quoted_utf16() {
fn combine((quote_char, s): (AsciiChar, Cow<str>)) -> String {
let mut new = String::with_capacity(s.len() + 2);
new.push(quote_char.as_char());
new.push_str(s.as_ref());
new.push(quote_char.as_char());
new
}

#[track_caller]
fn es2020(src: &str, expected: &str) {
assert_eq!(
super::get_quoted_utf16(src, true, EsVersion::Es2020),
combine(super::get_quoted_utf16(src, true, EsVersion::Es2020)),
expected
)
}

#[track_caller]
fn es2020_nonascii(src: &str, expected: &str) {
assert_eq!(
super::get_quoted_utf16(src, true, EsVersion::Es2020),
combine(super::get_quoted_utf16(src, true, EsVersion::Es2020)),
expected
)
}

#[track_caller]
fn es5(src: &str, expected: &str) {
assert_eq!(super::get_quoted_utf16(src, true, EsVersion::Es5), expected)
assert_eq!(
combine(super::get_quoted_utf16(src, true, EsVersion::Es5)),
expected
)
}

es2020("abcde", "\"abcde\"");
Expand Down Expand Up @@ -669,8 +680,8 @@ fn issue_1619_2() {
#[test]
fn issue_1619_3() {
assert_eq!(
get_quoted_utf16("\x00\x31", true, EsVersion::Es3),
"\"\\x001\""
&*get_quoted_utf16("\x00\x31", true, EsVersion::Es3).1,
"\\x001"
);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/swc_ecma_codegen/src/text_writer/basic_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl<'a, W: Write> JsWriter<'a, W> {
Ok(())
}

#[inline]
#[inline(always)]
fn write(&mut self, span: Option<Span>, data: &str) -> Result {
if !data.is_empty() {
if self.line_start {
Expand Down

0 comments on commit 9c89d57

Please sign in to comment.