Skip to content

Commit 006c7f9

Browse files
perf: inline Segment struct, VLQ fast path, and capacity hints
Replace heap-allocated Vec<i64> segments with inline [i64; 5] struct, add single-char fast path to codec VLQ decoder, and use single-pass byte counting with per-line capacity hints for decode allocation. Codec decode: -23% to -54% depending on data shape. Sourcemap parse: -3% to -5% on large maps.
1 parent 872b423 commit 006c7f9

8 files changed

Lines changed: 231 additions & 65 deletions

File tree

crates/cli/src/main.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,10 +616,14 @@ fn cmd_decode(mappings: Option<String>, compact: bool) -> Result<(), CliError> {
616616

617617
reject_control_chars(&input, "mappings input")?;
618618
let decoded = decode(&input).map_err(|e| CliError::parse(format!("decode error: {e}")))?;
619+
let as_vecs: Vec<Vec<Vec<i64>>> = decoded
620+
.into_iter()
621+
.map(|line| line.into_iter().map(|seg| seg.to_vec()).collect())
622+
.collect();
619623
let json = if compact {
620-
serde_json::to_string(&decoded)
624+
serde_json::to_string(&as_vecs)
621625
} else {
622-
serde_json::to_string_pretty(&decoded)
626+
serde_json::to_string_pretty(&as_vecs)
623627
}
624628
.map_err(|e| CliError::parse(format!("JSON serialization error: {e}")))?;
625629
println!("{json}");
@@ -638,8 +642,16 @@ fn cmd_encode(file: Option<PathBuf>, json: bool) -> Result<(), CliError> {
638642
}
639643
};
640644

641-
let decoded: srcmap_codec::SourceMapMappings =
645+
let raw: Vec<Vec<Vec<i64>>> =
642646
serde_json::from_str(&input).map_err(|e| CliError::parse(format!("invalid JSON: {e}")))?;
647+
let decoded: srcmap_codec::SourceMapMappings = raw
648+
.into_iter()
649+
.map(|line| {
650+
line.into_iter()
651+
.map(srcmap_codec::Segment::from)
652+
.collect()
653+
})
654+
.collect();
643655
let encoded = encode(&decoded);
644656

