-
Notifications
You must be signed in to change notification settings - Fork 5k
sourcemap: bit-packed InternalSourceMap (~2.4 B/mapping, no decode) #29358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b18fac2
631a655
0d6bbb1
bd62e3f
dc4ad33
371cbc2
5ab24dd
3af7239
960ba47
baf016d
00ecf84
db32176
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| big-module.generated.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Microbench for the InternalSourceMap stack-trace remapping path. Compare | ||
| // against a baseline build by running: | ||
| // | ||
| // bun run build:release bench/sourcemap/internal-sourcemap-bench.ts | ||
| // | ||
| // On first run this generates a ~150k-line .ts module next to this file so the | ||
| // stack frames being remapped live inside a large SavedSourceMap entry. | ||
|
|
||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| const ITER = 10_000; | ||
| const BIG_LINES = 150_000; | ||
|
|
||
| const flag = process.argv[2] ?? "bench"; | ||
| const mb = (n: number) => (n / 1048576).toFixed(2) + " MB"; | ||
|
|
||
| const bigPath = path.join(import.meta.dir, "big-module.generated.ts"); | ||
| if (!fs.existsSync(bigPath)) { | ||
| let src = "let v: number = 0;\n"; | ||
| for (let i = 0; i < BIG_LINES; i++) src += `v = (v + ${i % 97}) | 0;\n`; | ||
| src += "export function go(): string { return new Error('e').stack!; }\n"; | ||
| fs.writeFileSync(bigPath, src); | ||
| } | ||
|
|
||
| const rssBefore = process.memoryUsage().rss; | ||
| const tLoad0 = performance.now(); | ||
| const { go } = require(bigPath) as { go: () => string }; | ||
| const tLoad1 = performance.now(); | ||
| const rssAfterLoad = process.memoryUsage().rss; | ||
|
|
||
| // First .stack: triggers full VLQ decode in the old path. | ||
| const tFirst0 = performance.now(); | ||
| let s = go(); | ||
| const tFirst1 = performance.now(); | ||
| const rssAfterFirst = process.memoryUsage().rss; | ||
|
|
||
| const t0 = performance.now(); | ||
| for (let i = 0; i < ITER; i++) s = go(); | ||
| const t1 = performance.now(); | ||
| const rssAfterLoop = process.memoryUsage().rss; | ||
|
|
||
| console.log(`[${flag}] load big module: ${(tLoad1 - tLoad0).toFixed(2)} ms`); | ||
| console.log(`[${flag}] first new Error().stack: ${(tFirst1 - tFirst0).toFixed(3)} ms`); | ||
| console.log( | ||
| `[${flag}] ${ITER}x new Error().stack: ${(t1 - t0).toFixed(2)} ms (${(((t1 - t0) * 1000) / ITER).toFixed(2)} µs/op)`, | ||
| ); | ||
| console.log( | ||
| `[${flag}] rss before/afterLoad/afterFirst/afterLoop: ${mb(rssBefore)} / ${mb(rssAfterLoad)} / ${mb(rssAfterFirst)} / ${mb(rssAfterLoop)}`, | ||
| ); | ||
| console.log(`[${flag}] rss delta load→firstStack: ${mb(rssAfterFirst - rssAfterLoad)}`); | ||
| void s; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -264,25 +264,22 @@ | |
| defer init_lock.unlock(); | ||
|
|
||
| return switch (this.*) { | ||
| .none => null, | ||
| .parsed => |map| map, | ||
| .serialized => |serialized| { | ||
| var stored = switch (SourceMap.Mapping.parse( | ||
| bun.default_allocator, | ||
| serialized.mappingVLQ(), | ||
| null, | ||
| std.math.maxInt(i32), | ||
| std.math.maxInt(i32), | ||
| .{}, | ||
| )) { | ||
| .success => |x| x, | ||
| .fail => { | ||
| this.* = .none; | ||
| return null; | ||
| }, | ||
| const blob = serialized.mappingBlob(); | ||
| if (blob.len < SourceMap.InternalSourceMap.header_size) { | ||
| this.* = .none; | ||
| return null; | ||
| } | ||
| const ism = SourceMap.InternalSourceMap{ .data = blob.ptr }; | ||
| var stored: SourceMap.ParsedSourceMap = .{ | ||
| .ref_count = .init(), | ||
| .internal = ism, | ||
| .input_line_count = ism.inputLineCount(), | ||
| }; | ||
|
|
||
| const source_files = serialized.sourceFileNames(); | ||
|
Check failure on line 282 in src/StandaloneModuleGraph.zig
|
||
|
claude[bot] marked this conversation as resolved.
Comment on lines
267
to
285
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 In Extended reasoning...What the bug is and how it manifests In const slices = bun.default_allocator.alloc(?[]u8, source_files.len * 2);The first half becomes stored.external_source_names = file_names; // N borrowed pointers
stored.is_standalone_module_graph = true;
parsed.ref(); // never freeThe specific code path that triggers it
if (this.external_source_names.len > 0) {
for (this.external_source_names) |name|
allocator.free(name); // (1) frees borrowed blob pointers
allocator.free(this.external_source_names); // (2) size-N free of a 2N allocation
}This has no if (this.internal) |ism| {
if (\!this.is_standalone_module_graph) ism.deinit(); // ← guarded
}The inconsistency — adding the guard for ISM but not for Why existing code doesn't prevent it The protection is entirely behavioral: What the impact would be If
Step-by-step proof
How to fix it Add the same guard to the if (this.external_source_names.len > 0 and \!this.is_standalone_module_graph) {
for (this.external_source_names) |name|
allocator.free(name);
allocator.free(this.external_source_names);
}This mirrors exactly the pattern already used for |
||
| const slices = bun.handleOom(bun.default_allocator.alloc(?[]u8, source_files.len * 2)); | ||
|
|
||
| const file_names: [][]const u8 = @ptrCast(slices[0..source_files.len]); | ||
|
|
@@ -1358,14 +1355,15 @@ | |
|
|
||
| /// Source map serialization in the bundler is specially designed to be | ||
| /// loaded in memory as is. Source contents are compressed with ZSTD to | ||
| /// reduce the file size, and mappings are stored as uncompressed VLQ. | ||
| /// reduce the file size, and mappings are stored as an InternalSourceMap | ||
| /// blob (varint deltas + sync points) so lookups need no decode pass. | ||
| pub const SerializedSourceMap = struct { | ||
| bytes: []const u8, | ||
|
|
||
| /// Following the header bytes: | ||
| /// - source_files_count number of StringPointer, file names | ||
| /// - source_files_count number of StringPointer, zstd compressed contents | ||
| /// - the mapping data, `map_vlq_length` bytes | ||
| /// - the InternalSourceMap blob, `map_bytes_length` bytes | ||
| /// - all the StringPointer contents | ||
| pub const Header = extern struct { | ||
| source_files_count: u32, | ||
|
|
@@ -1376,7 +1374,7 @@ | |
| return @ptrCast(map.bytes.ptr); | ||
| } | ||
|
|
||
| pub fn mappingVLQ(map: SerializedSourceMap) []const u8 { | ||
| pub fn mappingBlob(map: SerializedSourceMap) []const u8 { | ||
| const head = map.header(); | ||
| const start = @sizeOf(Header) + head.source_files_count * @sizeOf(StringPointer) * 2; | ||
| return map.bytes[start..][0..head.map_bytes_length]; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
@@ -1466,14 +1464,16 @@ | |
| } | ||
|
|
||
| const map_vlq: []const u8 = mappings_str.data.e_string.slice(arena); | ||
| const map_blob = SourceMap.InternalSourceMap.fromVLQ(arena, map_vlq, 0) catch | ||
| return error.InvalidSourceMap; | ||
|
claude[bot] marked this conversation as resolved.
|
||
|
|
||
| try out.writeInt(u32, sources_paths.items.len, .little); | ||
| try out.writeInt(u32, @intCast(map_vlq.len), .little); | ||
| try out.writeInt(u32, @intCast(map_blob.len), .little); | ||
|
|
||
| const string_payload_start_location = @sizeOf(u32) + | ||
| @sizeOf(u32) + | ||
| @sizeOf(bun.StringPointer) * sources_content.items.len * 2 + // path + source | ||
| map_vlq.len; | ||
| map_blob.len; | ||
|
|
||
| for (sources_paths.items.slice()) |item| { | ||
| if (item.data != .e_string) | ||
|
|
@@ -1519,7 +1519,7 @@ | |
| try out.writeInt(u32, slice.length, .little); | ||
| } | ||
|
|
||
| try out.writeAll(map_vlq); | ||
| try out.writeAll(map_blob); | ||
|
|
||
| bun.assert(header_list.items.len == string_payload_start_location); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.