Skip to content

Commit 1ab1c17

Browse files
fix: correct lazy lookup optimization
1 parent 10c157f commit 1ab1c17

3 files changed

Lines changed: 156 additions & 70 deletions

File tree

benchmarks/real-world.mjs

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createBench, latencyMeanMs, latencyP99Ms, throughputHz } from "./codspeed.mjs";
22
import { createDeterministicLookups, setFailureExitCode } from "./workload.mjs";
3+
import assert from "node:assert/strict";
34
import { readFileSync, existsSync } from "node:fs";
45
import { dirname, join } from "node:path";
56
import { fileURLToPath } from "node:url";
@@ -27,6 +28,49 @@ const createOrderedLookups = (count, maxLine, descending) =>
2728
};
2829
});
2930

31+
const createLookupPatterns = (maxLine) => {
32+
const midLine = Math.floor(maxLine / 2);
33+
return [
34+
{ name: "ascending", lookups: createOrderedLookups(LOOKUP_COUNT, maxLine, false) },
35+
{ name: "descending", lookups: createOrderedLookups(LOOKUP_COUNT, maxLine, true) },
36+
{
37+
name: "repeated",
38+
lookups: Array.from({ length: LOOKUP_COUNT }, () => ({ line: midLine, column: 20 })),
39+
},
40+
{
41+
name: "randomized",
42+
lookups: createDeterministicLookups(LOOKUP_COUNT, maxLine, LOOKUP_MAX_COLUMN, LOOKUP_SEED),
43+
},
44+
];
45+
};
46+
47+
const assertLazyMatchesEager = (eager, lazy, lookups, context) => {
48+
for (const { line, column } of lookups) {
49+
assert.deepEqual(
50+
lazy.originalPositionFor(line, column),
51+
eager.originalPositionFor(line, column),
52+
`${context} at ${line}:${column}`,
53+
);
54+
}
55+
};
56+
57+
const consumeLookups = (map, lookups) => {
58+
let checksum = 2_166_136_261;
59+
60+
for (const { line, column } of lookups) {
61+
const result = map.originalPositionFor(line, column);
62+
const value =
63+
result === null
64+
? 0
65+
: result.line ^ result.column ^ (result.source?.length ?? 0) ^ (result.name?.length ?? 0);
66+
checksum = Math.imul(checksum ^ value, 16_777_619) >>> 0;
67+
}
68+
69+
return checksum;
70+
};
71+
72+
let lookupChecksum = 0;
73+
3074
// ── Load fixtures ────────────────────────────────────────────────
3175

3276
const FIXTURES = [
@@ -181,39 +225,78 @@ for (const { name, json, size } of maps) {
181225
);
182226
}
183227

184-
// ── Fast-lazy lookup order ───────────────────────────────────────
228+
// ── Fast-lazy cold map lookup order ─────────────────────────────
185229

186-
console.log("\n--- Fast-Lazy Lookup Order ---\n");
230+
console.log("\n--- Fast-Lazy Cold Map: Construct and First Lookup Pass ---\n");
187231

188232
for (const { name, json, size, lines } of maps) {
189233
console.log(`### ${name}\n`);
190234

191-
const midLine = Math.floor(lines / 2);
192-
const patterns = [
193-
{ name: "ascending", lookups: createOrderedLookups(LOOKUP_COUNT, lines, false) },
194-
{ name: "descending", lookups: createOrderedLookups(LOOKUP_COUNT, lines, true) },
195-
{
196-
name: "repeated",
197-
lookups: Array.from({ length: LOOKUP_COUNT }, () => ({ line: midLine, column: 20 })),
198-
},
199-
{
200-
name: "randomized",
201-
lookups: createDeterministicLookups(LOOKUP_COUNT, lines, LOOKUP_MAX_COLUMN, LOOKUP_SEED),
202-
},
203-
].map((pattern) => ({ ...pattern, map: new FastSourceMap(json) }));
235+
const eager = new SourceMap(json);
236+
const patterns = createLookupPatterns(lines);
237+
238+
for (const pattern of patterns) {
239+
const lazy = new FastSourceMap(json);
240+
assertLazyMatchesEager(eager, lazy, pattern.lookups, `${name} ${pattern.name}`);
241+
lazy.free();
242+
}
243+
eager.free();
244+
245+
const isLargeMap = size > 1024 * 1024;
246+
const bench = createBench({
247+
warmupIterations: isLargeMap ? 2 : 5,
248+
iterations: isLargeMap ? 10 : 50,
249+
});
250+
const prefix = `real_world_lazy_lookup_cold_1000x[${name}]`;
251+
252+
for (const pattern of patterns) {
253+
bench.add(`${prefix} ${pattern.name}`, () => {
254+
const lazy = new FastSourceMap(json);
255+
lookupChecksum = (lookupChecksum + consumeLookups(lazy, pattern.lookups)) >>> 0;
256+
lazy.free();
257+
});
258+
}
259+
260+
await bench.run();
261+
262+
console.table(
263+
bench.tasks.map((task) => ({
264+
Name: task.name,
265+
"ops/sec": Math.round(throughputHz(task)).toLocaleString(),
266+
"avg (μs)": (latencyMeanMs(task) * 1000).toFixed(1),
267+
"per lookup (ns)": Math.round(
268+
(latencyMeanMs(task) * 1_000_000) / LOOKUP_COUNT,
269+
).toLocaleString(),
270+
})),
271+
);
272+
}
273+
274+
// ── Fast-lazy warm cache lookup order ────────────────────────────
275+
276+
console.log("\n--- Fast-Lazy Warm Cache: Reuse Decoded Lines ---\n");
277+
278+
for (const { name, json, size, lines } of maps) {
279+
console.log(`### ${name}\n`);
280+
281+
const patterns = createLookupPatterns(lines).map((pattern) => ({
282+
...pattern,
283+
map: new FastSourceMap(json),
284+
}));
285+
286+
for (const pattern of patterns) {
287+
lookupChecksum = (lookupChecksum + consumeLookups(pattern.map, pattern.lookups)) >>> 0;
288+
}
204289

205290
const isLargeMap = size > 1024 * 1024;
206291
const bench = createBench({
207292
warmupIterations: isLargeMap ? 5 : 20,
208293
iterations: isLargeMap ? 50 : 200,
209294
});
210-
const prefix = `real_world_lazy_lookup_1000x[${name}]`;
295+
const prefix = `real_world_lazy_lookup_warm_1000x[${name}]`;
211296

212297
for (const pattern of patterns) {
213298
bench.add(`${prefix} ${pattern.name}`, () => {
214-
for (const { line, column } of pattern.lookups) {
215-
pattern.map.originalPositionFor(line, column);
216-
}
299+
lookupChecksum = (lookupChecksum + consumeLookups(pattern.map, pattern.lookups)) >>> 0;
217300
});
218301
}
219302

@@ -235,6 +318,8 @@ for (const { name, json, size, lines } of maps) {
235318
}
236319
}
237320