645657
if json {

crates/codec/benches/vlq.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use criterion::{Criterion, criterion_group, criterion_main};
22
#[cfg(feature = "parallel")]
33
use srcmap_codec::encode_parallel;
4-
use srcmap_codec::{decode, encode};
4+
use srcmap_codec::{Segment, decode, encode};
55
use std::hint::black_box;
66

77
/// Synthetic all-zero mappings (best case: single-char VLQ values).
@@ -50,9 +50,9 @@ fn make_realistic_mappings() -> String {
5050
// ~20% of segments have names
5151
if seg % 5 == 0 {
5252
name += 1;
53-
line.push(vec![gen_col, src, src_line, src_col, name]);
53+
line.push(Segment::five(gen_col, src, src_line, src_col, name));
5454
} else {
55-
line.push(vec![gen_col, src, src_line, src_col]);
55+
line.push(Segment::four(gen_col, src, src_line, src_col));
5656
}
5757
}
5858

@@ -106,9 +106,9 @@ fn make_large_realistic_mappings() -> srcmap_codec::SourceMapMappings {
106106

107107
if seg % 5 == 0 {
108108
name += 1;
109-
line.push(vec![gen_col, src, src_line, src_col, name]);
109+
line.push(Segment::five(gen_col, src, src_line, src_col, name));
110110
} else {
111-
line.push(vec![gen_col, src, src_line, src_col]);
111+
line.push(Segment::four(gen_col, src, src_line, src_col));
112112
}
113113
}
114114
mappings.push(line);
@@ -166,9 +166,9 @@ fn bench_encode(c: &mut Criterion) {
166166
src_col = src_col.max(0);
167167
if seg % 5 == 0 {
168168
name += 1;
169-
line.push(vec![gen_col, src, src_line, src_col, name]);
169+
line.push(Segment::five(gen_col, src, src_line, src_col, name));
170170
} else {
171-
line.push(vec![gen_col, src, src_line, src_col]);
171+
line.push(Segment::four(gen_col, src, src_line, src_col));
172172
}
173173
}
174174
mappings.push(line);

crates/codec/src/decode.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::vlq::vlq_decode;
22
use crate::{DecodeError, Line, Segment, SourceMapMappings};
33

4+
45
/// Decode a VLQ-encoded source map mappings string into structured data.
56
///
67
/// The mappings string uses `;` to separate lines and `,` to separate
@@ -24,8 +25,16 @@ pub fn decode(input: &str) -> Result<SourceMapMappings, DecodeError> {
2425
let bytes = input.as_bytes();
2526
let len = bytes.len();
2627

27-
// Pre-count lines for capacity hint
28-
let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
28+
// Pre-count lines and segments in a single pass for capacity hints
29+
let mut semicolons = 0usize;
30+
let mut commas = 0usize;
31+
for &b in bytes {
32+
semicolons += (b == b';') as usize;
33+
commas += (b == b',') as usize;
34+
}
35+
let line_count = semicolons + 1;
36+
let approx_segments = commas + line_count;
37+
let avg_segments_per_line = approx_segments / line_count;
2938
let mut mappings: SourceMapMappings = Vec::with_capacity(line_count);
3039

3140
// Cumulative state across the entire mappings string
@@ -39,7 +48,7 @@ pub fn decode(input: &str) -> Result<SourceMapMappings, DecodeError> {
3948
loop {
4049
// Generated column resets per line
4150
let mut generated_column: i64 = 0;
42-
let mut line: Line = Vec::new();
51+
let mut line: Line = Vec::with_capacity(avg_segments_per_line);
4352
let mut saw_semicolon = false;
4453

4554
while pos < len {
@@ -81,23 +90,23 @@ pub fn decode(input: &str) -> Result<SourceMapMappings, DecodeError> {
8190
let (delta, consumed) = vlq_decode(bytes, pos)?;
8291
name_index += delta;
8392
pos += consumed;
84-
vec![
93+
Segment::five(
8594
generated_column,
8695
source_index,
8796
original_line,
8897
original_column,
8998
name_index,
90-
]
99+
)
91100
} else {
92-
vec![
101+
Segment::four(
93102
generated_column,
94103
source_index,
95104
original_line,
96105
original_column,
97-
]
106+
)
98107
}
99108
} else {
100-
vec![generated_column]
109+
Segment::one(generated_column)
101110
};
102111

103112
line.push(segment);

crates/codec/src/lib.rs

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,111 @@ pub use vlq::{vlq_decode, vlq_decode_unsigned, vlq_encode, vlq_encode_unsigned};
4747

4848
use std::fmt;
4949

50-
/// A single source map segment.
50+
/// A single source map segment stored inline (no heap allocation).
5151
///
5252
/// Segments have 1, 4, or 5 fields:
5353
/// - 1 field: `[generated_column]`
5454
/// - 4 fields: `[generated_column, source_index, original_line, original_column]`
5555
/// - 5 fields: `[generated_column, source_index, original_line, original_column, name_index]`
56-
pub type Segment = Vec<i64>;
56+
///
57+
/// Implements `Deref<Target=[i64]>` so indexing, `len()`, `is_empty()`, and
58+
/// iteration work identically to `Vec<i64>`.
59+
#[derive(Debug, Clone, Copy)]
60+
pub struct Segment {
61+
data: [i64; 5],
62+
len: u8,
63+
}
64+
65+
impl Segment {
66+
/// Create a 1-field segment (generated column only).
67+
#[inline]
68+
pub fn one(a: i64) -> Self {
69+
Self {
70+
data: [a, 0, 0, 0, 0],
71+
len: 1,
72+
}
73+
}
74+
75+
/// Create a 4-field segment (with source info, no name).
76+
#[inline]
77+
pub fn four(a: i64, b: i64, c: i64, d: i64) -> Self {
78+
Self {
79+
data: [a, b, c, d, 0],
80+
len: 4,
81+
}
82+
}
83+
84+
/// Create a 5-field segment (with source info and name).
85+
#[inline]
86+
pub fn five(a: i64, b: i64, c: i64, d: i64, e: i64) -> Self {
87+
Self {
88+
data: [a, b, c, d, e],
89+
len: 5,
90+
}
91+
}
92+
93+
/// Convert to a `Vec<i64>` (for interop with APIs that expect `Vec`).
94+
pub fn to_vec(&self) -> Vec<i64> {
95+
self.data[..self.len as usize].to_vec()
96+
}
97+
}
98+
99+
impl std::ops::Deref for Segment {
100+
type Target = [i64];
101+
102+
#[inline]
103+
fn deref(&self) -> &[i64] {
104+
&self.data[..self.len as usize]
105+
}
106+
}
107+
108+
impl<'a> IntoIterator for &'a Segment {
109+
type Item = &'a i64;
110+
type IntoIter = std::slice::Iter<'a, i64>;
111+
112+
#[inline]
113+
fn into_iter(self) -> Self::IntoIter {
114+
self.data[..self.len as usize].iter()
115+
}
116+
}
117+
118+
impl PartialEq for Segment {
119+
fn eq(&self, other: &Self) -> bool {
120+
**self == **other
121+
}
122+
}
123+
124+
impl Eq for Segment {}
125+
126+
impl PartialEq<Vec<i64>> for Segment {
127+
fn eq(&self, other: &Vec<i64>) -> bool {
128+
**self == **other
129+
}
130+
}
131+
132+
impl PartialEq<Segment> for Vec<i64> {
133+
fn eq(&self, other: &Segment) -> bool {
134+
**self == **other
135+
}
136+
}
137+
138+
impl From<Vec<i64>> for Segment {
139+
fn from(v: Vec<i64>) -> Self {
140+
let mut data = [0i64; 5];
141+
let len = v.len().min(5);
142+
data[..len].copy_from_slice(&v[..len]);
143+
Self { data, len: len as u8 }
144+
}
145+
}
146+
147+
impl From<&[i64]> for Segment {
148+
fn from(s: &[i64]) -> Self {
149+
let mut data = [0i64; 5];
150+
let len = s.len().min(5);
151+
data[..len].copy_from_slice(&s[..len]);
152+
Self { data, len: len as u8 }
153+
}
154+
}
57155

58156
/// A source map line is a list of segments.
59157
pub type Line = Vec<Segment>;
@@ -124,15 +222,15 @@ mod tests {
124222

125223
#[test]
126224
fn roundtrip_large_values() {
127-
let mappings = vec![vec![vec![1000_i64, 50, 999, 500, 100]]];
225+
let mappings = vec![vec![Segment::five(1000, 50, 999, 500, 100)]];
128226
let encoded = encode(&mappings);
129227
let decoded = decode(&encoded).unwrap();
130228
assert_eq!(decoded, mappings);
131229
}
132230

133231
#[test]
134232
fn roundtrip_negative_deltas() {
135-
let mappings = vec![vec![vec![10_i64, 0, 10, 10], vec![20, 0, 5, 5]]];
233+
let mappings = vec![vec![Segment::four(10, 0, 10, 10), Segment::four(20, 0, 5, 5)]];
136234
let encoded = encode(&mappings);
137235
let decoded = decode(&encoded).unwrap();
138236
assert_eq!(decoded, mappings);
@@ -259,20 +357,22 @@ mod tests {
259357
#[test]
260358
fn encode_empty_segments_no_dangling_comma() {
261359
// Empty segments should be skipped without producing dangling commas
262-
let mappings = vec![vec![vec![], vec![0, 0, 0, 0], vec![], vec![2, 0, 0, 1]]];
360+
let empty = Segment::from(&[] as &[i64]);
361+
let mappings = vec![vec![empty, Segment::four(0, 0, 0, 0), empty, Segment::four(2, 0, 0, 1)]];
263362
let encoded = encode(&mappings);
264363
assert!(
265364
!encoded.contains(",,"),
266365
"should not contain dangling commas"
267366
);
268367
// Should encode as if empty segments don't exist
269-
let expected = encode(&vec![vec![vec![0, 0, 0, 0], vec![2, 0, 0, 1]]]);
368+
let expected = encode(&vec![vec![Segment::four(0, 0, 0, 0), Segment::four(2, 0, 0, 1)]]);
270369
assert_eq!(encoded, expected);
271370
}
272371

273372
#[test]
274373
fn encode_all_empty_segments() {
275-
let mappings = vec![vec![vec![], vec![], vec![]]];
374+
let empty = Segment::from(&[] as &[i64]);
375+
let mappings = vec![vec![empty, empty, empty]];
276376
let encoded = encode(&mappings);
277377
assert_eq!(encoded, "");
278378
}
@@ -288,13 +388,13 @@ mod tests {
288388
for line in 0..lines {
289389
let mut line_segments = Vec::with_capacity(segments_per_line);
290390
for seg in 0..segments_per_line {
291-
line_segments.push(vec![
391+
line_segments.push(Segment::five(
292392
(seg * 10) as i64, // generated column
293393
(seg % 5) as i64, // source index
294394
line as i64, // original line
295395
(seg * 5) as i64, // original column
296396
(seg % 3) as i64, // name index
297-
]);
397+
));
298398
}
299399
mappings.push(line_segments);
300400
}
@@ -328,25 +428,22 @@ mod tests {
328428
let mut line_segments = Vec::new();
329429
for seg in 0..8 {
330430
if seg % 4 == 0 {
331-
// 1-field segment (generated-only)
332-
line_segments.push(vec![(seg * 10) as i64]);
431+
line_segments.push(Segment::one((seg * 10) as i64));
333432
} else if seg % 4 == 3 {
334-
// 5-field segment (with name)
335-
line_segments.push(vec![
433+
line_segments.push(Segment::five(
336434
(seg * 10) as i64,
337435
(seg % 3) as i64,
338436
line as i64,
339437
(seg * 5) as i64,
340438
(seg % 2) as i64,
341-
]);
439+
));
342440
} else {
343-
// 4-field segment
344-
line_segments.push(vec![
441+
line_segments.push(Segment::four(
345442
(seg * 10) as i64,
346443
(seg % 3) as i64,
347444
line as i64,
348445
(seg * 5) as i64,
349-
]);
446+
));
350447
}
351448
}
352449
mappings.push(line_segments);
@@ -411,7 +508,7 @@ mod tests {
411508

412509
#[test]
413510
fn encode_single_segment_one_field() {
414-
let mappings = vec![vec![vec![5_i64]]];
511+
let mappings = vec![vec![Segment::one(5)]];
415512
let encoded = encode(&mappings);
416513
let decoded = decode(&encoded).unwrap();
417514
assert_eq!(decoded, mappings);

0 commit comments

Comments
 (0)