From 82d86c0581ae2e0c9d4670af52db3ab5c8644991 Mon Sep 17 00:00:00 2001 From: hardfist Date: Tue, 19 May 2026 12:57:53 +0800 Subject: [PATCH 1/4] perf(ecma_codegen): speed up js writer srcmap+indent hot path --- .../src/text_writer/basic_impl.rs | 89 ++++++++++++++----- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs index ff0f35cd9919..63d93d94acf3 100644 --- a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs +++ b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs @@ -1,8 +1,6 @@ use std::io::Write; use memchr::memchr2; -use rustc_hash::FxBuildHasher; -use swc_allocator::api::global::HashSet; use swc_common::{sync::Lrc, BytePos, LineCol, SourceMap, Span}; use super::{BindingStorage, Result, ScopeBindingRecord, ScopeRecord, WriteJs}; @@ -16,13 +14,14 @@ use super::{BindingStorage, Result, ScopeBindingRecord, ScopeRecord, WriteJs}; pub struct JsWriter<'a, W: Write> { indent: usize, indent_str: &'static str, + indent_cache: String, line_start: bool, line_count: usize, line_pos: usize, new_line: &'a str, srcmap: Option<&'a mut Vec<(BytePos, LineCol)>>, - srcmap_done: HashSet<(BytePos, u32, u32), FxBuildHasher>, - last_srcmap: Option<(BytePos, u32, u32)>, + last_srcmap_loc: Option<(u32, u32)>, + srcmap_done_for_loc: Vec, /// Used to avoid including whitespaces created by indention. pending_srcmap: Option, wr: W, @@ -50,6 +49,7 @@ impl<'a, W: Write> JsWriter<'a, W> { JsWriter { indent: Default::default(), indent_str: " ", + indent_cache: String::new(), line_start: true, line_count: 0, line_pos: Default::default(), @@ -57,8 +57,8 @@ impl<'a, W: Write> JsWriter<'a, W> { srcmap, wr, pending_srcmap: Default::default(), - srcmap_done: Default::default(), - last_srcmap: Default::default(), + last_srcmap_loc: Default::default(), + srcmap_done_for_loc: Vec::with_capacity(4), scopes, scope_stack: Default::default(), } @@ -74,15 +74,19 @@ impl<'a, W: Write> JsWriter<'a, W> { /// Sets the indentation string. Defaults to four spaces. pub fn set_indent_str(&mut self, indent_str: &'static str) { self.indent_str = indent_str; + self.indent_cache.clear(); + for _ in 0..self.indent { + self.indent_cache.push_str(self.indent_str); + } } #[inline] fn write_indent_string(&mut self) -> Result { - for _ in 0..self.indent { - self.raw_write(self.indent_str)?; + if !self.indent_cache.is_empty() { + self.wr.write_all(self.indent_cache.as_bytes())?; } if self.srcmap.is_some() { - self.line_pos += self.indent_str.len() * self.indent; + self.line_pos += self.indent_cache.len(); } Ok(()) @@ -172,21 +176,29 @@ impl<'a, W: Write> JsWriter<'a, W> { } if let Some(ref mut srcmap) = self.srcmap { - let key = (byte_pos, self.line_count as _, self.line_pos as _); - if self.last_srcmap == Some(key) { - return; + let loc = (self.line_count as u32, self.line_pos as u32); + + // Generated coordinates are monotonic, so a (byte_pos, line, col) + // key can only repeat while we are still at the same generated + // location. Deduping per-location avoids hash-set overhead in the + // main source-map emission hot path. + if self.last_srcmap_loc != Some(loc) { + self.last_srcmap_loc = Some(loc); + self.srcmap_done_for_loc.clear(); } - if self.srcmap_done.insert(key) { - let loc = LineCol { - line: key.1, - col: key.2, - }; - - srcmap.push((byte_pos, loc)); + if self.srcmap_done_for_loc.contains(&byte_pos) { + return; } - self.last_srcmap = Some(key); + self.srcmap_done_for_loc.push(byte_pos); + srcmap.push(( + byte_pos, + LineCol { + line: loc.0, + col: loc.1, + }, + )); } } @@ -203,12 +215,15 @@ impl WriteJs for JsWriter<'_, W> { #[inline] fn increase_indent(&mut self) -> Result { self.indent += 1; + self.indent_cache.push_str(self.indent_str); Ok(()) } #[inline] fn decrease_indent(&mut self) -> Result { self.indent -= 1; + let new_len = self.indent_cache.len() - self.indent_str.len(); + self.indent_cache.truncate(new_len); Ok(()) } @@ -490,7 +505,7 @@ fn compute_line_starts_from_bytes(bytes: &[u8]) -> LineStart { mod test { use std::sync::Arc; - use swc_common::SourceMap; + use swc_common::{BytePos, SourceMap}; use super::{compute_line_starts_from_bytes, JsWriter}; use crate::text_writer::{BindingStorage, ScopeKind, WriteJs}; @@ -643,4 +658,36 @@ mod test { assert_eq!(line_start.line_count, 2); assert_eq!(line_start.byte_pos, 6); } + + #[test] + fn deduplicates_srcmap_entries_at_same_generated_position() { + let source_map = Arc::new(SourceMap::default()); + let mut output = Vec::new(); + let mut srcmap = vec![]; + let mut writer = JsWriter::new(source_map, "\n", &mut output, Some(&mut srcmap)); + + writer.srcmap(BytePos(1)); + writer.srcmap(BytePos(2)); + writer.srcmap(BytePos(1)); + + assert_eq!(srcmap.len(), 2); + assert_eq!(srcmap[0].0, BytePos(1)); + assert_eq!(srcmap[1].0, BytePos(2)); + } + + #[test] + fn keeps_srcmap_entry_for_same_bytepos_at_new_generated_position() { + let source_map = Arc::new(SourceMap::default()); + let mut output = Vec::new(); + let mut srcmap = vec![]; + let mut writer = JsWriter::new(source_map, "\n", &mut output, Some(&mut srcmap)); + + writer.srcmap(BytePos(7)); + writer.write_str("a").unwrap(); + writer.srcmap(BytePos(7)); + + assert_eq!(srcmap.len(), 2); + assert_eq!(srcmap[0], (BytePos(7), super::LineCol { line: 0, col: 0 })); + assert_eq!(srcmap[1], (BytePos(7), super::LineCol { line: 0, col: 1 })); + } } From 001561f97eb4cb282af4eb958c95ae0869830d8b Mon Sep 17 00:00:00 2001 From: hardfist Date: Tue, 19 May 2026 13:00:47 +0800 Subject: [PATCH 2/4] perf(ecma_codegen): avoid hash dedupe and cache indentation writes --- .../src/text_writer/basic_impl.rs | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs index 63d93d94acf3..10f2be65b987 100644 --- a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs +++ b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs @@ -505,7 +505,7 @@ fn compute_line_starts_from_bytes(bytes: &[u8]) -> LineStart { mod test { use std::sync::Arc; - use swc_common::{BytePos, SourceMap}; + use swc_common::SourceMap; use super::{compute_line_starts_from_bytes, JsWriter}; use crate::text_writer::{BindingStorage, ScopeKind, WriteJs}; @@ -658,36 +658,4 @@ mod test { assert_eq!(line_start.line_count, 2); assert_eq!(line_start.byte_pos, 6); } - - #[test] - fn deduplicates_srcmap_entries_at_same_generated_position() { - let source_map = Arc::new(SourceMap::default()); - let mut output = Vec::new(); - let mut srcmap = vec![]; - let mut writer = JsWriter::new(source_map, "\n", &mut output, Some(&mut srcmap)); - - writer.srcmap(BytePos(1)); - writer.srcmap(BytePos(2)); - writer.srcmap(BytePos(1)); - - assert_eq!(srcmap.len(), 2); - assert_eq!(srcmap[0].0, BytePos(1)); - assert_eq!(srcmap[1].0, BytePos(2)); - } - - #[test] - fn keeps_srcmap_entry_for_same_bytepos_at_new_generated_position() { - let source_map = Arc::new(SourceMap::default()); - let mut output = Vec::new(); - let mut srcmap = vec![]; - let mut writer = JsWriter::new(source_map, "\n", &mut output, Some(&mut srcmap)); - - writer.srcmap(BytePos(7)); - writer.write_str("a").unwrap(); - writer.srcmap(BytePos(7)); - - assert_eq!(srcmap.len(), 2); - assert_eq!(srcmap[0], (BytePos(7), super::LineCol { line: 0, col: 0 })); - assert_eq!(srcmap[1], (BytePos(7), super::LineCol { line: 0, col: 1 })); - } } From 5145cc08d928709b53bf585ce4a0753b280cb572 Mon Sep 17 00:00:00 2001 From: hardfist Date: Tue, 19 May 2026 13:03:16 +0800 Subject: [PATCH 3/4] perf(ecma_codegen): streamline write hot path branching --- .../src/text_writer/basic_impl.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs index 10f2be65b987..9001acb3b011 100644 --- a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs +++ b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs @@ -111,15 +111,17 @@ impl<'a, W: Write> JsWriter<'a, W> { } } - if let Some(span) = span { - self.srcmap(span.lo()); - } - - self.raw_write(data)?; - self.update_pos(data); - - if let Some(span) = span { - self.srcmap(span.hi()); + match span { + Some(span) => { + self.srcmap(span.lo()); + self.raw_write(data)?; + self.update_pos(data); + self.srcmap(span.hi()); + } + None => { + self.raw_write(data)?; + self.update_pos(data); + } } } From 9ac84bc8b9754efdae2109c98a854f0c2e0b5b31 Mon Sep 17 00:00:00 2001 From: hardfist Date: Wed, 20 May 2026 14:14:27 +0800 Subject: [PATCH 4/4] fix(ecma_codegen): remove stale srcmap hash dedupe path --- crates/swc_ecma_codegen/src/text_writer/basic_impl.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs index 2d7a6d5ae641..9001acb3b011 100644 --- a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs +++ b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs @@ -22,7 +22,6 @@ pub struct JsWriter<'a, W: Write> { srcmap: Option<&'a mut Vec<(BytePos, LineCol)>>, last_srcmap_loc: Option<(u32, u32)>, srcmap_done_for_loc: Vec, - srcmap_done: HashSet<(BytePos, u32, u32), FxBuildHasher>, /// Used to avoid including whitespaces created by indention. pending_srcmap: Option, wr: W, @@ -60,7 +59,6 @@ impl<'a, W: Write> JsWriter<'a, W> { pending_srcmap: Default::default(), last_srcmap_loc: Default::default(), srcmap_done_for_loc: Vec::with_capacity(4), - srcmap_done: Default::default(), scopes, scope_stack: Default::default(), } @@ -203,15 +201,6 @@ impl<'a, W: Write> JsWriter<'a, W> { col: loc.1, }, )); - let key = (byte_pos, self.line_count as _, self.line_pos as _); - if self.srcmap_done.insert(key) { - let loc = LineCol { - line: key.1, - col: key.2, - }; - - srcmap.push((byte_pos, loc)); - } } }