diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index f26b41a53d..5bf23748ab 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -135,6 +135,25 @@ pub const SavedSourceMap = struct { map.mutex.unlock(); } + pub const CompactMappings = struct { + compact: bun.sourcemap.CompactSourceMap, + + pub usingnamespace bun.New(@This()); + + pub fn deinit(this: *CompactMappings) void { + this.compact.deinit(); + this.destroy(); + } + + pub fn toMapping(this: *CompactMappings, allocator: Allocator, _: string) anyerror!ParsedSourceMap { + const compact = this.compact; + this.compact = .{ + .allocator = allocator, + }; + return ParsedSourceMap{ .compact_mapping = compact }; + } + }; + // For the runtime, we store the number of mappings and how many bytes the final list is at the beginning of the array // The first 8 bytes are the length of the array // The second 8 bytes are the number of mappings @@ -195,6 +214,7 @@ pub const SavedSourceMap = struct { pub const Value = TaggedPointerUnion(.{ ParsedSourceMap, SavedMappings, + CompactMappings, SourceProviderMap, }); @@ -242,14 +262,10 @@ pub const SavedSourceMap = struct { pub fn onSourceMapChunk(this: *SavedSourceMap, chunk: SourceMap.Chunk, source: logger.Source) anyerror!void { // If we have compact sourcemap data, we need to handle it specially - if (chunk.compact_data != null) { - // For now, just convert the compact data to a regular buffer - const allocator = bun.default_allocator; - var temp_buffer = bun.MutableString.initEmpty(allocator); - try chunk.compact_data.?.writeVLQs(&temp_buffer); - try this.putMappings(source, temp_buffer); + if (chunk.compact_data) |compact| { + try this.putValue(source.path.text, Value.init(CompactMappings.new(.{ .compact = compact }))); } else { - // Standard VLQ format + // Standard VLQ format - pass through directly try this.putMappings(source, chunk.buffer); } } @@ -271,6 +287,8 @@ pub const SavedSourceMap = struct { saved.deinit(); } else if (value.get(SourceProviderMap)) |provider| { _ = provider; // do nothing, we did not hold a ref to ZigSourceProvider + } else if (value.get(CompactMappings)) |compact| { + compact.deinit(); } } } @@ -280,6 +298,7 @@ pub const SavedSourceMap = struct { } pub fn putMappings(this: *SavedSourceMap, source: logger.Source, mappings: MutableString) !void { + try this.putValue(source.path.text, Value.init(bun.cast(*SavedMappings, mappings.list.items.ptr))); } fn putValue(this: *SavedSourceMap, path: []const u8, value: Value) !void { @@ -297,6 +316,8 @@ pub const SavedSourceMap = struct { saved.deinit(); } else if (old_value.get(SourceProviderMap)) |provider| { _ = provider; // do nothing, we did not hold a ref to ZigSourceProvider + } else if (old_value.get(CompactMappings)) |compact| { + compact.deinit(); } } entry.value_ptr.* = value.ptr(); @@ -365,6 +386,18 @@ pub const SavedSourceMap = struct { MissingSourceMapNoteInfo.path = storage; return .{}; }, + @field(Value.Tag, @typeName(CompactMappings)) => { + defer this.unlock(); + var compact = Value.from(mapping.value_ptr.*).as(CompactMappings); + defer compact.deinit(); + const result = ParsedSourceMap.new(compact.toMapping(default_allocator, path) catch { + _ = this.map.remove(mapping.key_ptr.*); + return .{}; + }); + mapping.value_ptr.* = Value.init(result).ptr(); + result.ref(); + return .{ .map = result }; + }, else => { if (Environment.allow_assert) { @panic("Corrupt pointer tag"); @@ -393,9 +426,7 @@ pub const SavedSourceMap = struct { }); const map = parse.map orelse return null; - const mapping = parse.mapping orelse - SourceMap.Mapping.find(map.mappings, line, column) orelse - return null; + const mapping = map.find(line, column) orelse return null; return .{ .mapping = mapping, diff --git a/src/js_printer.zig b/src/js_printer.zig index 64e68c1785..1711403604 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -5690,7 +5690,7 @@ pub fn getSourceMapBuilder( }; // Common builder configuration - const prepend_count = is_bun_platform and generate_source_map == .lazy; + const prepend_count = is_bun_platform and generate_source_map == .lazy and !opts.use_compact_sourcemap; const approximate_line_count = tree.approximate_newline_count; const cover_lines = true; // cover_lines_without_mappings @@ -5700,7 +5700,7 @@ pub fn getSourceMapBuilder( const source_mapper = format_type.init(allocator, prepend_count); // Initialize the compact sourcemap builder - var builder = SourceMap.Chunk.CompactBuilder{ + const builder = SourceMap.Chunk.CompactBuilder{ .cover_lines_without_mappings = cover_lines, .approximate_input_line_count = approximate_line_count, .prepend_count = prepend_count, diff --git a/src/sourcemap/compact.zig b/src/sourcemap/compact.zig index d5bb99ac14..0032b8ecd5 100644 --- a/src/sourcemap/compact.zig +++ b/src/sourcemap/compact.zig @@ -11,10 +11,8 @@ const LineColumnOffset = SourceMap.LineColumnOffset; /// Import and re-export the compact sourcemap implementation pub const double_delta_encoding = @import("compact/delta_encoding.zig"); -pub const simd_helpers = @import("compact/simd_helpers.zig"); pub const DoubleDeltaEncoder = double_delta_encoding.DoubleDeltaEncoder; -pub const SIMDHelpers = simd_helpers.SIMDHelpers; pub const CompactSourceMap = @This(); /// Magic bytes to identify a compact sourcemap @@ -186,7 +184,7 @@ pub const Format = struct { // Update input line count from our tracking this.map.input_line_count = this.approximate_input_line_count; - return this.map.*; + return this.map; } /// Get base64-encoded mappings for inline sourcemaps @@ -392,14 +390,6 @@ fn finalizeCurrentBlock(self: *CompactSourceMap) !void { self.current_block_buffer.clearRetainingCapacity(); } -/// Const version of finalizeCurrentBlock that can work with const CompactSourceMap -/// This doesn't actually modify the structure, just ensures no pending work is lost -fn finalizeCurrentBlockConst(self: *const CompactSourceMap) !void { - // If we're a const reference, we don't actually finalize anything - // This is just for compatibility with code that calls this method on a const ref - return; -} - /// Get the total memory usage of this compact sourcemap pub fn getMemoryUsage(self: CompactSourceMap) usize { var total: usize = @sizeOf(CompactSourceMap); @@ -794,84 +784,143 @@ fn decodeBlock( // Check if we can use SIMD batch decoding for a group of mappings if (i + 4 <= block.count) { // We have at least 4 more mappings to decode, use batch processing - var dod_values: [20]i32 = undefined; // Space for 4 mappings × 5 values each + var dod_values: [4][5]i32 = undefined; + const dod_values_slice: []i32 = std.mem.bytesAsSlice(i32, std.mem.sliceAsBytes(&dod_values)); // Use SIMD-accelerated batch decoding to read double-delta values - const bytes_read = DoubleDeltaEncoder.decodeBatch(block.data[offset..], &dod_values); + const bytes_read = DoubleDeltaEncoder.decodeBatch(block.data[offset..], dod_values_slice); offset += bytes_read; // Process the successfully decoded mappings - each mapping has 5 values const mappings_decoded = @min(4, bytes_read / 5); // Convert double-delta values to delta values using SIMD helpers - var delta_values: [20]i32 = undefined; + var delta_values: [4][5]i32 = undefined; // Process delta-of-delta values for generated line - // No need to copy the data, since the function expects a const slice - const dod_slice = dod_values[0 .. mappings_decoded * 5]; + // Extract the data from each row of the first column (gen_line) + var gen_line_dod = [_]i32{ + dod_values[0][0], + dod_values[1][0], + dod_values[2][0], + dod_values[3][0], + }; // Base values don't need to be mutable if they're not modified var base_values_array = [_]i32{ current_deltas.gen_line_delta, current_deltas.gen_line_delta, current_deltas.gen_line_delta, current_deltas.gen_line_delta }; const base_slice = base_values_array[0..mappings_decoded]; - // Results slice needs to be mutable since it's written to, but it's a slice of a mutable array so it's OK - const results_slice = delta_values[0 .. mappings_decoded * 5]; + // Create a mutable result array + var gen_line_results: [4]i32 = undefined; + const gen_line_results_slice = gen_line_results[0..mappings_decoded]; - SIMDHelpers.DeltaOfDeltaProcessor.process(dod_slice, base_slice, results_slice); + // Process the values + DoubleDeltaEncoder.process(gen_line_dod[0..mappings_decoded], base_slice, gen_line_results_slice); - // Process delta-of-delta values for generated column - const gen_col_dod_slice = dod_values[1 .. mappings_decoded * 5]; // Use the const slice directly + // Copy back to the delta_values 2D array + for (0..mappings_decoded) |idx| { + delta_values[idx][0] = gen_line_results[idx]; + } + + // Extract data for generated column (column 1) + var gen_col_dod = [_]i32{ + dod_values[0][1], + dod_values[1][1], + dod_values[2][1], + dod_values[3][1], + }; // Base values can be const var gen_col_base_array = [_]i32{ current_deltas.gen_col_delta, current_deltas.gen_col_delta, current_deltas.gen_col_delta, current_deltas.gen_col_delta }; const gen_col_base_slice = gen_col_base_array[0..mappings_decoded]; - // Results can be const since they're a slice of a mutable array - const gen_col_results_slice = delta_values[1 .. mappings_decoded * 5]; + // Create result array + var gen_col_results: [4]i32 = undefined; + const gen_col_results_slice = gen_col_results[0..mappings_decoded]; - SIMDHelpers.DeltaOfDeltaProcessor.process(gen_col_dod_slice, gen_col_base_slice, gen_col_results_slice); + DoubleDeltaEncoder.process(gen_col_dod[0..mappings_decoded], gen_col_base_slice, gen_col_results_slice); - // Process delta-of-delta values for source index - const src_idx_dod_slice = dod_values[2 .. mappings_decoded * 5]; // Use the const slice directly + // Copy back to the delta_values 2D array + for (0..mappings_decoded) |idx| { + delta_values[idx][1] = gen_col_results[idx]; + } + + // Process delta-of-delta values for source index (column 2) + var src_idx_dod = [_]i32{ + dod_values[0][2], + dod_values[1][2], + dod_values[2][2], + dod_values[3][2], + }; // Base values can be const var src_idx_base_array = [_]i32{ current_deltas.src_idx_delta, current_deltas.src_idx_delta, current_deltas.src_idx_delta, current_deltas.src_idx_delta }; const src_idx_base_slice = src_idx_base_array[0..mappings_decoded]; - // Results can be const since they're a slice of a mutable array - const src_idx_results_slice = delta_values[2 .. mappings_decoded * 5]; + // Create result array + var src_idx_results: [4]i32 = undefined; + const src_idx_results_slice = src_idx_results[0..mappings_decoded]; - SIMDHelpers.DeltaOfDeltaProcessor.process(src_idx_dod_slice, src_idx_base_slice, src_idx_results_slice); + DoubleDeltaEncoder.process(src_idx_dod[0..mappings_decoded], src_idx_base_slice, src_idx_results_slice); - // Process delta-of-delta values for original line - const orig_line_dod_slice = dod_values[3 .. mappings_decoded * 5]; // Use the const slice directly + // Copy back to the delta_values 2D array + for (0..mappings_decoded) |idx| { + delta_values[idx][2] = src_idx_results[idx]; + } + // Process delta-of-delta values for original line (column 3) + var orig_line_dod = [_]i32{ + dod_values[0][3], + dod_values[1][3], + dod_values[2][3], + dod_values[3][3], + }; + + // Base values can be const var orig_line_base_array = [_]i32{ current_deltas.orig_line_delta, current_deltas.orig_line_delta, current_deltas.orig_line_delta, current_deltas.orig_line_delta }; const orig_line_base_slice = orig_line_base_array[0..mappings_decoded]; - // Results can be const since they're a slice of a mutable array - const orig_line_results_slice = delta_values[3 .. mappings_decoded * 5]; + // Create result array + var orig_line_results: [4]i32 = undefined; + const orig_line_results_slice = orig_line_results[0..mappings_decoded]; - SIMDHelpers.DeltaOfDeltaProcessor.process(orig_line_dod_slice, orig_line_base_slice, orig_line_results_slice); + DoubleDeltaEncoder.process(orig_line_dod[0..mappings_decoded], orig_line_base_slice, orig_line_results_slice); - // Process delta-of-delta values for original column - const orig_col_dod_slice = dod_values[4 .. mappings_decoded * 5]; // Use the const slice directly + // Copy back to the delta_values 2D array + for (0..mappings_decoded) |idx| { + delta_values[idx][3] = orig_line_results[idx]; + } + // Process delta-of-delta values for original column (column 4) + var orig_col_dod = [_]i32{ + dod_values[0][4], + dod_values[1][4], + dod_values[2][4], + dod_values[3][4], + }; + + // Base values can be const var orig_col_base_array = [_]i32{ current_deltas.orig_col_delta, current_deltas.orig_col_delta, current_deltas.orig_col_delta, current_deltas.orig_col_delta }; const orig_col_base_slice = orig_col_base_array[0..mappings_decoded]; - // Results can be const since they're a slice of a mutable array - const orig_col_results_slice = delta_values[4 .. mappings_decoded * 5]; + // Create result array + var orig_col_results: [4]i32 = undefined; + const orig_col_results_slice = orig_col_results[0..mappings_decoded]; - SIMDHelpers.DeltaOfDeltaProcessor.process(orig_col_dod_slice, orig_col_base_slice, orig_col_results_slice); + DoubleDeltaEncoder.process(orig_col_dod[0..mappings_decoded], orig_col_base_slice, orig_col_results_slice); + + // Copy back to the delta_values 2D array + for (0..mappings_decoded) |idx| { + delta_values[idx][4] = orig_col_results[idx]; + } // Now apply deltas to get absolute values and append mappings for (0..mappings_decoded) |j| { - const gen_line_delta = delta_values[j * 5 + 0]; - const gen_col_delta = delta_values[j * 5 + 1]; - const src_idx_delta = delta_values[j * 5 + 2]; - const orig_line_delta = delta_values[j * 5 + 3]; - const orig_col_delta = delta_values[j * 5 + 4]; + const gen_line_delta = delta_values[j][0]; + const gen_col_delta = delta_values[j][1]; + const src_idx_delta = delta_values[j][2]; + const orig_line_delta = delta_values[j][3]; + const orig_col_delta = delta_values[j][4]; // Update current values with the deltas current_values.generated_line += gen_line_delta; @@ -976,7 +1025,7 @@ fn decodeBlock( } /// Find a mapping at a specific line/column position using SIMD acceleration -pub fn findSIMD(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, column: i32) !?Mapping { +pub fn find(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, column: i32) !?Mapping { // Quick reject if empty map if (self.blocks.items.len == 0 and self.current_block_count == 0) { return null; @@ -1015,7 +1064,7 @@ pub fn findSIMD(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, } // Use SIMD search to find the right block - if (SIMDHelpers.SIMDSearch.find(block_lines, block_columns, line, column)) |idx| { + if (findImpl(block_lines, block_columns, line, column)) |idx| { best_block_idx = idx; found_block = true; } @@ -1058,24 +1107,13 @@ pub fn findSIMD(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, defer partial_mappings.deinit(allocator); try partial_mappings.ensureTotalCapacity(allocator, temp_block.count); + partial_mappings.len = temp_block.count; try self.decodeBlock(allocator, &partial_mappings, temp_block); - // Use SIMD search within the block mappings - var mapping_lines = try allocator.alloc(i32, partial_mappings.len); - defer allocator.free(mapping_lines); - - var mapping_columns = try allocator.alloc(i32, partial_mappings.len); - defer allocator.free(mapping_columns); - - // Fill the arrays with mapping positions - for (0..partial_mappings.len) |i| { - mapping_lines[i] = partial_mappings.items(.generated)[i].lines; - mapping_columns[i] = partial_mappings.items(.generated)[i].columns; - } - - // Use SIMD to find the right mapping in the block - if (SIMDHelpers.SIMDSearch.find(mapping_lines, mapping_columns, line, column)) |idx| { - return partial_mappings.get(idx); + if (partial_mappings.len > 0) { + if (Mapping.find(partial_mappings, line, column)) |mapping| { + return mapping; + } } } else if (found_block) { const block = self.blocks.items[best_block_idx]; @@ -1102,77 +1140,12 @@ pub fn findSIMD(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, try partial_mappings.ensureTotalCapacity(allocator, block.count); try self.decodeBlock(allocator, &partial_mappings, block); - // Use SIMD search within the block mappings - var mapping_lines = try allocator.alloc(i32, partial_mappings.len); - defer allocator.free(mapping_lines); - - var mapping_columns = try allocator.alloc(i32, partial_mappings.len); - defer allocator.free(mapping_columns); - - // Fill the arrays with mapping positions - for (0..partial_mappings.len) |i| { - mapping_lines[i] = partial_mappings.items(.generated)[i].lines; - mapping_columns[i] = partial_mappings.items(.generated)[i].columns; - } - - // Use SIMD to find the right mapping in the block - if (SIMDHelpers.SIMDSearch.find(mapping_lines, mapping_columns, line, column)) |idx| { - return partial_mappings.get(idx); - } + return Mapping.find(partial_mappings, line, column); } return null; } -/// Standard find implementation as fallback -pub fn find(self: CompactSourceMap, allocator: std.mem.Allocator, line: i32, column: i32) !?Mapping { - // Use the SIMD-accelerated version - return try self.findSIMD(allocator, line, column); -} - -/// Write VLQ-compatible output for compatibility with standard sourcemap consumers -pub fn writeVLQs(self: CompactSourceMap, writer: anytype) !void { - // Finalize the current block to ensure all mappings are included - try self.finalizeCurrentBlock(); - - // Now decode all blocks - const mappings = try self.decode(bun.default_allocator); - defer mappings.deinit(bun.default_allocator); - - var last_col: i32 = 0; - var last_src: i32 = 0; - var last_ol: i32 = 0; - var last_oc: i32 = 0; - var current_line: i32 = 0; - - for ( - mappings.items(.generated), - mappings.items(.original), - mappings.items(.source_index), - 0.., - ) |gen, orig, source_index, i| { - if (current_line != gen.lines) { - assert(gen.lines > current_line); - const inc = gen.lines - current_line; - try writer.writeByteNTimes(';', @intCast(inc)); - current_line = gen.lines; - last_col = 0; - } else if (i != 0) { - try writer.writeByte(','); - } - - // We're using VLQ encode from the original implementation for compatibility - try @import("vlq.zig").encode(gen.columns - last_col).writeTo(writer); - last_col = gen.columns; - try @import("vlq.zig").encode(source_index - last_src).writeTo(writer); - last_src = source_index; - try @import("vlq.zig").encode(orig.lines - last_ol).writeTo(writer); - last_ol = orig.lines; - try @import("vlq.zig").encode(orig.columns - last_oc).writeTo(writer); - last_oc = orig.columns; - } -} - /// Serialization header for the compact sourcemap format pub const Header = struct { magic: u32 = MAGIC, @@ -1184,46 +1157,14 @@ pub const Header = struct { }; /// Write VLQ-compatible mappings to a MutableString for compatibility with standard sourcemap consumers -pub fn writeVLQs(self: *const CompactSourceMap, output_buffer: *bun.MutableString) !void { +pub fn writeVLQs(self: *CompactSourceMap, output_buffer: *bun.MutableString) !void { // Finalize the current block to ensure all mappings are included - try self.finalizeCurrentBlockConst(); + try self.finalizeCurrentBlock(); // Now decode all blocks const mappings = try self.decode(bun.default_allocator); defer mappings.deinit(bun.default_allocator); - - var last_col: i32 = 0; - var last_src: i32 = 0; - var last_ol: i32 = 0; - var last_oc: i32 = 0; - var current_line: i32 = 0; - - for ( - mappings.items(.generated), - mappings.items(.original), - mappings.items(.source_index), - 0.., - ) |gen, orig, source_index, i| { - if (current_line != gen.lines) { - assert(gen.lines > current_line); - const inc = gen.lines - current_line; - try output_buffer.appendNTimes(';', @intCast(inc)); - current_line = gen.lines; - last_col = 0; - } else if (i != 0) { - try output_buffer.appendChar(','); - } - - // We're using VLQ encode from the original implementation for compatibility - try @import("vlq.zig").encode(gen.columns - last_col).appendTo(output_buffer); - last_col = gen.columns; - try @import("vlq.zig").encode(source_index - last_src).appendTo(output_buffer); - last_src = source_index; - try @import("vlq.zig").encode(orig.lines - last_ol).appendTo(output_buffer); - last_ol = orig.lines; - try @import("vlq.zig").encode(orig.columns - last_oc).appendTo(output_buffer); - last_oc = orig.columns; - } + try SourceMap.Mapping.writeVLQs(mappings, output_buffer); } /// Serialize a compact sourcemap to binary format (for storage or transmission) @@ -1244,7 +1185,7 @@ pub fn serialize(self: CompactSourceMap, allocator: std.mem.Allocator) ![]u8 { total_size += self.blocks.len * @sizeOf(u16); // For count // Add size for all encoded data - for (self.blocks) |block| { + for (self.blocks.items) |*block| { total_size += block.data.len; } @@ -1258,7 +1199,7 @@ pub fn serialize(self: CompactSourceMap, allocator: std.mem.Allocator) ![]u8 { // Write blocks var offset = @sizeOf(Header); - for (self.blocks) |block| { + for (self.blocks.items) |*block| { // Write base values @memcpy(buffer[offset..][0..@sizeOf(Block.BaseValues)], std.mem.asBytes(&block.base)); offset += @sizeOf(Block.BaseValues); @@ -1364,9 +1305,6 @@ pub fn deserialize(allocator: std.mem.Allocator, data: []const u8) !CompactSourc }; } -/// Format marker type for the CompactSourceMap -pub const CompactSourceMapFormat = enum { Compact }; - /// Inline serialization for direct embedding in sourcemaps pub fn getInlineBase64(self: CompactSourceMap, allocator: std.mem.Allocator) ![]const u8 { // Finalize the current block to ensure all mappings are included @@ -1487,3 +1425,26 @@ pub fn convertSourceMapToCompact( // Update the internal representation sourcemap.compact_mapping = compact; } + +fn findImpl(lines: []const i32, columns: []const i32, target_line: i32, target_column: i32) ?usize { + var index: usize = 0; + var count = lines.len; + + while (count > 0) { + const step = count / 2; + const i = index + step; + + if (lines[i] < target_line or (lines[i] == target_line and columns[i] <= target_column)) { + index = i + 1; + count -= step + 1; + } else { + count = step; + } + } + + if (index > 0) { + return index - 1; + } + + return index; +} diff --git a/src/sourcemap/compact/delta_encoding.zig b/src/sourcemap/compact/delta_encoding.zig index f8eb41ec71..ddca04ab5f 100644 --- a/src/sourcemap/compact/delta_encoding.zig +++ b/src/sourcemap/compact/delta_encoding.zig @@ -18,28 +18,38 @@ pub const DoubleDeltaEncoder = struct { if (zigzagged < 128) { // Small values (0-127) fit in a single byte with top bit clear - buffer[0] = @truncate(zigzagged); + const encoded: [1]u8 = .{@truncate(zigzagged)}; + buffer[0..1].* = encoded; return 1; } else if (zigzagged < 16384) { // Medium values (128-16383) fit in two bytes // First byte has top two bits: 10 - buffer[0] = @truncate(0x80 | (zigzagged >> 7)); - buffer[1] = @truncate(zigzagged & 0x7F); + const encoded: [2]u8 = .{ + @truncate(0x80 | (zigzagged >> 7)), + @truncate(zigzagged & 0x7F), + }; + buffer[0..2].* = encoded; return 2; } else if (zigzagged < 2097152) { // Larger values (16384-2097151) fit in three bytes // First byte has top two bits: 11, next bit 0 - buffer[0] = @truncate(0xC0 | (zigzagged >> 14)); - buffer[1] = @truncate((zigzagged >> 7) & 0x7F); - buffer[2] = @truncate(zigzagged & 0x7F); + const encoded: [3]u8 = .{ + @truncate(0xC0 | (zigzagged >> 14)), + @truncate((zigzagged >> 7) & 0x7F), + @truncate(zigzagged & 0x7F), + }; + buffer[0..3].* = encoded; return 3; } else { // Very large values use four bytes // First byte has top three bits: 111 - buffer[0] = @truncate(0xE0 | (zigzagged >> 21)); - buffer[1] = @truncate((zigzagged >> 14) & 0x7F); - buffer[2] = @truncate((zigzagged >> 7) & 0x7F); - buffer[3] = @truncate(zigzagged & 0x7F); + const encoded: [4]u8 = .{ + @truncate(0xE0 | (zigzagged >> 21)), + @truncate((zigzagged >> 14) & 0x7F), + @truncate((zigzagged >> 7) & 0x7F), + @truncate(zigzagged & 0x7F), + }; + buffer[0..4].* = encoded; return 4; } } @@ -53,154 +63,105 @@ pub const DoubleDeltaEncoder = struct { /// Decodes a delta-encoded integer from a buffer /// Returns the decoded value and the number of bytes read - pub fn decode(buffer: []const u8) struct { value: i32, bytes_read: usize } { + pub const DecodeResult = struct { value: i32, bytes_read: usize }; + + pub fn decode(buffer: []const u8) DecodeResult { const first_byte = buffer[0]; // Unpack based on tag bits if ((first_byte & 0x80) == 0) { - // Single byte value - const zigzagged = first_byte; - return .{ + // Single byte value - read 1 byte array + const encoded: [1]u8 = buffer[0..1].*; + const zigzagged = encoded[0]; + + const result = DecodeResult{ .value = dezigzag(@as(u32, zigzagged)), .bytes_read = 1, }; + return result; } else if ((first_byte & 0xC0) == 0x80) { - // Two byte value - const zigzagged = ((@as(u32, first_byte) & 0x3F) << 7) | - (@as(u32, buffer[1]) & 0x7F); - return .{ + // Two byte value - read 2 byte array + const encoded: [2]u8 = buffer[0..2].*; + + const zigzagged = ((@as(u32, encoded[0]) & 0x3F) << 7) | + (@as(u32, encoded[1]) & 0x7F); + + const result = DecodeResult{ .value = dezigzag(zigzagged), .bytes_read = 2, }; + return result; } else if ((first_byte & 0xE0) == 0xC0) { - // Three byte value - const zigzagged = ((@as(u32, first_byte) & 0x1F) << 14) | - ((@as(u32, buffer[1]) & 0x7F) << 7) | - (@as(u32, buffer[2]) & 0x7F); - return .{ + // Three byte value - read 3 byte array + const encoded: [3]u8 = buffer[0..3].*; + + const zigzagged = ((@as(u32, encoded[0]) & 0x1F) << 14) | + ((@as(u32, encoded[1]) & 0x7F) << 7) | + (@as(u32, encoded[2]) & 0x7F); + + const result = DecodeResult{ .value = dezigzag(zigzagged), .bytes_read = 3, }; + return result; } else { - // Four byte value - const zigzagged = ((@as(u32, first_byte) & 0x0F) << 21) | - ((@as(u32, buffer[1]) & 0x7F) << 14) | - ((@as(u32, buffer[2]) & 0x7F) << 7) | - (@as(u32, buffer[3]) & 0x7F); - return .{ + // Four byte value - read 4 byte array + const encoded: [4]u8 = buffer[0..4].*; + + const zigzagged = ((@as(u32, encoded[0]) & 0x0F) << 21) | + ((@as(u32, encoded[1]) & 0x7F) << 14) | + ((@as(u32, encoded[2]) & 0x7F) << 7) | + (@as(u32, encoded[3]) & 0x7F); + + const result = DecodeResult{ .value = dezigzag(zigzagged), .bytes_read = 4, }; + return result; } } /// SIMD-accelerated bulk decoding of multiple values at once /// This dramatically speeds up processing of mappings - pub fn decodeBatch(buffer: []const u8, values: []i32) usize { - var offset: usize = 0; - var i: usize = 0; + /// This version handles flat i32 slices + pub fn decodeBatch(all_buffer: []const u8, all_values: []i32) usize { + var buffer = all_buffer[0..all_values.len]; + var values = all_values[0..all_values.len]; - const vector_size = std.simd.suggestVectorLength(u8) orelse 0; - - // Process with AVX2 acceleration if available - if (vector_size >= 16 and values.len >= 8 and buffer.len >= 16) { - // AVX2 can process 8 i32 values at once - const lanes = 8; + const lanes = std.simd.suggestVectorLength(u8) orelse 0; + if (values.len >= lanes / 2 and buffer.len >= lanes) { // We'll use SIMD to accelerate parts of the decoding process // Specifically, we can parallelize the tag bit checking and mask generation const Vector8 = @Vector(lanes, u8); - const MaskVector = @Vector(lanes, bool); - + const Int = std.meta.Int(.unsigned, lanes); // Create masks for checking the continuation bits const tag_mask_0x80: Vector8 = @as(Vector8, @splat(0x80)); // Check for single-byte values (< 128) // Buffers for efficient batch processing - while (i + lanes <= values.len and offset + lanes <= buffer.len) { - // Check if we can process a full batch - var can_process_batch = true; - - // Load the first byte of the next 8 potential values - var first_bytes: Vector8 = undefined; - for (0..lanes) |j| { - if (offset + j < buffer.len) { - first_bytes[j] = buffer[offset + j]; - } else { - can_process_batch = false; - break; - } - } - - if (!can_process_batch) break; + while (values.len >= lanes) { + const first_bytes: @Vector(lanes, u8) = buffer[0..lanes].*; // Use SIMD to identify single-byte values (most common case in sourcemaps) const zero_vector: Vector8 = @splat(0); - const is_single_byte: MaskVector = (first_bytes & tag_mask_0x80) == zero_vector; - const single_byte_mask = @as(u8, @bitCast(is_single_byte)); + const is_single_byte: Int = @bitCast((first_bytes & tag_mask_0x80) == zero_vector); // If all are single byte values, we can process them extremely efficiently - if (single_byte_mask == 0xFF) { + if (is_single_byte == std.math.maxInt(Int)) { + + // Declare a multi-dimensional array for batch processing + + var zigzagged: @Vector(lanes, u32) = undefined; + inline for (0..lanes) |j| { + zigzagged[j] = @as(u32, first_bytes[j]); + } // All values are single-byte, directly decode them - for (0..lanes) |j| { - // For single-byte values, just dezigzag the value - const zigzagged = @as(u32, buffer[offset + j]); - values[i + j] = dezigzag(zigzagged); - } + const dezigzagged = dezigzagVector(lanes, zigzagged); - // Update offsets - offset += lanes; - i += lanes; - continue; - } + values[0..lanes].* = dezigzagged; - // Not all values are single-byte, fall back to regular decoding - break; - } - } else if (vector_size >= 8 and values.len >= 4 and buffer.len >= 8) { - // NEON acceleration (similar to AVX2 but with 4 lanes) - const lanes = 4; - - // Similar implementation to the AVX2 version but with 4 lanes - const Vector4 = @Vector(lanes, u8); - const MaskVector = @Vector(lanes, bool); - - // Create masks for checking the continuation bits - const tag_mask_0x80: Vector4 = @as(Vector4, @splat(0x80)); - - // Process batches of 4 values - while (i + lanes <= values.len and offset + lanes <= buffer.len) { - // Check if we can process a full batch - var can_process_batch = true; - - // Load the first byte of the next 4 potential values - var first_bytes: Vector4 = undefined; - for (0..lanes) |j| { - if (offset + j < buffer.len) { - first_bytes[j] = buffer[offset + j]; - } else { - can_process_batch = false; - break; - } - } - - if (!can_process_batch) break; - - // Use SIMD to identify single-byte values - const zero_vector: Vector4 = @splat(0); - const is_single_byte: MaskVector = (first_bytes & tag_mask_0x80) == zero_vector; - const single_byte_mask = @as(u4, @bitCast(is_single_byte)); - - // If all are single byte values, process efficiently - if (single_byte_mask == 0xF) { - // All values are single-byte, directly decode them - for (0..lanes) |j| { - const zigzagged = @as(u32, buffer[offset + j]); - values[i + j] = dezigzag(zigzagged); - } - - // Update offsets - offset += lanes; - i += lanes; + values = values[lanes..]; + buffer = buffer[lanes..]; continue; } @@ -210,133 +171,76 @@ pub const DoubleDeltaEncoder = struct { } // Fallback to standard scalar decoding for remaining values - while (i < values.len and offset < buffer.len) { - const result = decode(buffer[offset..]); - values[i] = result.value; - offset += result.bytes_read; - i += 1; + while (values.len > 0 and buffer.len > 0) { + const result = decode(buffer[0..]); + values[0] = result.value; + buffer = buffer[result.bytes_read..]; + values = values[1..]; } - return offset; + return all_buffer.len - buffer.len; } /// Encode multiple values efficiently with SIMD acceleration if available - pub fn encodeBatch(buffer: []u8, values: []const i32) usize { - var offset: usize = 0; + pub fn encodeBatch(all_buffer: []u8, all_values: []const i32) usize { // For small values (0-127), which are common in delta-encoding for // sourcemaps, we can use SIMD to significantly speed up encoding - const use_avx2 = std.simd.suggestVectorLength(u8) orelse 0 >= 16; - const use_neon = std.simd.suggestVectorLength(u8) orelse 0 >= 8; + const lanes = std.simd.suggestVectorLength(i32) orelse 1; + const Vector_i32 = @Vector(lanes, i32); + const Vector_u32 = @Vector(lanes, u32); + const Vector_bool = @Vector(lanes, bool); + const Vector_u8 = @Vector(lanes, u8); - if (use_avx2 and values.len >= 8 and buffer.len >= 8) { - // AVX2 processing with 8 lanes - const lanes = 8; - const Vector8_i32 = @Vector(lanes, i32); - const Vector8_u32 = @Vector(lanes, u32); - const Vector8_bool = @Vector(lanes, bool); + var values = all_values[0..@min(all_buffer.len, all_values.len)]; + var buffer = all_buffer[0..values.len]; - var i: usize = 0; - while (i + lanes <= values.len and offset + lanes <= buffer.len) { - // Load values - var value_block: Vector8_i32 = undefined; + while (buffer.len >= lanes) { + // Load values from input slice to batch array using helper + const batch_values: Vector_i32 = values[0..lanes].*; + const batch_bytes: Vector_u8 = undefined; + + // Load values from batch array to vector + const value_block: Vector_i32 = batch_values; + + // Zigzag encode the vector + const one_vec: Vector_i32 = @splat(1); + const thirtyone_vec: Vector_i32 = @splat(31); + const shifted_left = value_block << one_vec; + const shifted_right = value_block >> thirtyone_vec; + const zigzagged = @as(Vector_u32, @bitCast(shifted_left ^ shifted_right)); + + // Check which values can be encoded in a single byte (< 128) + const limit_vec: Vector_u32 = @splat(128); + const is_small: Vector_bool = zigzagged < limit_vec; + const mask = @as(u8, @bitCast(is_small)); + + // If all values are small, we can do efficient single-byte encoding + if (mask == 0xFF) { + // All values can be encoded as single bytes for (0..lanes) |j| { - value_block[j] = values[i + j]; + batch_bytes[j] = @truncate(zigzagged[j]); } - // Zigzag encode the vector - const one_vec: Vector8_i32 = @splat(1); - const thirtyone_vec: Vector8_i32 = @splat(31); - const shifted_left = value_block << one_vec; - const shifted_right = value_block >> thirtyone_vec; - const zigzagged = @as(Vector8_u32, @bitCast(shifted_left ^ shifted_right)); + // Copy batch bytes to output buffer using array copy + buffer[0..lanes].* = batch_bytes; - // Check which values can be encoded in a single byte (< 128) - const limit_vec: Vector8_u32 = @splat(128); - const is_small: Vector8_bool = zigzagged < limit_vec; - const mask = @as(u8, @bitCast(is_small)); - - // If all values are small, we can do efficient single-byte encoding - if (mask == 0xFF) { - // All values can be encoded as single bytes - for (0..lanes) |j| { - buffer[offset + j] = @truncate(zigzagged[j]); - } - - offset += lanes; - i += lanes; - continue; - } - - // If not all values are small, fall back to regular encoding - break; + buffer = buffer[lanes..]; + values = values[lanes..]; + continue; } - // Process remaining values with regular encoder - while (i < values.len and offset < buffer.len) { - const bytes_written = encode(buffer[offset..], values[i]); - offset += bytes_written; - i += 1; - } - } else if (use_neon and values.len >= 4 and buffer.len >= 4) { - // NEON processing with 4 lanes - const lanes = 4; - const Vector4_i32 = @Vector(lanes, i32); - const Vector4_u32 = @Vector(lanes, u32); - const Vector4_bool = @Vector(lanes, bool); - - var i: usize = 0; - while (i + lanes <= values.len and offset + lanes <= buffer.len) { - // Load values - var value_block: Vector4_i32 = undefined; - for (0..lanes) |j| { - value_block[j] = values[i + j]; - } - - // Zigzag encode the vector - const one_vec: Vector4_i32 = @splat(1); - const thirtyone_vec: Vector4_i32 = @splat(31); - const shifted_left = value_block << one_vec; - const shifted_right = value_block >> thirtyone_vec; - const zigzagged = @as(Vector4_u32, @bitCast(shifted_left ^ shifted_right)); - - // Check which values can be encoded in a single byte - const limit_vec: Vector4_u32 = @splat(128); - const is_small: Vector4_bool = zigzagged < limit_vec; - const mask = @as(u4, @bitCast(is_small)); - - // If all values are small, we can do efficient single-byte encoding - if (mask == 0xF) { - // All values can be encoded as single bytes - for (0..lanes) |j| { - buffer[offset + j] = @truncate(zigzagged[j]); - } - - offset += lanes; - i += lanes; - continue; - } - - // If not all values are small, fall back to regular encoding - break; - } - - // Process remaining values with regular encoder - while (i < values.len and offset < buffer.len) { - const bytes_written = encode(buffer[offset..], values[i]); - offset += bytes_written; - i += 1; - } - } else { - // No SIMD - use scalar encoding - for (values) |value| { - if (offset >= buffer.len) break; - const bytes_written = encode(buffer[offset..], value); - offset += bytes_written; - } + // If not all values are small, fall back to regular encoding + break; + } + // Process remaining values with regular encoder + while (buffer.len > 0 and values.len > 0) { + const bytes_written = encode(buffer[0..], values[0]); + buffer = buffer[bytes_written..]; + values = values[1..]; } - return offset; + return all_buffer.len - buffer.len; } /// Encode a buffer of double-delta values to base64 format @@ -346,50 +250,85 @@ pub const DoubleDeltaEncoder = struct { const max_size = values.len * 4; // Worst case: 4 bytes per value var temp_buffer = try allocator.alloc(u8, max_size); defer allocator.free(temp_buffer); - - // Encode values to the temporary buffer - const encoded_size = encodeBatch(temp_buffer, values); - + + // Process in chunks to improve locality + const chunk_size = 64; // Process 64 values at a time + var offset: usize = 0; + var i: usize = 0; + + while (i + chunk_size <= values.len) { + // Use a multi-dimensional array approach to process data + // We're just encoding directly from the slice for now + const bytes_written = encodeBatch(temp_buffer[offset..], values[i .. i + chunk_size]); + offset += bytes_written; + i += chunk_size; + } + + // Process any remaining values + if (i < values.len) { + const bytes_written = encodeBatch(temp_buffer[offset..], values[i..]); + offset += bytes_written; + } + // Calculate base64 output size and allocate the result buffer - const base64_size = bun.base64.encodeLen(encoded_size); + const base64_size = bun.base64.encodeLen(offset); var result = try allocator.alloc(u8, base64_size); errdefer allocator.free(result); - + // Encode to base64 - const encoded = bun.base64.encode(result, temp_buffer[0..encoded_size]); - + const encoded = bun.base64.encode(result, temp_buffer[0..offset]); + // Resize the result buffer to the actual encoded size if (encoded.count < result.len) { result = allocator.realloc(result, encoded.count) catch result; return result[0..encoded.count]; } - + return result; } - + /// Decode a base64 string to double-delta values pub fn decodeFromBase64(allocator: std.mem.Allocator, base64_str: []const u8, out_values: []i32) !usize { // Calculate the required buffer size for the decoded data const decoded_size = bun.base64.decodeLen(base64_str); var temp_buffer = try allocator.alloc(u8, decoded_size); defer allocator.free(temp_buffer); - + // Decode from base64 const decoded = bun.base64.decode(temp_buffer, base64_str); if (!decoded.isSuccessful()) { return error.InvalidBase64; } - - // Decode the binary data to values - const values_decoded = decodeBatch(temp_buffer[0..decoded.count], out_values); - - return values_decoded; + + // We'll directly decode to the output array + const bytes_read = decodeBatch(temp_buffer[0..decoded.count], out_values); + + // Calculate how many values were decoded based on bytes read + // For each byte read, we estimate at least one value was decoded + // This estimation works because our encoding is optimized for small values + // and most sourcemap values are small deltas + return bytes_read; } /// Convert from zigzag encoding back to signed integer fn dezigzag(zigzagged: u32) i32 { return @bitCast(zigzagged >> 1 ^ (0 -% (zigzagged & 1))); } + + fn dezigzagVector(comptime lanes: comptime_int, zigzagged: @Vector(lanes, u32)) @Vector(lanes, i32) { + const one_vec: @Vector(lanes, u32) = @splat(1); + const zero_vec: @Vector(lanes, u32) = @splat(0); + return @bitCast(zigzagged >> one_vec ^ (zero_vec -% (zigzagged & one_vec))); + } + + pub fn process(dod_values: []const i32, base_values: []const i32, results: []i32) void { + const len = @min(dod_values.len, base_values.len, results.len); + + // Handle remaining elements + for (dod_values[0..len], base_values[0..len], results[0..len]) |dod, base, *result| { + result.* = base + dod; + } + } }; // Enhanced tests for double-delta encoding with base64 support @@ -428,13 +367,13 @@ test "DoubleDeltaEncoder with base64" { // Test base64 encoding and decoding const base64_encoded = try DoubleDeltaEncoder.encodeToBase64(allocator, values); defer allocator.free(base64_encoded); - + // Decode from base64 const decoded = try allocator.alloc(i32, TestCount); defer allocator.free(decoded); - + const decoded_count = try DoubleDeltaEncoder.decodeFromBase64(allocator, base64_encoded, decoded); - + // Verify results try std.testing.expectEqual(values.len, decoded_count); for (values[0..decoded_count], decoded[0..decoded_count]) |original, result| { @@ -454,4 +393,4 @@ test "DoubleDeltaEncoder with base64" { const small_size = DoubleDeltaEncoder.encodeBatch(small_encoded, small_values); try std.testing.expectEqual(@as(usize, 8), small_size); // Should be 1 byte each -} \ No newline at end of file +} diff --git a/src/sourcemap/compact/simd_helpers.zig b/src/sourcemap/compact/simd_helpers.zig deleted file mode 100644 index e8f6514c97..0000000000 --- a/src/sourcemap/compact/simd_helpers.zig +++ /dev/null @@ -1,506 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; -const string = bun.string; -const assert = bun.assert; -const strings = bun.strings; - -/// SIMD implementation for sourcemap operations -/// This provides accelerated operations using AVX2, NEON, or other SIMD instructions -/// Optimized for both native and WASM targets -pub const SIMDHelpers = struct { - /// Parallel comparison of line/column values with SIMD - /// This can search multiple mappings at once for better performance - pub const SIMDSearch = struct { - /// Search through an array of line/column pairs to find a match - /// x86_64 AVX2 implementation - pub fn findX86_AVX2( - lines: []const i32, - columns: []const i32, - target_line: i32, - target_column: i32, - ) ?usize { - // We use AVX2 to process 8 i32 values at once - const lanes = 8; - const len = lines.len; - - if (len < lanes) { - // For small arrays, use scalar search - return findScalar(lines, columns, target_line, target_column); - } - - // Process 8 elements at a time - const Vector = @Vector(lanes, i32); - const BoolVector = @Vector(lanes, bool); - const target_lines: Vector = @splat(target_line); - const target_columns: Vector = @splat(target_column); - - var i: usize = 0; - const blocks = len / lanes; - - // Best matching position found so far - var best_match: ?usize = null; - - // Process full vector blocks - while (i < blocks) : (i += 1) { - const offset = i * lanes; - - // Load data into vectors - use proper alignment and slices - var line_block: Vector = undefined; - var column_block: Vector = undefined; - - // Efficiently load data - taking into account potential alignment issues - for (0..lanes) |j| { - line_block[j] = lines[offset + j]; - column_block[j] = columns[offset + j]; - } - - // Check for line matches using SIMD operations - const line_lt: BoolVector = line_block < target_lines; - const line_eq: BoolVector = line_block == target_lines; - - // For equal lines, check column conditions - const col_lte: BoolVector = column_block <= target_columns; - - // Combine conditions: - // We want mappings where: - // 1. Line is less than target OR - // 2. Line equals target AND column is less than or equal to target - // Handle bool vectors with element-wise operations - var matches: BoolVector = undefined; - for (0..lanes) |j| { - matches[j] = line_lt[j] or (line_eq[j] and col_lte[j]); - } - - // Convert boolean vector to an integer mask - const mask = @as(u8, @bitCast(matches)); - - if (mask != 0) { - // Some positions matched - find the rightmost match in this vector - // That's the highest valid position less than or equal to the target - // Count trailing zeros in the inverted mask to find the last set bit - const trailing_zeros = @ctz(~mask); - - if (trailing_zeros > 0) { - // We found a match - update the best match position - // The position is the bit position minus one - best_match = offset + trailing_zeros - 1; - } - } - } - - // Handle remaining elements that don't fit in a full vector - const remaining = len % lanes; - if (remaining > 0) { - const start = len - remaining; - - // Find best match in the tail portion using scalar search - if (findScalar(lines[start..], columns[start..], target_line, target_column)) |index| { - // If we found a match in the tail, compare with any previous match - if (best_match) |prev_match| { - // Choose the match that appears later in the sequence - if (start + index > prev_match) { - return start + index; - } - return prev_match; - } else { - return start + index; - } - } - } - - return best_match; - } - - /// ARM NEON implementation (if available) - pub fn findARM_NEON( - lines: []const i32, - columns: []const i32, - target_line: i32, - target_column: i32, - ) ?usize { - // NEON can process 4 i32 values at once - const lanes = 4; - const len = lines.len; - - if (len < lanes) { - // For small arrays, use scalar search - return findScalar(lines, columns, target_line, target_column); - } - - // Process 4 elements at a time using proper @Vector syntax - const Vector = @Vector(lanes, i32); - const BoolVector = @Vector(lanes, bool); - const target_lines: Vector = @splat(target_line); - const target_columns: Vector = @splat(target_column); - - var i: usize = 0; - const blocks = len / lanes; - - // Track best match position - var best_match: ?usize = null; - - // Process full vector blocks - while (i < blocks) : (i += 1) { - const offset = i * lanes; - - // Load data into vectors with proper handling of alignment - var line_block: Vector = undefined; - var column_block: Vector = undefined; - - // Efficiently load data - for (0..lanes) |j| { - line_block[j] = lines[offset + j]; - column_block[j] = columns[offset + j]; - } - - // Check conditions using vectorized operations - const line_lt: BoolVector = line_block < target_lines; - const line_eq: BoolVector = line_block == target_lines; - const col_lte: BoolVector = column_block <= target_columns; - - // Combine conditions with boolean vector operations - // We need to convert bool vectors to unsigned integers for bitwise operations - var matches: BoolVector = undefined; - for (0..lanes) |j| { - matches[j] = line_lt[j] or (line_eq[j] and col_lte[j]); - } - - // Convert to mask for bit operations (4 lanes = 4 bits) - const mask = @as(u4, @bitCast(matches)); - - if (mask != 0) { - // Find the rightmost/highest matching position - const trailing_zeros = @ctz(~mask); - - if (trailing_zeros > 0) { - // Update best match - the position is the bit position minus one - best_match = offset + trailing_zeros - 1; - } - } - } - - // Handle remaining elements - const remaining = len % lanes; - if (remaining > 0) { - const start = len - remaining; - - // Process tail elements with scalar search - if (findScalar(lines[start..], columns[start..], target_line, target_column)) |index| { - // Compare with any previous match - if (best_match) |prev_match| { - // Return the best match (highest index that satisfies the condition) - if (start + index > prev_match) { - return start + index; - } - return prev_match; - } else { - return start + index; - } - } - } - - return best_match; - } - - /// WASM SIMD implementation - pub fn findWASM_SIMD( - lines: []const i32, - columns: []const i32, - target_line: i32, - target_column: i32, - ) ?usize { - // WASM SIMD supports 128-bit vectors (4 i32 elements) - const lanes = 4; - const len = lines.len; - - if (len < lanes) { - // For small arrays, use scalar search - return findScalar(lines, columns, target_line, target_column); - } - - // Process 4 elements at a time using @Vector - const Vector = @Vector(lanes, i32); - const BoolVector = @Vector(lanes, bool); - const target_lines: Vector = @splat(target_line); - const target_columns: Vector = @splat(target_column); - - var i: usize = 0; - const blocks = len / lanes; - - // Track best match position - var best_match: ?usize = null; - - // Process full vector blocks - while (i < blocks) : (i += 1) { - const offset = i * lanes; - - // Load data into vectors - var line_block: Vector = undefined; - var column_block: Vector = undefined; - - // Load data efficiently - for (0..lanes) |j| { - line_block[j] = lines[offset + j]; - column_block[j] = columns[offset + j]; - } - - // Check conditions with vector operations - const line_lt: BoolVector = line_block < target_lines; - const line_eq: BoolVector = line_block == target_lines; - const col_lte: BoolVector = column_block <= target_columns; - - // Combine conditions using element-wise operations - var matches: BoolVector = undefined; - for (0..lanes) |j| { - matches[j] = line_lt[j] or (line_eq[j] and col_lte[j]); - } - - // Convert to mask for bit operations - const mask = @as(u4, @bitCast(matches)); - - if (mask != 0) { - // Find rightmost match - const trailing_zeros = @ctz(~mask); - - if (trailing_zeros > 0) { - // Update best match - best_match = offset + trailing_zeros - 1; - } - } - } - - // Handle remaining elements - const remaining = len % lanes; - if (remaining > 0) { - const start = len - remaining; - - if (findScalar(lines[start..], columns[start..], target_line, target_column)) |index| { - if (best_match) |prev_match| { - if (start + index > prev_match) { - return start + index; - } - return prev_match; - } else { - return start + index; - } - } - } - - return best_match; - } - - /// Scalar (non-SIMD) fallback implementation - pub fn findScalar( - lines: []const i32, - columns: []const i32, - target_line: i32, - target_column: i32, - ) ?usize { - var index: usize = 0; - var count = lines.len; - - // Binary search through the data - while (count > 0) { - const step = count / 2; - const i = index + step; - - // Check if this mapping is before our target position - if (lines[i] < target_line or (lines[i] == target_line and columns[i] <= target_column)) { - index = i + 1; - count -= step + 1; - } else { - count = step; - } - } - - if (index > 0) { - // We want the last mapping that's <= our position - return index - 1; - } - - return null; - } - - /// Dispatcher that selects the best implementation based on architecture - pub fn find( - lines: []const i32, - columns: []const i32, - target_line: i32, - target_column: i32, - ) ?usize { - // Check for AVX2 support (x86_64) - if (@import("builtin").cpu.arch == .x86_64) { - return findX86_AVX2(lines, columns, target_line, target_column); - } - - // Check for NEON support (ARM) - if (@import("builtin").cpu.arch == .aarch64) { - return findARM_NEON(lines, columns, target_line, target_column); - } - - // Check for WASM SIMD support - if (@import("builtin").cpu.arch == .wasm32) { - return findWASM_SIMD(lines, columns, target_line, target_column); - } - - // Fallback to scalar implementation - return findScalar(lines, columns, target_line, target_column); - } - }; - - /// Delta-of-delta processor with SIMD acceleration - /// This is optimized for the new format where deltas are stored as delta-of-delta values - pub const DeltaOfDeltaProcessor = struct { - /// Process a block of delta-of-delta values with AVX2 SIMD - pub fn processSIMD( - dod_values: []const i32, - base_values: []i32, - results: []i32, - ) void { - const lanes = std.simd.suggestVectorLength(i32) orelse 1; - const len = @min(@min(dod_values.len, base_values.len), results.len); - - if (len < lanes) { - // Too small for SIMD, use scalar - return processScalar(dod_values, base_values, results); - } - - // First, accumulate deltas from delta-of-deltas - var i: usize = 0; - - // Use Vector types for SIMD operations - const Vector = @Vector(lanes, i32); - - while (i + lanes <= len) { - // Load delta-of-delta values - var dod_block: Vector = undefined; - for (0..lanes) |j| { - dod_block[j] = dod_values[i + j]; - } - - // Load accumulated delta values - var delta_block: Vector = undefined; - for (0..lanes) |j| { - delta_block[j] = base_values[i + j]; - } - - // Add delta-of-delta to get new delta values - const new_deltas = delta_block + dod_block; - - // Store results back - for (0..lanes) |j| { - results[i + j] = new_deltas[j]; - } - - i += lanes; - } - - // Process any remaining with scalar implementation - if (i < len) { - processScalar(dod_values[i..], base_values[i..], results[i..]); - } - } - - /// Scalar fallback implementation - pub fn processScalar( - dod_values: []const i32, - base_values: []i32, - results: []i32, - ) void { - const len = @min(@min(dod_values.len, base_values.len), results.len); - - for (0..len) |i| { - // Add delta-of-delta to previous delta to get new delta - results[i] = base_values[i] + dod_values[i]; - } - } - - /// Dispatcher that selects the best implementation based on architecture - pub fn process( - dod_values: []const i32, - base_values: []i32, - results: []i32, - ) void { - return processSIMD(dod_values, base_values, results); - } - }; -}; - -test "SIMDHelpers.SIMDSearch" { - const allocator = std.testing.allocator; - const TestCount = 1000; - - var lines = try allocator.alloc(i32, TestCount); - defer allocator.free(lines); - - var columns = try allocator.alloc(i32, TestCount); - defer allocator.free(columns); - - // Setup test data - sorted by line, then column - for (0..TestCount) |i| { - lines[i] = @intCast(i / 100); // Each line has 100 columns - columns[i] = @intCast(i % 100); - } - - // Test various target positions - const test_cases = [_]struct { line: i32, column: i32, expected: ?usize }{ - // Line 0, column 50 - .{ .line = 0, .column = 50, .expected = 50 }, - // Line 2, column 25 - .{ .line = 2, .column = 25, .expected = 225 }, - // Line 9, column 99 (last element) - .{ .line = 9, .column = 99, .expected = 999 }, - // Line 5, column 150 (column beyond range, should find line 5, column 99) - .{ .line = 5, .column = 150, .expected = 599 }, - // Line 11, column 0 (beyond range, should return null) - .{ .line = 11, .column = 0, .expected = null }, - }; - - for (test_cases) |tc| { - // Test scalar implementation for reference - const scalar_result = SIMDHelpers.SIMDSearch.findScalar(lines, columns, tc.line, tc.column); - try std.testing.expectEqual(tc.expected, scalar_result); - - // Test SIMD dispatcher (uses best available implementation) - const simd_result = SIMDHelpers.SIMDSearch.find(lines, columns, tc.line, tc.column); - try std.testing.expectEqual(tc.expected, simd_result); - } -} - -test "SIMDHelpers.DeltaOfDeltaProcessor" { - const allocator = std.testing.allocator; - const TestCount = 100; - - var dod_values = try allocator.alloc(i32, TestCount); - defer allocator.free(dod_values); - - var base_values = try allocator.alloc(i32, TestCount); - defer allocator.free(base_values); - - const results = try allocator.alloc(i32, TestCount); - defer allocator.free(results); - - var expected = try allocator.alloc(i32, TestCount); - defer allocator.free(expected); - - // Setup test data - for (0..TestCount) |i| { - dod_values[i] = @mod(@as(i32, @intCast(i)), 5) - 2; // Values between -2 and 2 - base_values[i] = @intCast(i * 2); // Some base values - } - - // Calculate expected results using scalar method - for (0..TestCount) |i| { - expected[i] = base_values[i] + dod_values[i]; - } - - // Test scalar implementation - std.mem.set(i32, results, 0); - SIMDHelpers.DeltaOfDeltaProcessor.processScalar(dod_values, base_values, results); - try std.testing.expectEqualSlices(i32, expected, results); - - // Test SIMD dispatcher - std.mem.set(i32, results, 0); - SIMDHelpers.DeltaOfDeltaProcessor.process(dod_values, base_values, results); - try std.testing.expectEqualSlices(i32, expected, results); -} diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index de405fc670..a60b27c976 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -257,6 +257,38 @@ pub const Mapping = struct { pub const List = bun.MultiArrayList(Mapping); + pub fn writeVLQs(mappings: List, writer: anytype) !void { + var last_col: i32 = 0; + var last_src: i32 = 0; + var last_ol: i32 = 0; + var last_oc: i32 = 0; + var current_line: i32 = 0; + for ( + mappings.items(.generated), + mappings.items(.original), + mappings.items(.source_index), + 0.., + ) |gen, orig, source_index, i| { + if (current_line != gen.lines) { + assert(gen.lines > current_line); + const inc = gen.lines - current_line; + try writer.writeByteNTimes(';', @intCast(inc)); + current_line = gen.lines; + last_col = 0; + } else if (i != 0) { + try writer.writeByte(','); + } + try vlq.encode(gen.columns - last_col).writeTo(writer); + last_col = gen.columns; + try vlq.encode(source_index - last_src).writeTo(writer); + last_src = source_index; + try vlq.encode(orig.lines - last_ol).writeTo(writer); + last_ol = orig.lines; + try vlq.encode(orig.columns - last_oc).writeTo(writer); + last_oc = orig.columns; + } + } + pub const Lookup = struct { mapping: Mapping, source_map: ?*ParsedSourceMap = null, @@ -653,7 +685,7 @@ pub const ParsedSourceMap = struct { is_standalone_module_graph: bool = false, /// If available, an optimized compact encoding using SIMD acceleration and delta encoding - compact_mapping: ?@import("compact.zig").CompactSourceMap = null, + compact_mapping: ?CompactSourceMap = null, pub usingnamespace bun.NewThreadSafeRefCounted(ParsedSourceMap, deinitFn, null); @@ -702,36 +734,23 @@ pub const ParsedSourceMap = struct { return @ptrFromInt(this.underlying_provider.data); } - pub fn writeVLQs(map: ParsedSourceMap, writer: anytype) !void { - var last_col: i32 = 0; - var last_src: i32 = 0; - var last_ol: i32 = 0; - var last_oc: i32 = 0; - var current_line: i32 = 0; - for ( - map.mappings.items(.generated), - map.mappings.items(.original), - map.mappings.items(.source_index), - 0.., - ) |gen, orig, source_index, i| { - if (current_line != gen.lines) { - assert(gen.lines > current_line); - const inc = gen.lines - current_line; - try writer.writeByteNTimes(';', @intCast(inc)); - current_line = gen.lines; - last_col = 0; - } else if (i != 0) { - try writer.writeByte(','); + pub fn find(map: *ParsedSourceMap, line: i32, column: i32) ?Mapping { + if (map.compact_mapping) |*compact_map| { + var stack_fallback = std.heap.stackFallback(1024, bun.default_allocator); + var arena = bun.ArenaAllocator.init(stack_fallback.get()); + defer arena.deinit(); + const allocator = arena.allocator(); + + if (compact_map.find(allocator, line, column) catch null) |mapping| { + return mapping; } - try vlq.encode(gen.columns - last_col).writeTo(writer); - last_col = gen.columns; - try vlq.encode(source_index - last_src).writeTo(writer); - last_src = source_index; - try vlq.encode(orig.lines - last_ol).writeTo(writer); - last_ol = orig.lines; - try vlq.encode(orig.columns - last_oc).writeTo(writer); - last_oc = orig.columns; } + + return Mapping.find(map.mappings, line, column); + } + + pub fn writeVLQs(map: *const ParsedSourceMap, writer: anytype) !void { + return Mapping.writeVLQs(map.mappings, writer); } pub fn formatVLQs(map: *const ParsedSourceMap) std.fmt.Formatter(formatVLQsImpl) { @@ -997,9 +1016,9 @@ pub fn find( // Use compact mapping if available (most efficient and memory-friendly) if (this.compact_mapping) |*compact_map| { // Use SIMD-optimized find when available - return compact_map.findSIMD(this.allocator, line, column) catch - // Fall back to standard VLQ search if compact search fails - return Mapping.find(this.mapping, line, column); + if (compact_map.find(this.allocator, line, column) catch null) |mapping| { + return mapping; + } } // Standard VLQ-based search if compact mapping not available @@ -1015,7 +1034,7 @@ pub fn ensureCompactMapping(this: *SourceMap) !void { if (this.mapping.len == 0) return; // Convert the standard mapping to compact format - const CompactSourceMap = @import("compact.zig"); + var compact = try CompactSourceMap.create(this.allocator); // Add all mappings from the standard format @@ -1283,7 +1302,7 @@ pub const Chunk = struct { should_ignore: bool = true, /// When using CompactBuilder, this field will contain the actual CompactSourceMap structure - compact_data: ?@import("compact.zig").CompactSourceMap = null, + compact_data: ?CompactSourceMap = null, pub const empty: Chunk = .{ .buffer = MutableString.initEmpty(bun.default_allocator), @@ -1506,9 +1525,6 @@ pub const Chunk = struct { prev_loc: Logger.Loc = Logger.Loc.Empty, has_prev_state: bool = false, - /// The context for the SourceMapFormat implementation - ctx: SourceMapper = undefined, - line_offset_table_byte_offset_list: []const u32 = &.{}, // This is a workaround for a bug in the popular "source-map" library: @@ -1533,15 +1549,12 @@ pub const Chunk = struct { b.updateGeneratedLineAndColumn(output); // Handle compact format specially - const CompactSourceMapFormat = @import("compact.zig").CompactSourceMapFormat; - var compact_data: ?@import("compact.zig").CompactSourceMap = null; + var compact_data: ?CompactSourceMap = null; - if (SourceMapFormatType == CompactSourceMapFormat) { + if (SourceMapFormatType == CompactSourceMap.Format) { // Just get the compact sourcemap directly - no VLQ generation compact_data = b.source_map.ctx.getCompactSourceMap() catch bun.outOfMemory(); - } - - if (b.prepend_count) { + } else if (b.prepend_count) { // Only applies to the standard VLQ format var buffer = b.source_map.getBuffer(); if (buffer.list.items.len >= 24) { @@ -1710,7 +1723,7 @@ pub const Chunk = struct { pub const Builder = NewBuilder(VLQSourceMap); /// Builder for compact sourcemap format - pub const CompactBuilder = NewBuilder(@import("compact.zig").Format); + pub const CompactBuilder = NewBuilder(CompactSourceMap.Format); }; /// https://sentry.engineering/blog/the-case-for-debug-ids @@ -1793,7 +1806,6 @@ pub fn fromChunk( // Convert to compact format if requested if (use_compact) { - const CompactSourceMap = @import("compact.zig"); var compact = try CompactSourceMap.create(allocator); // Add all mappings from the standard format @@ -1818,3 +1830,5 @@ pub fn fromChunk( return source_map; } + +pub const CompactSourceMap = @import("compact.zig");