lib/coz/src/debug.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3
4 const path_filter = @import("filter.zig");
5 const source_map = @import("map.zig");
6
7 pub const default_source_scope = [_][]const u8{"%"};
8
9 pub const Scope = struct {
10 patterns: []const []const u8 = &default_source_scope,
11 allow_system_sources: bool = true,
12 };
13
14 pub const ResolveError = error{UnsupportedTarget} ||
15 std.debug.SelfInfoError ||
16 std.mem.Allocator.Error;
17
18 pub fn resolveSelfAddress(
19 allocator: std.mem.Allocator,
20 map: *source_map.Index,
21 address: usize,
22 scope: Scope,
23 ) ResolveError!?*source_map.Line {
24 if (comptime std.debug.SelfInfo == void) return error.UnsupportedTarget;
25 return resolveAddress(allocator, map, try std.debug.getSelfDebugInfo(), address, scope);
26 }
27
28 pub fn resolveAddress(
29 allocator: std.mem.Allocator,
30 map: *source_map.Index,
31 debug_info: *std.debug.SelfInfo,
32 address: usize,
33 scope: Scope,
34 ) ResolveError!?*source_map.Line {
35 if (comptime std.debug.SelfInfo == void) {
36 return error.UnsupportedTarget;
37 }
38
39 if (map.findLineByAddress(address)) |line| return line;
40 if (map.isUnresolved(address)) return null;
41
42 const scratch = sys.allocator.debugInfoScratchAllocator();
43 var text_arena_state = std.heap.ArenaAllocator.init(scratch);
44 defer text_arena_state.deinit();
45
46 var symbols = try std.ArrayList(std.debug.Symbol).initCapacity(scratch, 1);
47 defer symbols.deinit(scratch);
48
49 try debug_info.getSymbols(
50 sys.stdio.debugIo(),
51 scratch,
52 text_arena_state.allocator(),
53 address,
54 true,
55 &symbols,
56 );
57
58 const line = try resolveSymbols(allocator, map, address, symbols.items, scope);
59 if (line == null) map.markUnresolved(allocator, address) catch {};
60 return line;
61 }
62
63 pub fn resolveSymbols(
64 allocator: std.mem.Allocator,
65 map: *source_map.Index,
66 address: usize,
67 symbols: []const std.debug.Symbol,
68 scope: Scope,
69 ) std.mem.Allocator.Error!?*source_map.Line {
70 for (symbols) |symbol| {
71 const location = symbol.source_location orelse continue;
72 if (try resolveLocation(allocator, map, address, location, scope)) |line| return line;
73 }
74
75 return null;
76 }
77
78 pub fn resolveLocation(
79 allocator: std.mem.Allocator,
80 map: *source_map.Index,
81 address: usize,
82 location: std.debug.SourceLocation,
83 scope: Scope,
84 ) std.mem.Allocator.Error!?*source_map.Line {
85 if (location.line == 0) return null;
86 const in_scope = path_filter.fileMatchesScopeNormalized(location.file_name, scope.patterns, scope.allow_system_sources);
87 if (!in_scope) return null;
88 return try map.addRange(allocator, location.file_name, location.line, source_map.Interval.unit(address));
89 }
90
91 pub fn resolveSample(
92 allocator: std.mem.Allocator,
93 map: *source_map.Index,
94 sample: source_map.Sample,
95 scope: Scope,
96 ) ResolveError!void {
97 _ = try resolveSelfAddress(allocator, map, sample.ip, scope);
98 for (sample.callchain) |pc| {
99 _ = try resolveSelfAddress(allocator, map, pc -| 1, scope);
100 }
101 }
102
103 test "self address resolution never allocates resolution scratch from the caller" {
104 if (comptime std.debug.SelfInfo == void) return error.SkipZigTest;
105
106 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
107 var map: source_map.Index = .{};
108 defer map.deinit(failing.allocator());
109
110 const patterns = [_][]const u8{"/coz-scratch-test-no-such-scope/%"};
111 const line = resolveSelfAddress(
112 failing.allocator(),
113 &map,
114 @returnAddress(),
115 .{ .patterns = &patterns },
116 ) catch |err| switch (err) {
117 error.OutOfMemory => return error.ResolutionScratchUsedCallerAllocator,
118 else => return error.SkipZigTest,
119 };
120
121 try std.testing.expect(line == null);
122 try std.testing.expectEqual(@as(usize, 0), map.ranges.items.len);
123 }
124
125 test "self address resolution memoizes unresolvable addresses" {
126 if (comptime std.debug.SelfInfo == void) return error.SkipZigTest;
127
128 var map: source_map.Index = .{};
129 defer map.deinit(std.testing.allocator);
130
131 const patterns = [_][]const u8{"/coz-memo-test-no-such-scope/%"};
132 const address = @returnAddress();
133 const line = resolveSelfAddress(std.testing.allocator, &map, address, .{ .patterns = &patterns }) catch |err| switch (err) {
134 error.OutOfMemory => return err,
135 else => return error.SkipZigTest,
136 };
137 try std.testing.expect(line == null);
138 try std.testing.expect(map.isUnresolved(address));
139
140 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
141 const memoized = try resolveSelfAddress(failing.allocator(), &map, address, .{ .patterns = &patterns });
142 try std.testing.expect(memoized == null);
143 }
144
145 test "debug symbols resolve source locations into the source map" {
146 var map: source_map.Index = .{};
147 defer map.deinit(std.testing.allocator);
148
149 const symbols = [_]std.debug.Symbol{.{
150 .name = "sample",
151 .compile_unit_name = "unit",
152 .source_location = .{
153 .file_name = "/tmp/app/main.zig",
154 .line = 42,
155 .column = 7,
156 },
157 }};
158 const line = (try resolveSymbols(std.testing.allocator, &map, 0x1000, &symbols, .{})).?;
159
160 try std.testing.expectEqualStrings("/tmp/app/main.zig", line.file.name);
161 try std.testing.expectEqual(@as(u64, 42), line.number);
162 try std.testing.expectEqual(line, map.findLineByAddress(0x1000).?);
163 }
164
165 test "debug symbol resolution honors source scope filtering" {
166 var map: source_map.Index = .{};
167 defer map.deinit(std.testing.allocator);
168
169 const patterns = [_][]const u8{"/definitely/not/coz/%"};
170 const line = try resolveLocation(
171 std.testing.allocator,
172 &map,
173 0x1000,
174 .{
175 .file_name = "/tmp/app/main.zig",
176 .line = 42,
177 .column = 7,
178 },
179 .{ .patterns = &patterns },
180 );
181
182 try std.testing.expect(line == null);
183 try std.testing.expectEqual(@as(usize, 0), map.ranges.items.len);
184 }