1const Coverage = @This();
2
3const std = @import("../std.zig");
4const Io = std.Io;
5const Allocator = std.mem.Allocator;
6const Hash = std.hash.Wyhash;
7const Dwarf = std.debug.Dwarf;
8const assert = std.debug.assert;
9
10/// Provides a globally-scoped integer index for directories.
11///
12/// As opposed to, for example, a directory index that is compilation-unit
13/// scoped inside a single ELF module.
14///
15/// String memory references the memory-mapped debug information.
16///
17/// Protected by `mutex`.
18directories: std.array_hash_map.Custom(String, void, String.MapContext, false),
19/// Provides a globally-scoped integer index for files.
20///
21/// String memory references the memory-mapped debug information.
22///
23/// Protected by `mutex`.
24files: std.array_hash_map.Custom(File, void, File.MapContext, false),
25string_bytes: std.ArrayList(u8),
26/// Protects the other fields.
27mutex: Io.Mutex,
28
29pub const init: Coverage = .{
30 .directories = .empty,
31 .files = .empty,
32 .mutex = .init,
33 .string_bytes = .empty,
34};
35
36pub const String = enum(u32) {
37 _,
38
39 pub const MapContext = struct {
40 string_bytes: []const u8,
41
42 pub fn eql(self: @This(), a: String, b: String, b_index: usize) bool {
43 _ = b_index;
44 const a_slice = span(self.string_bytes[@backingInt(a)..]);
45 const b_slice = span(self.string_bytes[@backingInt(b)..]);
46 return std.mem.eql(u8, a_slice, b_slice);
47 }
48
49 pub fn hash(self: @This(), a: String) u32 {
50 return @truncate(Hash.hash(0, span(self.string_bytes[@backingInt(a)..])));
51 }
52 };
53
54 pub const SliceAdapter = struct {
55 string_bytes: []const u8,
56
57 pub fn eql(self: @This(), a_slice: []const u8, b: String, b_index: usize) bool {
58 _ = b_index;
59 const b_slice = span(self.string_bytes[@backingInt(b)..]);
60 return std.mem.eql(u8, a_slice, b_slice);
61 }
62 pub fn hash(self: @This(), a: []const u8) u32 {
63 _ = self;
64 return @truncate(Hash.hash(0, a));
65 }
66 };
67};
68
69pub const SourceLocation = extern struct {
70 file: File.Index,
71 line: u32,
72 column: u32,
73
74 pub const invalid: SourceLocation = .{
75 .file = .invalid,
76 .line = 0,
77 .column = 0,
78 };
79};
80
81pub const File = extern struct {
82 directory_index: u32,
83 basename: String,
84
85 pub const Index = enum(u32) {
86 invalid = std.math.maxInt(u32),
87 _,
88 };
89
90 pub const MapContext = struct {
91 string_bytes: []const u8,
92
93 pub fn hash(self: MapContext, a: File) u32 {
94 const a_basename = span(self.string_bytes[@backingInt(a.basename)..]);
95 return @truncate(Hash.hash(a.directory_index, a_basename));
96 }
97
98 pub fn eql(self: MapContext, a: File, b: File, b_index: usize) bool {
99 _ = b_index;
100 if (a.directory_index != b.directory_index) return false;
101 const a_basename = span(self.string_bytes[@backingInt(a.basename)..]);
102 const b_basename = span(self.string_bytes[@backingInt(b.basename)..]);
103 return std.mem.eql(u8, a_basename, b_basename);
104 }
105 };
106
107 pub const SliceAdapter = struct {
108 string_bytes: []const u8,
109
110 pub const Entry = struct {
111 directory_index: u32,
112 basename: []const u8,
113 };
114
115 pub fn hash(self: @This(), a: Entry) u32 {
116 _ = self;
117 return @truncate(Hash.hash(a.directory_index, a.basename));
118 }
119
120 pub fn eql(self: @This(), a: Entry, b: File, b_index: usize) bool {
121 _ = b_index;
122 if (a.directory_index != b.directory_index) return false;
123 const b_basename = span(self.string_bytes[@backingInt(b.basename)..]);
124 return std.mem.eql(u8, a.basename, b_basename);
125 }
126 };
127};
128
129pub fn deinit(cov: *Coverage, gpa: Allocator) void {
130 cov.directories.deinit(gpa);
131 cov.files.deinit(gpa);
132 cov.string_bytes.deinit(gpa);
133 cov.* = undefined;
134}
135
136pub fn fileAt(cov: *Coverage, index: File.Index) *File {
137 return &cov.files.keys()[@backingInt(index)];
138}
139
140pub fn stringAt(cov: *Coverage, index: String) [:0]const u8 {
141 return span(cov.string_bytes.items[@backingInt(index)..]);
142}
143
144pub const ResolveAddressesDwarfError = Dwarf.ScanError || Io.Cancelable;
145
146pub fn resolveAddressesDwarf(
147 cov: *Coverage,
148 gpa: Allocator,
149 io: Io,
150 endian: std.builtin.Endian,
151 /// Asserts the addresses are in ascending order.
152 sorted_pc_addrs: []const u64,
153 /// Asserts its length equals length of `sorted_pc_addrs`.
154 output: []SourceLocation,
155 d: *Dwarf,
156) ResolveAddressesDwarfError!void {
157 assert(sorted_pc_addrs.len == output.len);
158 assert(d.ranges.items.len != 0); // call `populateRanges` first.
159
160 var range_i: usize = 0;
161 var range: *std.debug.Dwarf.Range = &d.ranges.items[0];
162 var line_table_i: usize = undefined;
163 var prev_pc: u64 = 0;
164 var prev_cu: ?*std.debug.Dwarf.CompileUnit = null;
165 // Protects directories and files tables from other threads.
166 try cov.mutex.lock(io);
167 defer cov.mutex.unlock(io);
168 next_pc: for (sorted_pc_addrs, output) |pc, *out| {
169 assert(pc >= prev_pc);
170 prev_pc = pc;
171
172 while (pc >= range.end) {
173 range_i += 1;
174 if (range_i >= d.ranges.items.len) {
175 out.* = SourceLocation.invalid;
176 continue :next_pc;
177 }
178 range = &d.ranges.items[range_i];
179 }
180 if (pc < range.start) {
181 out.* = SourceLocation.invalid;
182 continue :next_pc;
183 }
184 const cu = &d.compile_unit_list.items[range.compile_unit_index];
185 if (cu != prev_cu) {
186 prev_cu = cu;
187 if (cu.src_loc_cache == null) {
188 cov.mutex.unlock(io);
189 defer cov.mutex.lockUncancelable(io);
190 d.populateSrcLocCache(gpa, endian, cu) catch |err| switch (err) {
191 error.MissingDebugInfo, error.InvalidDebugInfo => {
192 out.* = SourceLocation.invalid;
193 continue :next_pc;
194 },
195 else => |e| return e,
196 };
197 }
198 const slc = &cu.src_loc_cache.?;
199 const table_addrs = slc.line_table.keys();
200 line_table_i = std.sort.upperBound(u64, table_addrs, pc, struct {
201 fn order(context: u64, item: u64) std.math.Order {
202 return std.math.order(context, item);
203 }
204 }.order);
205 }
206 const slc = &cu.src_loc_cache.?;
207 const table_addrs = slc.line_table.keys();
208 while (line_table_i < table_addrs.len and table_addrs[line_table_i] <= pc) line_table_i += 1;
209
210 const entry = slc.line_table.values()[line_table_i - 1];
211 const corrected_file_index = entry.file - @intFromBool(slc.version < 5);
212 const file_entry = slc.files[corrected_file_index];
213 const dir_path = slc.directories[file_entry.dir_index].path;
214 try cov.string_bytes.ensureUnusedCapacity(gpa, dir_path.len + file_entry.path.len + 2);
215 const dir_gop = try cov.directories.getOrPutContextAdapted(gpa, dir_path, String.SliceAdapter{
216 .string_bytes = cov.string_bytes.items,
217 }, String.MapContext{
218 .string_bytes = cov.string_bytes.items,
219 });
220 if (!dir_gop.found_existing)
221 dir_gop.key_ptr.* = addStringAssumeCapacity(cov, dir_path);
222 const file_gop = try cov.files.getOrPutContextAdapted(gpa, File.SliceAdapter.Entry{
223 .directory_index = @intCast(dir_gop.index),
224 .basename = file_entry.path,
225 }, File.SliceAdapter{
226 .string_bytes = cov.string_bytes.items,
227 }, File.MapContext{
228 .string_bytes = cov.string_bytes.items,
229 });
230 if (!file_gop.found_existing) file_gop.key_ptr.* = .{
231 .directory_index = @intCast(dir_gop.index),
232 .basename = addStringAssumeCapacity(cov, file_entry.path),
233 };
234 out.* = .{
235 .file = @fromBackingInt(@intCast(file_gop.index)),
236 .line = entry.line,
237 .column = entry.column,
238 };
239 }
240}
241
242pub fn addStringAssumeCapacity(cov: *Coverage, s: []const u8) String {
243 const result: String = @fromBackingInt(@intCast(cov.string_bytes.items.len));
244 cov.string_bytes.appendSliceAssumeCapacity(s);
245 cov.string_bytes.appendAssumeCapacity(0);
246 return result;
247}
248
249fn span(s: []const u8) [:0]const u8 {
250 return std.mem.sliceTo(@as([:0]const u8, @ptrCast(s)), 0);
251}