authorgravatar for carl@astholm.seCarl Åstholm <carl@astholm.se> 2026-05-23 22:48:11+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-26 20:37:09+02:00
logfc3406a96169ee72c33ba6a0c965ec62fbd44539
tree8af07a171ce2a5615d7600ed162c74ff64a770c8
parent456b2ec07a8c5fa4d34aeb89a8909dfc4c78cb77

std.debug.Pdb: deduplicate inline source locations

Previously, if the same inline function was called multiple times by the same caller, the inline function's frame would be repeated multiple times, once for each prior call.

4 files changed, 94 insertions(+), 63 deletions(-)

lib/std/debug/Pdb.zig+61-34
......@@ -26,10 +26,11 @@ pub const Module = struct {
2626 symbols: []u8,
2727 subsect_info: []u8,
2828 checksum_offset: ?usize,
29 /// The inlinee source lines, sorted by inlinee. This saves us from repeatedly doing linear
30 /// searches over all inlinees. We prefer binary search over a hashmap as LLVM somtimes outputs
31 /// multiple entries for a single inlinee ID, see `getInlineeSourceLines` for more info.
32 inlinee_source_lines: []InlineeSourceLine,
29 /// The inlinee source lines, sorted by inlinee, then file, then line number.
30 /// This saves us from repeatedly doing linear searches over all inlinees.
31 /// We prefer binary search over a hashmap as LLVM somtimes outputs multiple entries
32 /// for a single inlinee ID, see `getInlineeSourceLines` for more info.
33 inlinee_source_lines: []*align(1) const pdb.InlineeSourceLine,
3334
3435 pub fn deinit(self: *Module, allocator: Allocator) void {
3536 allocator.free(self.module_name);
......@@ -669,44 +670,68 @@ pub fn getSymbolName(self: *Pdb, proc_sym: *align(1) const pdb.ProcSym) []const
669670 return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&proc_sym.name[0])), 0);
670671}
671672
672pub const InlineeSourceLine = struct {
673 signature: pdb.InlineeSourceLineSignature,
674 info: *align(1) const pdb.InlineeSourceLine,
673fn inlineeSourceLineLessThan(
674 _: void,
675 lhs: *align(1) const pdb.InlineeSourceLine,
676 rhs: *align(1) const pdb.InlineeSourceLine,
677) bool {
678 if (lhs.inlinee < rhs.inlinee) return true;
679 if (lhs.inlinee > rhs.inlinee) return false;
680 if (lhs.file_id < rhs.file_id) return true;
681 if (lhs.file_id > rhs.file_id) return false;
682 return lhs.source_line_num < rhs.source_line_num;
683}
675684
676 fn lessThan(_: void, lhs: InlineeSourceLine, rhs: InlineeSourceLine) bool {
677 return lhs.info.inlinee < rhs.info.inlinee;
678 }
685fn compareInlineeSourceLineInlinee(
686 inlinee: u32,
687 inlinee_src_line: *align(1) const pdb.InlineeSourceLine,
688) std.math.Order {
689 return std.math.order(inlinee, inlinee_src_line.inlinee);
690}
679691
680 fn compare(inlinee: u32, self: InlineeSourceLine) std.math.Order {
681 return std.math.order(inlinee, self.info.inlinee);
692pub const InlineeSourceLocationIterator = struct {
693 /// The iterator assumes that all source lines in the slice are associated
694 /// with the same inlinee, and that it is sorted by file, then line number.
695 lines: []*align(1) const pdb.InlineeSourceLine,
696
697 pub const empty: InlineeSourceLocationIterator = .{ .lines = &.{} };
698
699 pub fn next(iter: *InlineeSourceLocationIterator) ?*align(1) const pdb.InlineeSourceLine {
700 if (iter.lines.len == 0) return null;
701 const line = iter.lines[0];
702 iter.lines = iter.lines[1..];
703 // Filter out duplicate entries
704 while (iter.lines.len != 0 and
705 iter.lines[0].file_id == line.file_id and
706 iter.lines[0].source_line_num == line.source_line_num)
707 {
708 iter.lines = iter.lines[1..];
709 }
710 return line;
682711 }
683712};
684713
685/// Returns all `InlineeSourceLine`s for a given module with the given inlinee. Ideally there would
686/// only be one entry per inlinee, but LLVM appears to assign all functions that share a name the
687/// same inlinee ID. This appears to be a bug, so the best the caller can do right now is print all
688/// the results.
689pub fn getInlineeSourceLines(
690 self: *Pdb,
691 mod: *Module,
692 inlinee: u32,
693) []const InlineeSourceLine {
714/// Returns all `pdb.InlineeSourceLine`s for a given module with the given inlinee. Ideally
715/// there would only be one entry per inlinee, but LLVM appears to assign all functions that share
716/// a name the same inlinee ID. This is a bug: https://github.com/llvm/llvm-project/issues/191787
717/// The best the caller can do right now is print all the results.
718pub fn getInlineeSourceLines(self: *Pdb, mod: *Module, inlinee: u32) InlineeSourceLocationIterator {
694719 _ = self;
695720
696721 // Binary search to an arbitrary match, if there are other matches they will be adjacent
697722 const any = std.sort.binarySearch(
698 InlineeSourceLine,
723 *align(1) const pdb.InlineeSourceLine,
699724 mod.inlinee_source_lines,
700725 inlinee,
701 InlineeSourceLine.compare,
702 ) orelse return &.{};
726 compareInlineeSourceLineInlinee,
727 ) orelse return .empty;
703728
704729 // Linearly scan to the first match
705730 const begin = b: {
706731 var begin = any;
707732 while (begin > 0) {
708733 const prev = begin - 1;
709 if (mod.inlinee_source_lines[prev].info.inlinee != inlinee) break;
734 if (mod.inlinee_source_lines[prev].inlinee != inlinee) break;
710735 begin = prev;
711736 }
712737 break :b begin;
......@@ -716,13 +741,13 @@ pub fn getInlineeSourceLines(
716741 const end = b: {
717742 var end = any + 1;
718743 while (end < mod.inlinee_source_lines.len and
719 mod.inlinee_source_lines[end].info.inlinee == inlinee) : (end += 1)
744 mod.inlinee_source_lines[end].inlinee == inlinee) : (end += 1)
720745 {}
721746 break :b end;
722747 };
723748
724 // Return a slice of all the matches
725 return mod.inlinee_source_lines[begin..end];
749 // Return an iterator over all matches (the iterator filters out duplicate entries)
750 return .{ .lines = mod.inlinee_source_lines[begin..end] };
726751}
727752
728753pub fn getLineNumberInfo(self: *Pdb, gpa: Allocator, module: *Module, address: u64) !std.debug.SourceLocation {
......@@ -845,7 +870,7 @@ pub fn getModule(self: *Pdb, index: usize) !?*Module {
845870 mod.subsect_info = try reader.readAlloc(gpa, mod.mod_info.c13_byte_size);
846871 errdefer gpa.free(mod.subsect_info);
847872 mod.inlinee_source_lines = b: {
848 var inlinee_source_lines: std.ArrayList(InlineeSourceLine) = .empty;
873 var inlinee_source_lines: std.ArrayList(*align(1) const pdb.InlineeSourceLine) = .empty;
849874 defer inlinee_source_lines.deinit(gpa);
850875 var subsects: Io.Reader = .fixed(mod.subsect_info);
851876 while (subsects.takeStructPointer(pdb.DebugSubsectionHeader) catch null) |subsect_hdr| {
......@@ -866,15 +891,17 @@ pub fn getModule(self: *Pdb, index: usize) !?*Module {
866891 return error.InvalidDebugInfo;
867892 }
868893
869 try inlinee_source_lines.append(gpa, .{
870 .signature = inlinee_source_line_signature,
871 .info = info,
872 });
894 try inlinee_source_lines.append(gpa, info);
873895 }
874896 }
875897 }
876898
877 std.mem.sortUnstable(InlineeSourceLine, inlinee_source_lines.items, {}, InlineeSourceLine.lessThan);
899 std.mem.sortUnstable(
900 *align(1) const pdb.InlineeSourceLine,
901 inlinee_source_lines.items,
902 {},
903 inlineeSourceLineLessThan,
904 );
878905 break :b try inlinee_source_lines.toOwnedSlice(gpa);
879906 };
880907 errdefer gpa.free(mod.inlinee_source_lines);
lib/std/debug/SelfInfo/Windows.zig+3-5
......@@ -311,15 +311,13 @@ const Module = struct {
311311
312312 // If our address points into this site, get the source location(s) it
313313 // points at
314 for (pdb.getInlineeSourceLines(
315 module,
316 inline_site.inlinee,
317 )) |inlinee_src_line| {
314 var line_iter = pdb.getInlineeSourceLines(module, inline_site.inlinee);
315 while (line_iter.next()) |inlinee_src_line| {
318316 const maybe_loc = pdb.getInlineSiteSourceLocation(
319317 text_arena,
320318 module,
321319 inline_site,
322 inlinee_src_line.info,
320 inlinee_src_line,
323321 offset_in_func,
324322 ) catch continue;
325323 const loc = maybe_loc orelse continue;
test/error_traces.zig+17-14
......@@ -580,12 +580,15 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext, os: std.Target.
580580
581581 cases.addCase(.{
582582 .name = "trace through inline call",
583 // The main function has two inline calls to ensure
584 // that inlinees in PDBs are properly deduplicated.
583585 .source =
584586 \\pub fn main() !void {
585 \\ try foo();
587 \\ try foo(false);
588 \\ try foo(true);
586589 \\}
587 \\inline fn foo() !void {
588 \\ try bar();
590 \\inline fn foo(b: bool) !void {
591 \\ if (b) try bar();
589592 \\}
590593 \\fn bar() !void {
591594 \\ return error.ThisIsSoSad;
......@@ -597,25 +600,25 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext, os: std.Target.
597600 // so our expected result is slightly different for Windows than on other operating
598601 // systems.
599602 .windows =>
600 \\source.zig:8:5: [address] in bar
603 \\source.zig:9:5: [address] in bar
601604 \\ return error.ThisIsSoSad;
602605 \\ ^
603 \\source.zig:5: [address] in foo
604 \\ try bar();
606 \\source.zig:6: [address] in foo
607 \\ if (b) try bar();
605608 \\
606 \\source.zig:2:5: [address] in main
607 \\ try foo();
609 \\source.zig:3:5: [address] in main
610 \\ try foo(true);
608611 \\ ^
609612 ,
610613 else =>
611 \\source.zig:8:5: [address] in bar
614 \\source.zig:9:5: [address] in bar
612615 \\ return error.ThisIsSoSad;
613616 \\ ^
614 \\source.zig:5:5: [address] in foo
615 \\ try bar();
616 \\ ^
617 \\source.zig:2:5: [address] in main
618 \\ try foo();
617 \\source.zig:6:12: [address] in foo
618 \\ if (b) try bar();
619 \\ ^
620 \\source.zig:3:5: [address] in main
621 \\ try foo(true);
619622 \\ ^
620623 ,
621624 },
test/stack_traces.zig+13-10
......@@ -226,12 +226,15 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext, os: std.Target.
226226
227227 cases.addCase(.{
228228 .name = "simple inline panic",
229 // The main function has two inline calls to ensure
230 // that inlinees in PDBs are properly deduplicated.
229231 .source =
230232 \\pub fn main() void {
231 \\ foo();
233 \\ foo(false);
234 \\ foo(true);
232235 \\}
233 \\inline fn foo() void {
234 \\ @panic("oh no");
236 \\inline fn foo(b: bool) void {
237 \\ if (b) @panic("oh no");
235238 \\}
236239 \\
237240 ,
......@@ -242,11 +245,11 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext, os: std.Target.
242245 // so the first location has only a row.
243246 .windows =>
244247 \\panic: oh no
245 \\source.zig:5: [address] in foo
246 \\ @panic("oh no");
248 \\source.zig:6: [address] in foo
249 \\ if (b) @panic("oh no");
247250 \\
248 \\source.zig:2:8: [address] in main
249 \\ foo();
251 \\source.zig:3:8: [address] in main
252 \\ foo(true);
250253 \\ ^
251254 \\
252255 ,
......@@ -254,9 +257,9 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext, os: std.Target.
254257 // resolve the inline callers.
255258 else =>
256259 \\panic: oh no
257 \\source.zig:5:5: [address] in foo
258 \\ @panic("oh no");
259 \\ ^
260 \\source.zig:6:12: [address] in foo
261 \\ if (b) @panic("oh no");
262 \\ ^
260263 ,
261264 },
262265 .expect_strip = switch (os) {