321+
console.log(`Lookup checksum: ${lookupChecksum}`);
322+
238323
// ── Single lookup ────────────────────────────────────────────────
239324

240325
console.log("\n--- Single Lookup ---\n");

crates/sourcemap/benches/parse.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,13 @@ fn randomized_lazy_lookup_input() -> LazyLookupInput {
642642
lazy_lookup_input(lookups)
643643
}
644644

645+
fn warm_lazy_lookup_input(input: LazyLookupInput) -> LazyLookupInput {
646+
for &(line, column) in &input.lookups {
647+
black_box(input.sm.original_position_for(black_box(line), black_box(column)));
648+
}
649+
input
650+
}
651+
645652
fn run_lazy_lookups(input: &mut LazyLookupInput) {
646653
for &(line, column) in &input.lookups {
647654
black_box(input.sm.original_position_for(black_box(line), black_box(column)));
@@ -667,28 +674,52 @@ fn bench_lookup(criterion: &mut Criterion) {
667674
fn bench_lazy_lookup(criterion: &mut Criterion) {
668675
bench_with_input(
669676
criterion,
670-
"lazy_lookup_1000x_ascending",
677+
"lazy_lookup_cold_1000x_ascending",
671678
ascending_lazy_lookup_input,
672679
run_lazy_lookups,
673680
);
674681
bench_with_input(
675682
criterion,
676-
"lazy_lookup_1000x_descending",
683+
"lazy_lookup_cold_1000x_descending",
677684
descending_lazy_lookup_input,
678685
run_lazy_lookups,
679686
);
680687
bench_with_input(
681688
criterion,
682-
"lazy_lookup_1000x_repeated",
689+
"lazy_lookup_cold_1000x_repeated",
683690
repeated_lazy_lookup_input,
684691
run_lazy_lookups,
685692
);
686693
bench_with_input(
687694
criterion,
688-
"lazy_lookup_1000x_randomized",
695+
"lazy_lookup_cold_1000x_randomized",
689696
randomized_lazy_lookup_input,
690697
run_lazy_lookups,
691698
);
699+
bench_with_input(
700+
criterion,
701+
"lazy_lookup_warm_1000x_ascending",
702+
|| warm_lazy_lookup_input(ascending_lazy_lookup_input()),
703+
run_lazy_lookups,
704+
);
705+
bench_with_input(
706+
criterion,
707+
"lazy_lookup_warm_1000x_descending",
708+
|| warm_lazy_lookup_input(descending_lazy_lookup_input()),
709+
run_lazy_lookups,
710+
);
711+
bench_with_input(
712+
criterion,
713+
"lazy_lookup_warm_1000x_repeated",
714+
|| warm_lazy_lookup_input(repeated_lazy_lookup_input()),
715+
run_lazy_lookups,
716+
);
717+
bench_with_input(
718+
criterion,
719+
"lazy_lookup_warm_1000x_randomized",
720+
|| warm_lazy_lookup_input(randomized_lazy_lookup_input()),
721+
run_lazy_lookups,
722+
);
692723
}
693724

694725
fn bench_vlq_decode(criterion: &mut Criterion) {

crates/sourcemap/src/lib.rs

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
//! assert_eq!(pos.column, 0);
2424
//! ```
2525
26-
use std::cell::{OnceCell, RefCell};
26+
use std::cell::{Cell, OnceCell, RefCell};
2727
use std::collections::HashMap;
2828
use std::fmt;
2929
use std::io;
@@ -32,9 +32,6 @@ use serde::Deserialize;
3232
use srcmap_codec::{DecodeError, vlq_encode_unsigned};
3333
use srcmap_scopes::{Binding, CallSite, GeneratedRange, OriginalScope, Position, ScopeInfo};
3434

35-
#[cfg(test)]
36-
use std::cell::Cell;
37-
3835
pub mod js_identifiers;
3936
pub mod offset_lookup;
4037
pub mod source_view;
@@ -1990,11 +1987,10 @@ pub struct LazySourceMap {
19901987
/// If true, decode_line must decode sequentially from the start.
19911988
fast_scan: bool,
19921989

1993-
/// Cumulative VLQ states indexed by the next undecoded line.
1994-
state_checkpoints: RefCell<Vec<VlqState>>,
1995-
1996-
#[cfg(test)]
1997-
decode_work: Cell<u32>,
1990+
/// Highest line fully decoded so far (for progressive decode in fast-scan mode).
1991+
/// VLQ state at the end of this line is stored in `decode_state`.
1992+
decode_watermark: Cell<u32>,
1993+
decode_state: Cell<VlqState>,
19981994
}
19991995

20001996
impl LazySourceMap {
@@ -2032,13 +2028,8 @@ impl LazySourceMap {
20322028
decoded_lines: RefCell::new(HashMap::new()),
20332029
source_map,
20342030
fast_scan,
2035-
state_checkpoints: RefCell::new(if fast_scan {
2036-
vec![VlqState::default()]
2037-
} else {
2038-
Vec::new()
2039-
}),
2040-
#[cfg(test)]
2041-
decode_work: Cell::new(0),
2031+
decode_watermark: Cell::new(0),
2032+
decode_state: Cell::new(VlqState::default()),
20422033
}
20432034
}
20442035

@@ -2331,16 +2322,13 @@ impl LazySourceMap {
23312322
}
23322323

23332324
if self.fast_scan {
2334-
let mut checkpoints = self.state_checkpoints.borrow_mut();
2335-
let start = line_idx.min(checkpoints.len() - 1);
2336-
let mut state = checkpoints[start];
2325+
let watermark = self.decode_watermark.get();
2326+
let start = if line >= watermark { watermark } else { 0 };
2327+
let mut state =
2328+
if line >= watermark { self.decode_state.get() } else { VlqState::default() };
23372329

2338-
for current_line_idx in start..=line_idx {
2339-
#[cfg(test)]
2340-
self.decode_work.set(self.decode_work.get() + 1);
2341-
2342-
let current_line = current_line_idx as u32;
2343-
let info = &self.line_info[current_line_idx];
2330+
for current_line in start..=line {
2331+
let info = &self.line_info[current_line as usize];
23442332
if self.decoded_lines.borrow().contains_key(&current_line) {
23452333
// Already cached — just walk VLQ bytes to compute end-state
23462334
let bytes = self.raw_mappings.as_bytes();
@@ -2350,13 +2338,11 @@ impl LazySourceMap {
23502338
state = new_state;
23512339
self.decoded_lines.borrow_mut().insert(current_line, mappings);
23522340
}
2341+
}
23532342

2354-
let next_line_idx = current_line_idx + 1;
2355-
if next_line_idx == checkpoints.len() {
2356-
checkpoints.push(state);
2357-
} else {
2358-
checkpoints[next_line_idx] = state;
2359-
}
2343+
if line + 1 > self.decode_watermark.get() {
2344+
self.decode_watermark.set(line + 1);
2345+
self.decode_state.set(state);
23602346
}
23612347

23622348
return Ok(());
@@ -7302,22 +7288,6 @@ mod tests {
73027288
assert_eq!(loc0.line, 0);
73037289
}
73047290

7305-
#[test]
7306-
fn lazy_sourcemap_backward_cache_miss_resumes_from_checkpoint() {
7307-
let mappings = std::iter::repeat_n("AACA", 64).collect::<Vec<_>>().join(";");
7308-
let json =
7309-
format!(r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"{mappings}"}}"#);
7310-
let sm = LazySourceMap::from_json_fast(&json).unwrap();
7311-
7312-
sm.decode_line(63).unwrap();
7313-
sm.decoded_lines.borrow_mut().remove(&15);
7314-
sm.decode_work.set(0);
7315-
7316-
sm.decode_line(15).unwrap();
7317-
7318-
assert_eq!(sm.decode_work.get(), 1, "must resume at the line-15 checkpoint");
7319-
}
7320-
73217291
#[test]
73227292
fn lazy_sourcemap_fast_scan_matches_prescan_in_descending_and_randomized_order() {
73237293
let mappings = std::iter::repeat_n("AACA,CAAC", 64).collect::<Vec<_>>().join(";");

0 commit comments

Comments
 (0)