Skip to content

Commit 1190237

Browse files
committed
perf(sourcemap): avoid allocate in encode if possible
1 parent 40cafb8 commit 1190237

File tree

3 files changed

+99
-26
lines changed

3 files changed

+99
-26
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_sourcemap/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ rustc-hash = { workspace = true }
2323
serde = { workspace = true, features = ["derive"] }
2424
serde_json = { workspace = true }
2525
base64-simd = { workspace = true }
26+
memchr = { workspace = true }
2627
cfg-if = { workspace = true }
2728

2829
rayon = { workspace = true, optional = true }

crates/oxc_sourcemap/src/encode.rs

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use std::borrow::Cow;
2+
#[cfg(feature = "concurrent")]
3+
use std::sync::atomic::{AtomicUsize, Ordering};
24

5+
use memchr::memchr_iter;
36
#[cfg(feature = "concurrent")]
47
use rayon::prelude::*;
58

6-
use crate::error::{Error, Result};
9+
use crate::error::Result;
710
use crate::JSONSourceMap;
811
/// Port from https://github.com/getsentry/rust-sourcemap/blob/master/src/encoder.rs
912
/// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different.
@@ -29,9 +32,10 @@ pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap {
2932
// It will escape the string to avoid invalid JSON string.
3033
pub fn encode_to_string(sourcemap: &SourceMap) -> Result<String> {
3134
let mut contents = PreAllocatedString::new(
32-
10 + sourcemap.names.len() * 2
35+
9 + sourcemap.names.len() * 2
3336
+ sourcemap.sources.len() * 2
34-
+ if let Some(x) = &sourcemap.x_google_ignore_list { x.len() * 2 } else { 0 },
37+
+ if let Some(x) = &sourcemap.x_google_ignore_list { x.len() * 2 } else { 0 }
38+
+ if let Some(s) = &sourcemap.source_contents { s.len() * 3 } else { 0 },
3539
);
3640
contents.push("{\"version\":3,".into());
3741
if let Some(file) = sourcemap.get_file() {
@@ -45,43 +49,77 @@ pub fn encode_to_string(sourcemap: &SourceMap) -> Result<String> {
4549
contents.push("\",".into());
4650
}
4751
contents.push("\"names\":[".into());
48-
for n in &sourcemap.names {
49-
contents.push(serde_json::to_string(n.as_ref())?.into());
50-
contents.push(",".into());
51-
}
5252
if !sourcemap.names.is_empty() {
53+
for n in &sourcemap.names {
54+
if needs_escaping(n) {
55+
contents.push(serde_json::to_string(n.as_ref())?.into());
56+
} else {
57+
contents.push("\"".into());
58+
contents.push((&**n).into());
59+
contents.push("\"".into());
60+
}
61+
contents.push(",".into());
62+
}
5363
// Remove the last `,`.
5464
contents.pop();
5565
}
5666
contents.push(Cow::Borrowed("],\"sources\":["));
57-
for s in &sourcemap.sources {
58-
contents.push(serde_json::to_string(s.as_ref())?.into());
59-
contents.push(",".into());
60-
}
6167
if !sourcemap.sources.is_empty() {
68+
for s in &sourcemap.sources {
69+
if needs_escaping(s) {
70+
contents.push(serde_escape_content(s)?.into());
71+
} else {
72+
contents.push("\"".into());
73+
contents.push((&**s).into());
74+
contents.push("\"".into());
75+
}
76+
contents.push(",".into());
77+
}
6278
// Remove the last `,`.
6379
contents.pop();
6480
}
6581
// Quote `source_content` at parallel.
6682
if let Some(source_contents) = &sourcemap.source_contents {
6783
contents.push("],\"sourcesContent\":[".into());
68-
cfg_if::cfg_if! {
69-
if #[cfg(feature = "concurrent")] {
70-
let quote_source_contents = source_contents
84+
if !source_contents.is_empty() {
85+
#[cfg(feature = "concurrent")]
86+
{
87+
let content_len = AtomicUsize::new(0);
88+
let mut quote_source_contents = source_contents
7189
.par_iter()
72-
.map(|x| serde_json::to_string(x.as_ref()))
73-
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
74-
.map_err(Error::from)?;
75-
} else {
76-
let quote_source_contents = source_contents
77-
.iter()
78-
.map(|x| serde_json::to_string(x.as_ref()))
79-
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
80-
.map_err(Error::from)?;
90+
.filter_map(|s| {
91+
if needs_escaping(s) {
92+
let escaped = serde_escape_content(s).ok()?;
93+
content_len.fetch_add(escaped.len(), Ordering::Relaxed);
94+
Some(["".into(), escaped.into(), "".into(), ",".into()])
95+
} else {
96+
content_len.fetch_add(s.len() + 3, Ordering::Relaxed);
97+
Some(["\"".into(), Cow::Borrowed(&**s), "\"".into(), ",".into()])
98+
}
99+
})
100+
.flatten()
101+
.collect::<Vec<_>>();
102+
// Remove the last `,`.
103+
quote_source_contents.pop();
104+
contents.extend(quote_source_contents, content_len.load(Ordering::Relaxed));
81105
}
82-
};
83-
84-
contents.push(quote_source_contents.join(",").into());
106+
#[cfg(not(feature = "concurrent"))]
107+
{
108+
for s in source_contents {
109+
if needs_escaping(&**s) {
110+
let escaped = serde_escape_content(s)?;
111+
contents.push(escaped.into());
112+
} else {
113+
contents.push("\"".into());
114+
contents.push((&**s).into());
115+
contents.push("\"".into());
116+
}
117+
contents.push(",".into());
118+
}
119+
// Remove the last `,`.
120+
contents.pop();
121+
}
122+
}
85123
}
86124
if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list {
87125
contents.push("],\"x_google_ignoreList\":[".into());
@@ -230,6 +268,16 @@ impl<'a> PreAllocatedString<'a> {
230268
}
231269
}
232270

271+
#[cfg(feature = "concurrent")]
272+
#[inline]
273+
fn extend<I>(&mut self, iter: I, extended: usize)
274+
where
275+
I: IntoIterator<Item = Cow<'a, str>>,
276+
{
277+
self.buf.extend(iter);
278+
self.len += extended;
279+
}
280+
233281
#[inline]
234282
fn consume(self) -> String {
235283
let mut buf = String::with_capacity(self.len);
@@ -238,6 +286,21 @@ impl<'a> PreAllocatedString<'a> {
238286
}
239287
}
240288

289+
#[inline]
290+
fn needs_escaping(s: &str) -> bool {
291+
let bytes = s.as_bytes();
292+
293+
// Check for control characters (0x00-0x1F)
294+
if memchr_iter(b'\x00', bytes).next().is_some() {
295+
return true;
296+
}
297+
298+
// Check for ", \, and /
299+
memchr_iter(b'"', bytes).next().is_some()
300+
|| memchr_iter(b'\\', bytes).next().is_some()
301+
|| memchr_iter(b'/', bytes).next().is_some()
302+
}
303+
241304
#[test]
242305
fn test_encode() {
243306
let input = r#"{
@@ -255,6 +318,14 @@ fn test_encode() {
255318
}
256319
}
257320

321+
#[inline]
322+
fn serde_escape_content<S: AsRef<str>>(s: S) -> Result<String> {
323+
let mut escaped_buf = Vec::with_capacity(s.as_ref().len() + 2);
324+
serde::Serialize::serialize(s.as_ref(), &mut serde_json::Serializer::new(&mut escaped_buf))?;
325+
// Safety: `escaped_buf` is valid utf8.
326+
Ok(unsafe { String::from_utf8_unchecked(escaped_buf) })
327+
}
328+
258329
#[test]
259330
fn test_encode_escape_string() {
260331
// '\0' should be escaped.

0 commit comments

Comments
 (0)