Files
bun.sh/src/sourcemap/CodeCoverage.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

732 lines
28 KiB
Zig

const LinesHits = bun.collections.BabyList(u32);
/// Our code coverage currently only deals with lines of code, not statements or branches.
/// JSC doesn't expose function names in their coverage data, so we don't include that either :(.
/// Since we only need to store line numbers, our job gets simpler
///
/// We can use two bitsets to store code coverage data for a given file
/// 1. executable_lines
/// 2. lines_which_have_executed
///
/// Not all lines of code are executable. Comments, whitespace, empty lines, etc. are not executable.
/// It's not a problem for anyone if comments, whitespace, empty lines etc are not executed, so those should always be omitted from coverage reports
///
/// We use two bitsets since the typical size will be decently small,
/// bitsets are simple and bitsets are relatively fast to construct and query
///
pub const Report = struct {
source_url: bun.jsc.ZigString.Slice,
executable_lines: Bitset,
lines_which_have_executed: Bitset,
line_hits: LinesHits = .{},
functions: std.ArrayListUnmanaged(Block),
functions_which_have_executed: Bitset,
stmts_which_have_executed: Bitset,
stmts: std.ArrayListUnmanaged(Block),
total_lines: u32 = 0,
pub fn linesCoverageFraction(this: *const Report) f64 {
var intersected = bun.handleOom(this.executable_lines.clone(bun.default_allocator));
defer intersected.deinit(bun.default_allocator);
intersected.setIntersection(this.lines_which_have_executed);
const total_count: f64 = @floatFromInt(this.executable_lines.count());
if (total_count == 0) {
return 1.0;
}
const intersected_count: f64 = @floatFromInt(intersected.count());
return (intersected_count / total_count);
}
pub fn stmtsCoverageFraction(this: *const Report) f64 {
const total_count: f64 = @floatFromInt(this.stmts.items.len);
if (total_count == 0) {
return 1.0;
}
return ((@as(f64, @floatFromInt(this.stmts_which_have_executed.count()))) / (total_count));
}
pub fn functionCoverageFraction(this: *const Report) f64 {
const total_count: f64 = @floatFromInt(this.functions.items.len);
if (total_count == 0) {
return 1.0;
}
return (@as(f64, @floatFromInt(this.functions_which_have_executed.count())) / total_count);
}
pub const Text = struct {
pub fn writeFormatWithValues(
filename: []const u8,
max_filename_length: usize,
vals: Fraction,
failing: Fraction,
failed: bool,
writer: *std.Io.Writer,
indent_name: bool,
comptime enable_colors: bool,
) !void {
if (comptime enable_colors) {
if (failed) {
try writer.writeAll(comptime prettyFmt("<r><b><red>", true));
} else {
try writer.writeAll(comptime prettyFmt("<r><b><green>", true));
}
}
if (indent_name) {
try writer.writeAll(" ");
}
try writer.writeAll(filename);
try writer.splatByteAll(' ', (max_filename_length - filename.len + @as(usize, @intFromBool(!indent_name))));
try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
if (comptime enable_colors) {
if (vals.functions < failing.functions) {
try writer.writeAll(comptime prettyFmt("<b><red>", true));
} else {
try writer.writeAll(comptime prettyFmt("<b><green>", true));
}
}
try writer.print("{d: >7.2}", .{vals.functions * 100.0});
// try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
// if (comptime enable_colors) {
// // if (vals.stmts < failing.stmts) {
// try writer.writeAll(comptime prettyFmt("<d>", true));
// // } else {
// // try writer.writeAll(comptime prettyFmt("<d>", true));
// // }
// }
// try writer.print("{d: >8.2}", .{vals.stmts * 100.0});
try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
if (comptime enable_colors) {
if (vals.lines < failing.lines) {
try writer.writeAll(comptime prettyFmt("<b><red>", true));
} else {
try writer.writeAll(comptime prettyFmt("<b><green>", true));
}
}
try writer.print("{d: >7.2}", .{vals.lines * 100.0});
}
pub fn writeFormat(
report: *const Report,
max_filename_length: usize,
fraction: *Fraction,
base_path: []const u8,
writer: *std.Io.Writer,
comptime enable_colors: bool,
) !void {
const failing = fraction.*;
const fns = report.functionCoverageFraction();
const lines = report.linesCoverageFraction();
const stmts = report.stmtsCoverageFraction();
fraction.functions = fns;
fraction.lines = lines;
fraction.stmts = stmts;
const failed = fns < failing.functions or lines < failing.lines; // or stmts < failing.stmts;
fraction.failing = failed;
var filename = report.source_url.slice();
if (base_path.len > 0) {
filename = bun.path.relative(base_path, filename);
}
try writeFormatWithValues(
filename,
max_filename_length,
fraction.*,
failing,
failed,
writer,
true,
enable_colors,
);
try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
var executable_lines_that_havent_been_executed = bun.handleOom(report.lines_which_have_executed.clone(bun.default_allocator));
defer executable_lines_that_havent_been_executed.deinit(bun.default_allocator);
executable_lines_that_havent_been_executed.toggleAll();
// This sets statements in executed scopes
executable_lines_that_havent_been_executed.setIntersection(report.executable_lines);
var iter = executable_lines_that_havent_been_executed.iterator(.{});
var start_of_line_range: usize = 0;
var prev_line: usize = 0;
var is_first = true;
while (iter.next()) |next_line| {
if (next_line == (prev_line + 1)) {
prev_line = next_line;
continue;
} else if (is_first and start_of_line_range == 0 and prev_line == 0) {
start_of_line_range = next_line;
prev_line = next_line;
continue;
}
if (is_first) {
is_first = false;
} else {
try writer.print(comptime prettyFmt("<r><d>,<r>", enable_colors), .{});
}
if (start_of_line_range == prev_line) {
try writer.print(comptime prettyFmt("<red>{d}", enable_colors), .{start_of_line_range + 1});
} else {
try writer.print(comptime prettyFmt("<red>{d}-{d}", enable_colors), .{ start_of_line_range + 1, prev_line + 1 });
}
prev_line = next_line;
start_of_line_range = next_line;
}
if (prev_line != start_of_line_range) {
if (is_first) {
is_first = false;
} else {
try writer.print(comptime prettyFmt("<r><d>,<r>", enable_colors), .{});
}
if (start_of_line_range == prev_line) {
try writer.print(comptime prettyFmt("<red>{d}", enable_colors), .{start_of_line_range + 1});
} else {
try writer.print(comptime prettyFmt("<red>{d}-{d}", enable_colors), .{ start_of_line_range + 1, prev_line + 1 });
}
}
}
};
pub const Lcov = struct {
pub fn writeFormat(
report: *const Report,
base_path: []const u8,
writer: *std.Io.Writer,
) !void {
var filename = report.source_url.slice();
if (base_path.len > 0) {
filename = bun.path.relative(base_path, filename);
}
// TN: test name
// Empty value appears fine. For example, `TN:`.
try writer.writeAll("TN:\n");
// SF: Source File path
// For example, `SF:path/to/source.ts`
try writer.print("SF:{s}\n", .{filename});
// ** Per-function coverage not supported yet, since JSC does not support function names yet. **
// FN: line number,function name
// FNF: functions found
try writer.print("FNF:{d}\n", .{report.functions.items.len});
// FNH: functions hit
try writer.print("FNH:{d}\n", .{report.functions_which_have_executed.count()});
// ** Track all executable lines **
// Executable lines that were not hit should be marked as 0
var executable_lines = bun.handleOom(report.executable_lines.clone(bun.default_allocator));
defer executable_lines.deinit(bun.default_allocator);
var iter = executable_lines.iterator(.{});
// ** Branch coverage not supported yet, since JSC does not support those yet. ** //
// BRDA: line, block, (expressions,count)+
// BRF: branches found
// BRH: branches hit
const line_hits = report.line_hits.slice();
while (iter.next()) |line| {
// DA: line number, hit count
try writer.print("DA:{d},{d}\n", .{ line + 1, line_hits[line] });
}
// LF: lines found
try writer.print("LF:{d}\n", .{report.executable_lines.count()});
// LH: lines hit
try writer.print("LH:{d}\n", .{report.lines_which_have_executed.count()});
try writer.writeAll("end_of_record\n");
}
};
pub fn deinit(this: *Report, allocator: std.mem.Allocator) void {
this.executable_lines.deinit(allocator);
this.lines_which_have_executed.deinit(allocator);
this.line_hits.deinit(allocator);
this.functions.deinit(allocator);
this.stmts.deinit(allocator);
this.functions_which_have_executed.deinit(allocator);
this.stmts_which_have_executed.deinit(allocator);
}
extern fn CodeCoverage__withBlocksAndFunctions(
*bun.jsc.VM,
i32,
*anyopaque,
bool,
*const fn (
*Generator,
[*]const BasicBlockRange,
usize,
usize,
bool,
) callconv(.c) void,
) bool;
const Generator = struct {
allocator: std.mem.Allocator,
byte_range_mapping: *ByteRangeMapping,
result: *?Report,
pub fn do(
this: *@This(),
blocks_ptr: [*]const BasicBlockRange,
blocks_len: usize,
function_start_offset: usize,
ignore_sourcemap: bool,
) callconv(.c) void {
const blocks: []const BasicBlockRange = blocks_ptr[0..function_start_offset];
var function_blocks: []const BasicBlockRange = blocks_ptr[function_start_offset..blocks_len];
if (function_blocks.len > 1) {
function_blocks = function_blocks[1..];
}
if (blocks.len == 0) {
return;
}
this.result.* = this.byte_range_mapping.generateReportFromBlocks(
this.allocator,
this.byte_range_mapping.source_url,
blocks,
function_blocks,
ignore_sourcemap,
) catch null;
}
};
pub fn generate(
globalThis: *bun.jsc.JSGlobalObject,
allocator: std.mem.Allocator,
byte_range_mapping: *ByteRangeMapping,
ignore_sourcemap_: bool,
) ?Report {
bun.jsc.markBinding(@src());
const vm = globalThis.vm();
var result: ?Report = null;
var generator = Generator{
.result = &result,
.allocator = allocator,
.byte_range_mapping = byte_range_mapping,
};
if (!CodeCoverage__withBlocksAndFunctions(
vm,
byte_range_mapping.source_id,
&generator,
ignore_sourcemap_,
&Generator.do,
)) {
return null;
}
return result;
}
};
const BasicBlockRange = extern struct {
startOffset: c_int = 0,
endOffset: c_int = 0,
hasExecuted: bool = false,
executionCount: usize = 0,
};
pub const ByteRangeMapping = struct {
line_offset_table: LineOffsetTable.List = .{},
source_id: i32,
source_url: bun.jsc.ZigString.Slice,
pub fn isLessThan(_: void, a: ByteRangeMapping, b: ByteRangeMapping) bool {
return bun.strings.order(a.source_url.slice(), b.source_url.slice()) == .lt;
}
pub const HashMap = std.HashMap(u64, ByteRangeMapping, bun.IdentityContext(u64), std.hash_map.default_max_load_percentage);
pub fn deinit(this: *ByteRangeMapping) void {
this.line_offset_table.deinit(bun.default_allocator);
}
pub threadlocal var map: ?*HashMap = null;
pub fn generate(str: bun.String, source_contents_str: bun.String, source_id: i32) callconv(.c) void {
var _map = map orelse brk: {
map = bun.handleOom(bun.jsc.VirtualMachine.get().allocator.create(HashMap));
map.?.* = HashMap.init(bun.jsc.VirtualMachine.get().allocator);
break :brk map.?;
};
var slice = str.toUTF8(bun.default_allocator);
const hash = bun.hash(slice.slice());
var entry = bun.handleOom(_map.getOrPut(hash));
if (entry.found_existing) {
entry.value_ptr.deinit();
}
var source_contents = source_contents_str.toUTF8(bun.default_allocator);
defer source_contents.deinit();
entry.value_ptr.* = compute(source_contents.slice(), source_id, slice);
}
pub fn getSourceID(this: *ByteRangeMapping) callconv(.c) i32 {
return this.source_id;
}
pub fn find(path: bun.String) callconv(.c) ?*ByteRangeMapping {
var slice = path.toUTF8(bun.default_allocator);
defer slice.deinit();
var map_ = map orelse return null;
const hash = bun.hash(slice.slice());
const entry = map_.getPtr(hash) orelse return null;
return entry;
}
pub fn generateReportFromBlocks(
this: *ByteRangeMapping,
allocator: std.mem.Allocator,
source_url: bun.jsc.ZigString.Slice,
blocks: []const BasicBlockRange,
function_blocks: []const BasicBlockRange,
ignore_sourcemap: bool,
) !Report {
const line_starts = this.line_offset_table.items(.byte_offset_to_start_of_line);
var executable_lines: Bitset = Bitset{};
var lines_which_have_executed: Bitset = Bitset{};
const parsed_mappings_ = bun.jsc.VirtualMachine.get().source_mappings.get(source_url.slice());
defer if (parsed_mappings_) |parsed_mapping| parsed_mapping.deref();
var line_hits = LinesHits{};
var functions = std.ArrayListUnmanaged(Block){};
try functions.ensureTotalCapacityPrecise(allocator, function_blocks.len);
errdefer functions.deinit(allocator);
var functions_which_have_executed: Bitset = try Bitset.initEmpty(allocator, function_blocks.len);
errdefer functions_which_have_executed.deinit(allocator);
var stmts_which_have_executed: Bitset = try Bitset.initEmpty(allocator, blocks.len);
errdefer stmts_which_have_executed.deinit(allocator);
var stmts = std.ArrayListUnmanaged(Block){};
try stmts.ensureTotalCapacityPrecise(allocator, function_blocks.len);
errdefer stmts.deinit(allocator);
errdefer executable_lines.deinit(allocator);
errdefer lines_which_have_executed.deinit(allocator);
var line_count: u32 = 0;
if (ignore_sourcemap or parsed_mappings_ == null) {
line_count = @truncate(line_starts.len);
executable_lines = try Bitset.initEmpty(allocator, line_count);
lines_which_have_executed = try Bitset.initEmpty(allocator, line_count);
line_hits = try LinesHits.initCapacity(allocator, line_count);
line_hits.len = line_count;
const line_hits_slice = line_hits.slice();
@memset(line_hits_slice, 0);
errdefer line_hits.deinit(allocator);
for (blocks, 0..) |block, i| {
if (block.endOffset < 0 or block.startOffset < 0) continue; // does not map to anything
const min: usize = @intCast(@min(block.startOffset, block.endOffset));
const max: usize = @intCast(@max(block.startOffset, block.endOffset));
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
const has_executed = block.hasExecuted or block.executionCount > 0;
for (min..max) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
continue;
}
const line: u32 = @intCast(new_line_index);
min_line = @min(min_line, line);
max_line = @max(max_line, line);
executable_lines.set(line);
if (has_executed) {
lines_which_have_executed.set(line);
line_hits_slice[line] += 1;
}
}
if (min_line != std.math.maxInt(u32)) {
if (has_executed)
stmts_which_have_executed.set(i);
try stmts.append(allocator, .{
.start_line = min_line,
.end_line = max_line,
});
}
}
for (function_blocks, 0..) |function, i| {
if (function.endOffset < 0 or function.startOffset < 0) continue; // does not map to anything
const min: usize = @intCast(@min(function.startOffset, function.endOffset));
const max: usize = @intCast(@max(function.startOffset, function.endOffset));
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
for (min..max) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
continue;
}
const line: u32 = @intCast(new_line_index);
min_line = @min(min_line, line);
max_line = @max(max_line, line);
}
const did_fn_execute = function.executionCount > 0 or function.hasExecuted;
// only mark the lines as executable if the function has not executed
// functions that have executed have non-executable lines in them and thats fine.
if (!did_fn_execute) {
const end = @min(max_line, line_count);
@memset(line_hits_slice[min_line..end], 0);
for (min_line..end) |line| {
executable_lines.set(line);
lines_which_have_executed.unset(line);
}
}
try functions.append(allocator, .{
.start_line = min_line,
.end_line = max_line,
});
if (did_fn_execute)
functions_which_have_executed.set(i);
}
} else if (parsed_mappings_) |parsed_mapping| {
line_count = @as(u32, @truncate(parsed_mapping.input_line_count)) + 1;
executable_lines = try Bitset.initEmpty(allocator, line_count);
lines_which_have_executed = try Bitset.initEmpty(allocator, line_count);
line_hits = try LinesHits.initCapacity(allocator, line_count);
line_hits.len = line_count;
const line_hits_slice = line_hits.slice();
@memset(line_hits_slice, 0);
errdefer line_hits.deinit(allocator);
for (blocks, 0..) |block, i| {
if (block.endOffset < 0 or block.startOffset < 0) continue; // does not map to anything
const min: usize = @intCast(@min(block.startOffset, block.endOffset));
const max: usize = @intCast(@max(block.startOffset, block.endOffset));
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
const has_executed = block.hasExecuted or block.executionCount > 0;
for (min..max) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
continue;
}
const column_position = byte_offset -| line_start_byte_offset;
if (parsed_mapping.mappings.find(.fromZeroBased(@intCast(new_line_index)), .fromZeroBased(@intCast(column_position)))) |*point| {
if (point.original.lines.zeroBased() < 0) continue;
const line: u32 = @as(u32, @intCast(point.original.lines.zeroBased()));
executable_lines.set(line);
if (has_executed) {
lines_which_have_executed.set(line);
line_hits_slice[line] += 1;
}
min_line = @min(min_line, line);
max_line = @max(max_line, line);
}
}
if (min_line != std.math.maxInt(u32)) {
try stmts.append(allocator, .{
.start_line = min_line,
.end_line = max_line,
});
if (has_executed)
stmts_which_have_executed.set(i);
}
}
for (function_blocks, 0..) |function, i| {
if (function.endOffset < 0 or function.startOffset < 0) continue; // does not map to anything
const min: usize = @intCast(@min(function.startOffset, function.endOffset));
const max: usize = @intCast(@max(function.startOffset, function.endOffset));
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
for (min..max) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
continue;
}
const column_position = byte_offset -| line_start_byte_offset;
if (parsed_mapping.mappings.find(.fromZeroBased(@intCast(new_line_index)), .fromZeroBased(@intCast(column_position)))) |point| {
if (point.original.lines.zeroBased() < 0) continue;
const line: u32 = @as(u32, @intCast(point.original.lines.zeroBased()));
min_line = @min(min_line, line);
max_line = @max(max_line, line);
}
}
// no sourcemaps? ignore it
if (min_line == std.math.maxInt(u32) and max_line == 0) {
continue;
}
const did_fn_execute = function.executionCount > 0 or function.hasExecuted;
// only mark the lines as executable if the function has not executed
// functions that have executed have non-executable lines in them and thats fine.
if (!did_fn_execute) {
const end = @min(max_line, line_count);
for (min_line..end) |line| {
executable_lines.set(line);
lines_which_have_executed.unset(line);
line_hits_slice[line] = 0;
}
}
try functions.append(allocator, .{
.start_line = min_line,
.end_line = max_line,
});
if (did_fn_execute)
functions_which_have_executed.set(i);
}
} else {
unreachable;
}
return .{
.source_url = source_url,
.functions = functions,
.executable_lines = executable_lines,
.lines_which_have_executed = lines_which_have_executed,
.line_hits = line_hits,
.total_lines = line_count,
.stmts = stmts,
.functions_which_have_executed = functions_which_have_executed,
.stmts_which_have_executed = stmts_which_have_executed,
};
}
pub fn findExecutedLines(
globalThis: *bun.jsc.JSGlobalObject,
source_url: bun.String,
blocks_ptr: [*]const BasicBlockRange,
blocks_len: usize,
function_start_offset: usize,
ignore_sourcemap: bool,
) callconv(.c) bun.jsc.JSValue {
var this = ByteRangeMapping.find(source_url) orelse return bun.jsc.JSValue.null;
const blocks: []const BasicBlockRange = blocks_ptr[0..function_start_offset];
var function_blocks: []const BasicBlockRange = blocks_ptr[function_start_offset..blocks_len];
if (function_blocks.len > 1) {
function_blocks = function_blocks[1..];
}
var url_slice = source_url.toUTF8(bun.default_allocator);
defer url_slice.deinit();
var report = this.generateReportFromBlocks(bun.default_allocator, url_slice, blocks, function_blocks, ignore_sourcemap) catch {
return globalThis.throwOutOfMemoryValue();
};
defer report.deinit(bun.default_allocator);
var coverage_fraction = Fraction{};
var allocating_writer = std.Io.Writer.Allocating.init(bun.default_allocator);
defer allocating_writer.deinit();
const buffered_writer = &allocating_writer.writer;
Report.Text.writeFormat(&report, source_url.utf8ByteLength(), &coverage_fraction, "", buffered_writer, false) catch {
return globalThis.throwOutOfMemoryValue();
};
buffered_writer.flush() catch {
return globalThis.throwOutOfMemoryValue();
};
return bun.String.createUTF8ForJS(globalThis, allocating_writer.written()) catch return .zero;
}
pub fn compute(source_contents: []const u8, source_id: i32, source_url: bun.jsc.ZigString.Slice) ByteRangeMapping {
return ByteRangeMapping{
.line_offset_table = LineOffsetTable.generate(bun.jsc.VirtualMachine.get().allocator, source_contents, 0),
.source_id = source_id,
.source_url = source_url,
};
}
};
comptime {
if (bun.Environment.isNative) {
@export(&ByteRangeMapping.generate, .{ .name = "ByteRangeMapping__generate" });
@export(&ByteRangeMapping.findExecutedLines, .{ .name = "ByteRangeMapping__findExecutedLines" });
@export(&ByteRangeMapping.find, .{ .name = "ByteRangeMapping__find" });
@export(&ByteRangeMapping.getSourceID, .{ .name = "ByteRangeMapping__getSourceID" });
}
}
pub const Fraction = struct {
functions: f64 = 0.9,
lines: f64 = 0.9,
// This metric is less accurate right now
stmts: f64 = 0.75,
failing: bool = false,
};
pub const Block = struct {
start_line: u32 = 0,
end_line: u32 = 0,
};
const std = @import("std");
const bun = @import("bun");
const Bitset = bun.bit_set.DynamicBitSetUnmanaged;
const LineOffsetTable = bun.SourceMap.LineOffsetTable;
const Output = bun.Output;
const prettyFmt = Output.prettyFmt;