mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 11:29:02 +00:00
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>
732 lines
28 KiB
Zig
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;
|