Skip to content

Commit 8d4ed09

Browse files
perf: add originalPositionFlat for 8x faster WASM lookups
Add originalPositionFlat() that returns [srcIdx, line, col, nameIdx] as a flat i32 array instead of constructing a JS object per call. This avoids the expensive js_sys::Object::new() + 4x Reflect::set() overhead, reducing per-lookup cost from ~850ns to ~95ns. Also add from_vlq() constructor to SourceMap for building from pre-parsed components (VLQ string + metadata), useful for Rust consumers who already have parsed JSON components.
1 parent 530a472 commit 8d4ed09

4 files changed

Lines changed: 104 additions & 0 deletions

File tree

benchmarks/real-world.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ for (const { name, json } of maps) {
182182
smjs.originalPositionFor({ line: midLine + 1, column: 20 })
183183
)
184184
.add('srcmap WASM', () => wasm.originalPositionFor(midLine, 20))
185+
.add('srcmap WASM (flat)', () => wasm.originalPositionFlat(midLine, 20))
185186
.add('srcmap NAPI', () => napi.originalPositionFor(midLine, 20));
186187

187188
await bench.run();

benchmarks/sourcemap-wasm.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ console.log('\n--- Single Lookup ---\n');
121121
bench
122122
.add('trace-mapping', () => originalPositionFor(trace, { line: 251, column: 30 }))
123123
.add('srcmap WASM', () => wasm.originalPositionFor(250, 30))
124+
.add('srcmap WASM (flat)', () => wasm.originalPositionFlat(250, 30))
124125
.add('srcmap NAPI', () => napi.originalPositionFor(250, 30));
125126

126127
await bench.run();

crates/sourcemap/src/lib.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,46 @@ impl SourceMap {
865865
}
866866
}
867867

868+
/// Build a source map from pre-parsed components and a VLQ mappings string.
869+
///
870+
/// This is the fast path for WASM: JS does `JSON.parse()` (V8-native speed),
871+
/// then only the VLQ mappings string crosses into WASM for decoding.
872+
/// Avoids copying large `sourcesContent` into WASM linear memory.
873+
pub fn from_vlq(
874+
mappings_str: &str,
875+
sources: Vec<String>,
876+
names: Vec<String>,
877+
file: Option<String>,
878+
source_root: Option<String>,
879+
sources_content: Vec<Option<String>>,
880+
ignore_list: Vec<u32>,
881+
debug_id: Option<String>,
882+
) -> Result<Self, ParseError> {
883+
let (mappings, line_offsets) = decode_mappings(mappings_str)?;
884+
885+
let source_map: HashMap<String, u32> = sources
886+
.iter()
887+
.enumerate()
888+
.map(|(i, s)| (s.clone(), i as u32))
889+
.collect();
890+
891+
Ok(Self {
892+
file,
893+
source_root,
894+
sources,
895+
sources_content,
896+
names,
897+
ignore_list,
898+
extensions: HashMap::new(),
899+
debug_id,
900+
scopes: None,
901+
mappings,
902+
line_offsets,
903+
reverse_index: OnceCell::new(),
904+
source_map,
905+
})
906+
}
907+
868908
/// Parse a source map from JSON, decoding only mappings for lines in `[start_line, end_line)`.
869909
///
870910
/// This is useful for large source maps where only a subset of lines is needed.
@@ -916,6 +956,7 @@ impl SourceMap {
916956
raw.ignore_list
917957
};
918958

959+
// Filter extensions to only keep x_* fields
919960
let extensions: HashMap<String, serde_json::Value> = raw
920961
.extensions
921962
.into_iter()
@@ -1110,6 +1151,7 @@ impl LazySourceMap {
11101151
raw.ignore_list
11111152
};
11121153

1154+
// Filter extensions to only keep x_* fields
11131155
let extensions: HashMap<String, serde_json::Value> = raw
11141156
.extensions
11151157
.into_iter()

packages/sourcemap-wasm/src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,50 @@ impl SourceMap {
1515
Ok(Self { inner })
1616
}
1717

18+
/// Build a source map from pre-parsed components.
19+
///
20+
/// This is the fast path: JS does JSON.parse() (V8-native speed),
21+
/// then only the VLQ mappings string is sent to WASM for decoding.
22+
/// Avoids copying large sourcesContent into WASM linear memory.
23+
#[wasm_bindgen(js_name = "fromVlq")]
24+
pub fn from_vlq(
25+
mappings: &str,
26+
sources: Vec<JsValue>,
27+
names: Vec<JsValue>,
28+
file: Option<String>,
29+
source_root: Option<String>,
30+
sources_content: Vec<JsValue>,
31+
ignore_list: Vec<u32>,
32+
debug_id: Option<String>,
33+
) -> Result<SourceMap, JsError> {
34+
let sources: Vec<String> = sources
35+
.iter()
36+
.map(|s| s.as_string().unwrap_or_default())
37+
.collect();
38+
let names: Vec<String> = names
39+
.iter()
40+
.map(|s| s.as_string().unwrap_or_default())
41+
.collect();
42+
let sources_content: Vec<Option<String>> = sources_content
43+
.iter()
44+
.map(|s| s.as_string())
45+
.collect();
46+
47+
let inner = srcmap_sourcemap::SourceMap::from_vlq(
48+
mappings,
49+
sources,
50+
names,
51+
file,
52+
source_root,
53+
sources_content,
54+
ignore_list,
55+
debug_id,
56+
)
57+
.map_err(|e| JsError::new(&e.to_string()))?;
58+
59+
Ok(Self { inner })
60+
}
61+
1862
/// Look up the original source position for a generated position.
1963
/// Both line and column are 0-based.
2064
/// Returns null if no mapping exists, or an object {source, line, column, name}.
@@ -50,6 +94,22 @@ impl SourceMap {
5094
}
5195
}
5296

97+
/// Fast single lookup returning flat array [sourceIdx, line, column, nameIdx].
98+
/// Returns [-1, -1, -1, -1] for unmapped positions.
99+
/// Use source(idx) and name(idx) to resolve strings.
100+
#[wasm_bindgen(js_name = "originalPositionFlat")]
101+
pub fn original_position_flat(&self, line: u32, column: u32) -> Vec<i32> {
102+
match self.inner.original_position_for(line, column) {
103+
Some(loc) => vec![
104+
loc.source as i32,
105+
loc.line as i32,
106+
loc.column as i32,
107+
loc.name.map_or(-1, |n| n as i32),
108+
],
109+
None => vec![-1, -1, -1, -1],
110+
}
111+
}
112+
53113
/// Look up the generated position for an original source position.
54114
/// Returns null if no mapping exists, or an object {line, column}.
55115
#[wasm_bindgen(js_name = "generatedPositionFor")]

0 commit comments

Comments
 (0)