Commit 1792258dc8

Andrew Kelley <andrew@ziglang.org>
2024-08-03 08:31:28
std.debug.Dwarf: precompute .debug_line table
yields a 60x speedup for resolveSourceLocations in debug builds
1 parent 66954e8
Changed files (3)
lib/std/debug/Dwarf.zig
@@ -138,6 +138,29 @@ pub const CompileUnit = struct {
     rnglists_base: usize,
     loclists_base: usize,
     frame_base: ?*const FormValue,
+
+    src_loc_cache: ?SrcLocCache,
+
+    pub const SrcLocCache = struct {
+        line_table: LineTable,
+        directories: []const FileEntry,
+        files: []FileEntry,
+        version: u16,
+
+        pub const LineTable = std.AutoArrayHashMapUnmanaged(u64, LineEntry);
+
+        pub const LineEntry = struct {
+            line: u32,
+            column: u32,
+            file: u32,
+        };
+
+        pub fn findSource(slc: *const SrcLocCache, address: u64) !LineEntry {
+            const index = std.sort.upperBound(u64, address, slc.line_table.keys(), {}, std.sort.asc(u64));
+            if (index == 0) return missing();
+            return slc.line_table.values()[index - 1];
+        }
+    };
 };
 
 pub const FormValue = union(enum) {
@@ -760,6 +783,11 @@ pub fn deinit(di: *Dwarf, gpa: Allocator) void {
     }
     di.abbrev_table_list.deinit(gpa);
     for (di.compile_unit_list.items) |*cu| {
+        if (cu.src_loc_cache) |*slc| {
+            slc.line_table.deinit(gpa);
+            gpa.free(slc.directories);
+            gpa.free(slc.files);
+        }
         cu.die.deinit(gpa);
     }
     di.compile_unit_list.deinit(gpa);
@@ -846,6 +874,7 @@ fn scanAllFunctions(di: *Dwarf, allocator: Allocator) ScanError!void {
             .rnglists_base = 0,
             .loclists_base = 0,
             .frame_base = null,
+            .src_loc_cache = null,
         };
 
         while (true) {
@@ -1032,6 +1061,7 @@ fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) ScanError!void {
             .rnglists_base = if (compile_unit_die.getAttr(AT.rnglists_base)) |fv| try fv.getUInt(usize) else 0,
             .loclists_base = if (compile_unit_die.getAttr(AT.loclists_base)) |fv| try fv.getUInt(usize) else 0,
             .frame_base = compile_unit_die.getAttr(AT.frame_base),
+            .src_loc_cache = null,
         };
 
         compile_unit.pc_range = x: {
@@ -1242,7 +1272,7 @@ const DebugRangeIterator = struct {
 };
 
 /// TODO: change this to binary searching the sorted compile unit list
-pub fn findCompileUnit(di: *const Dwarf, target_address: u64) !*const CompileUnit {
+pub fn findCompileUnit(di: *const Dwarf, target_address: u64) !*CompileUnit {
     for (di.compile_unit_list.items) |*compile_unit| {
         if (compile_unit.pc_range) |range| {
             if (target_address >= range.start and target_address < range.end) return compile_unit;
@@ -1352,34 +1382,36 @@ fn parseDie(
     };
 }
 
-pub fn getLineNumberInfo(
-    di: *Dwarf,
-    allocator: Allocator,
-    compile_unit: CompileUnit,
-    target_address: u64,
-) !std.debug.SourceLocation {
-    const compile_unit_cwd = try compile_unit.die.getAttrString(di, AT.comp_dir, di.section(.debug_line_str), compile_unit);
+fn runLineNumberProgram(d: *Dwarf, gpa: Allocator, compile_unit: *CompileUnit) !CompileUnit.SrcLocCache {
+    const compile_unit_cwd = try compile_unit.die.getAttrString(d, AT.comp_dir, d.section(.debug_line_str), compile_unit.*);
     const line_info_offset = try compile_unit.die.getAttrSecOffset(AT.stmt_list);
 
-    var fbr: FixedBufferReader = .{ .buf = di.section(.debug_line).?, .endian = di.endian };
+    var fbr: FixedBufferReader = .{
+        .buf = d.section(.debug_line).?,
+        .endian = d.endian,
+    };
     try fbr.seekTo(line_info_offset);
 
     const unit_header = try readUnitHeader(&fbr, null);
     if (unit_header.unit_length == 0) return missing();
+
     const next_offset = unit_header.header_length + unit_header.unit_length;
 
     const version = try fbr.readInt(u16);
     if (version < 2) return bad();
 
-    var addr_size: u8 = switch (unit_header.format) {
-        .@"32" => 4,
-        .@"64" => 8,
+    const addr_size: u8, const seg_size: u8 = if (version >= 5) .{
+        try fbr.readByte(),
+        try fbr.readByte(),
+    } else .{
+        switch (unit_header.format) {
+            .@"32" => 4,
+            .@"64" => 8,
+        },
+        0,
     };
-    var seg_size: u8 = 0;
-    if (version >= 5) {
-        addr_size = try fbr.readByte();
-        seg_size = try fbr.readByte();
-    }
+    _ = addr_size;
+    _ = seg_size;
 
     const prologue_length = try fbr.readAddress(unit_header.format);
     const prog_start_offset = fbr.pos + prologue_length;
@@ -1388,8 +1420,8 @@ pub fn getLineNumberInfo(
     if (minimum_instruction_length == 0) return bad();
 
     if (version >= 4) {
-        // maximum_operations_per_instruction
-        _ = try fbr.readByte();
+        const maximum_operations_per_instruction = try fbr.readByte();
+        _ = maximum_operations_per_instruction;
     }
 
     const default_is_stmt = (try fbr.readByte()) != 0;
@@ -1402,18 +1434,18 @@ pub fn getLineNumberInfo(
 
     const standard_opcode_lengths = try fbr.readBytes(opcode_base - 1);
 
-    var include_directories = std.ArrayList(FileEntry).init(allocator);
-    defer include_directories.deinit();
-    var file_entries = std.ArrayList(FileEntry).init(allocator);
-    defer file_entries.deinit();
+    var directories: std.ArrayListUnmanaged(FileEntry) = .{};
+    defer directories.deinit(gpa);
+    var file_entries: std.ArrayListUnmanaged(FileEntry) = .{};
+    defer file_entries.deinit(gpa);
 
     if (version < 5) {
-        try include_directories.append(.{ .path = compile_unit_cwd });
+        try directories.append(gpa, .{ .path = compile_unit_cwd });
 
         while (true) {
             const dir = try fbr.readBytesTo(0);
             if (dir.len == 0) break;
-            try include_directories.append(.{ .path = dir });
+            try directories.append(gpa, .{ .path = dir });
         }
 
         while (true) {
@@ -1422,7 +1454,7 @@ pub fn getLineNumberInfo(
             const dir_index = try fbr.readUleb128(u32);
             const mtime = try fbr.readUleb128(u64);
             const size = try fbr.readUleb128(u64);
-            try file_entries.append(.{
+            try file_entries.append(gpa, .{
                 .path = file_name,
                 .dir_index = dir_index,
                 .mtime = mtime,
@@ -1446,52 +1478,10 @@ pub fn getLineNumberInfo(
             }
 
             const directories_count = try fbr.readUleb128(usize);
-            try include_directories.ensureUnusedCapacity(directories_count);
-            {
-                var i: usize = 0;
-                while (i < directories_count) : (i += 1) {
-                    var e: FileEntry = .{ .path = &.{} };
-                    for (dir_ent_fmt_buf[0..directory_entry_format_count]) |ent_fmt| {
-                        const form_value = try parseFormValue(
-                            &fbr,
-                            ent_fmt.form_code,
-                            unit_header.format,
-                            null,
-                        );
-                        switch (ent_fmt.content_type_code) {
-                            DW.LNCT.path => e.path = try form_value.getString(di.*),
-                            DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
-                            DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
-                            DW.LNCT.size => e.size = try form_value.getUInt(u64),
-                            DW.LNCT.MD5 => e.md5 = switch (form_value) {
-                                .data16 => |data16| data16.*,
-                                else => return bad(),
-                            },
-                            else => continue,
-                        }
-                    }
-                    include_directories.appendAssumeCapacity(e);
-                }
-            }
-        }
 
-        var file_ent_fmt_buf: [10]FileEntFmt = undefined;
-        const file_name_entry_format_count = try fbr.readByte();
-        if (file_name_entry_format_count > file_ent_fmt_buf.len) return bad();
-        for (file_ent_fmt_buf[0..file_name_entry_format_count]) |*ent_fmt| {
-            ent_fmt.* = .{
-                .content_type_code = try fbr.readUleb128(u8),
-                .form_code = try fbr.readUleb128(u16),
-            };
-        }
-
-        const file_names_count = try fbr.readUleb128(usize);
-        try file_entries.ensureUnusedCapacity(file_names_count);
-        {
-            var i: usize = 0;
-            while (i < file_names_count) : (i += 1) {
-                var e: FileEntry = .{ .path = &.{} };
-                for (file_ent_fmt_buf[0..file_name_entry_format_count]) |ent_fmt| {
+            for (try directories.addManyAsSlice(gpa, directories_count)) |*e| {
+                e.* = .{ .path = &.{} };
+                for (dir_ent_fmt_buf[0..directory_entry_format_count]) |ent_fmt| {
                     const form_value = try parseFormValue(
                         &fbr,
                         ent_fmt.form_code,
@@ -1499,7 +1489,7 @@ pub fn getLineNumberInfo(
                         null,
                     );
                     switch (ent_fmt.content_type_code) {
-                        DW.LNCT.path => e.path = try form_value.getString(di.*),
+                        DW.LNCT.path => e.path = try form_value.getString(d.*),
                         DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
                         DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
                         DW.LNCT.size => e.size = try form_value.getUInt(u64),
@@ -1510,17 +1500,49 @@ pub fn getLineNumberInfo(
                         else => continue,
                     }
                 }
-                file_entries.appendAssumeCapacity(e);
+            }
+        }
+
+        var file_ent_fmt_buf: [10]FileEntFmt = undefined;
+        const file_name_entry_format_count = try fbr.readByte();
+        if (file_name_entry_format_count > file_ent_fmt_buf.len) return bad();
+        for (file_ent_fmt_buf[0..file_name_entry_format_count]) |*ent_fmt| {
+            ent_fmt.* = .{
+                .content_type_code = try fbr.readUleb128(u8),
+                .form_code = try fbr.readUleb128(u16),
+            };
+        }
+
+        const file_names_count = try fbr.readUleb128(usize);
+        try file_entries.ensureUnusedCapacity(gpa, file_names_count);
+
+        for (try file_entries.addManyAsSlice(gpa, file_names_count)) |*e| {
+            e.* = .{ .path = &.{} };
+            for (file_ent_fmt_buf[0..file_name_entry_format_count]) |ent_fmt| {
+                const form_value = try parseFormValue(
+                    &fbr,
+                    ent_fmt.form_code,
+                    unit_header.format,
+                    null,
+                );
+                switch (ent_fmt.content_type_code) {
+                    DW.LNCT.path => e.path = try form_value.getString(d.*),
+                    DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
+                    DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
+                    DW.LNCT.size => e.size = try form_value.getUInt(u64),
+                    DW.LNCT.MD5 => e.md5 = switch (form_value) {
+                        .data16 => |data16| data16.*,
+                        else => return bad(),
+                    },
+                    else => continue,
+                }
             }
         }
     }
 
-    var prog = LineNumberProgram.init(
-        default_is_stmt,
-        include_directories.items,
-        target_address,
-        version,
-    );
+    var prog = LineNumberProgram.init(default_is_stmt, version);
+    var line_table: CompileUnit.SrcLocCache.LineTable = .{};
+    errdefer line_table.deinit(gpa);
 
     try fbr.seekTo(prog_start_offset);
 
@@ -1536,7 +1558,7 @@ pub fn getLineNumberInfo(
             switch (sub_op) {
                 DW.LNE.end_sequence => {
                     prog.end_sequence = true;
-                    if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;
+                    try prog.addRow(gpa, &line_table);
                     prog.reset();
                 },
                 DW.LNE.set_address => {
@@ -1548,7 +1570,7 @@ pub fn getLineNumberInfo(
                     const dir_index = try fbr.readUleb128(u32);
                     const mtime = try fbr.readUleb128(u64);
                     const size = try fbr.readUleb128(u64);
-                    try file_entries.append(.{
+                    try file_entries.append(gpa, .{
                         .path = path,
                         .dir_index = dir_index,
                         .mtime = mtime,
@@ -1564,12 +1586,12 @@ pub fn getLineNumberInfo(
             const inc_line = @as(i32, line_base) + @as(i32, adjusted_opcode % line_range);
             prog.line += inc_line;
             prog.address += inc_addr;
-            if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;
+            try prog.addRow(gpa, &line_table);
             prog.basic_block = false;
         } else {
             switch (opcode) {
                 DW.LNS.copy => {
-                    if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;
+                    try prog.addRow(gpa, &line_table);
                     prog.basic_block = false;
                 },
                 DW.LNS.advance_pc => {
@@ -1611,7 +1633,35 @@ pub fn getLineNumberInfo(
         }
     }
 
-    return missing();
+    return .{
+        .line_table = line_table,
+        .directories = try directories.toOwnedSlice(gpa),
+        .files = try file_entries.toOwnedSlice(gpa),
+        .version = version,
+    };
+}
+
+pub fn getLineNumberInfo(
+    d: *Dwarf,
+    gpa: Allocator,
+    compile_unit: *CompileUnit,
+    target_address: u64,
+) !std.debug.SourceLocation {
+    if (compile_unit.src_loc_cache == null)
+        compile_unit.src_loc_cache = try runLineNumberProgram(d, gpa, compile_unit);
+    const slc = &compile_unit.src_loc_cache.?;
+    const entry = try slc.findSource(target_address);
+    const file_index = entry.file - @intFromBool(slc.version < 5);
+    if (file_index >= slc.files.len) return bad();
+    const file_entry = &slc.files[file_index];
+    if (file_entry.dir_index >= slc.directories.len) return bad();
+    const dir_name = slc.directories[file_entry.dir_index].path;
+    const file_name = try std.fs.path.join(gpa, &.{ dir_name, file_entry.path });
+    return .{
+        .line = entry.line,
+        .column = entry.column,
+        .file_name = file_name,
+    };
 }
 
 fn getString(di: Dwarf, offset: u64) ![:0]const u8 {
@@ -1826,17 +1876,6 @@ const LineNumberProgram = struct {
     end_sequence: bool,
 
     default_is_stmt: bool,
-    target_address: u64,
-    include_dirs: []const FileEntry,
-
-    prev_valid: bool,
-    prev_address: u64,
-    prev_file: usize,
-    prev_line: i64,
-    prev_column: u64,
-    prev_is_stmt: bool,
-    prev_basic_block: bool,
-    prev_end_sequence: bool,
 
     // Reset the state machine following the DWARF specification
     pub fn reset(self: *LineNumberProgram) void {
@@ -1847,24 +1886,10 @@ const LineNumberProgram = struct {
         self.is_stmt = self.default_is_stmt;
         self.basic_block = false;
         self.end_sequence = false;
-        // Invalidate all the remaining fields
-        self.prev_valid = false;
-        self.prev_address = 0;
-        self.prev_file = undefined;
-        self.prev_line = undefined;
-        self.prev_column = undefined;
-        self.prev_is_stmt = undefined;
-        self.prev_basic_block = undefined;
-        self.prev_end_sequence = undefined;
     }
 
-    pub fn init(
-        is_stmt: bool,
-        include_dirs: []const FileEntry,
-        target_address: u64,
-        version: u16,
-    ) LineNumberProgram {
-        return LineNumberProgram{
+    pub fn init(is_stmt: bool, version: u16) LineNumberProgram {
+        return .{
             .address = 0,
             .file = 1,
             .line = 1,
@@ -1873,60 +1898,17 @@ const LineNumberProgram = struct {
             .is_stmt = is_stmt,
             .basic_block = false,
             .end_sequence = false,
-            .include_dirs = include_dirs,
             .default_is_stmt = is_stmt,
-            .target_address = target_address,
-            .prev_valid = false,
-            .prev_address = 0,
-            .prev_file = undefined,
-            .prev_line = undefined,
-            .prev_column = undefined,
-            .prev_is_stmt = undefined,
-            .prev_basic_block = undefined,
-            .prev_end_sequence = undefined,
         };
     }
 
-    pub fn checkLineMatch(
-        self: *LineNumberProgram,
-        allocator: Allocator,
-        file_entries: []const FileEntry,
-    ) !?std.debug.SourceLocation {
-        if (self.prev_valid and
-            self.target_address >= self.prev_address and
-            self.target_address < self.address)
-        {
-            const file_index = if (self.version >= 5) self.prev_file else i: {
-                if (self.prev_file == 0) return missing();
-                break :i self.prev_file - 1;
-            };
-
-            if (file_index >= file_entries.len) return bad();
-            const file_entry = &file_entries[file_index];
-
-            if (file_entry.dir_index >= self.include_dirs.len) return bad();
-            const dir_name = self.include_dirs[file_entry.dir_index].path;
-
-            const file_name = try std.fs.path.join(allocator, &[_][]const u8{
-                dir_name, file_entry.path,
-            });
-
-            return std.debug.SourceLocation{
-                .line = if (self.prev_line >= 0) @as(u64, @intCast(self.prev_line)) else 0,
-                .column = self.prev_column,
-                .file_name = file_name,
-            };
-        }
-
-        self.prev_valid = true;
-        self.prev_address = self.address;
-        self.prev_file = self.file;
-        self.prev_line = self.line;
-        self.prev_column = self.column;
-        self.prev_is_stmt = self.is_stmt;
-        self.prev_basic_block = self.basic_block;
-        self.prev_end_sequence = self.end_sequence;
-        return null;
+    pub fn addRow(prog: *LineNumberProgram, gpa: Allocator, table: *CompileUnit.SrcLocCache.LineTable) !void {
+        if (prog.line == 0) return; // garbage data
+        try table.put(gpa, prog.address, .{
+            .line = cast(u32, prog.line) orelse maxInt(u32),
+            .column = cast(u32, prog.column) orelse maxInt(u32),
+            .file = cast(u32, prog.file) orelse return bad(),
+        });
     }
 };
 
@@ -2381,7 +2363,7 @@ pub fn resolveSourceLocations(
     defer prog_node.end();
 
     var cu_i: usize = 0;
-    var cu: *const CompileUnit = &d.compile_unit_list.items[0];
+    var cu: *CompileUnit = &d.compile_unit_list.items[0];
     var range = cu.pc_range.?;
     next_pc: for (sorted_pc_addrs, output) |pc, *out| {
         defer prog_node.completeOne();
@@ -2403,7 +2385,7 @@ pub fn resolveSourceLocations(
         }
         // TODO: instead of calling this function, break the function up into one that parses the
         // information once and prepares a context that can be reused for the entire batch.
-        if (getLineNumberInfo(d, gpa, cu.*, pc)) |src_loc| {
+        if (getLineNumberInfo(d, gpa, cu, pc)) |src_loc| {
             out.* = src_loc;
         } else |err| switch (err) {
             error.MissingDebugInfo, error.InvalidDebugInfo => out.* = std.debug.SourceLocation.invalid,
@@ -2419,7 +2401,7 @@ fn getSymbol(di: *Dwarf, allocator: Allocator, address: u64) !std.debug.Symbol {
             .compile_unit_name = compile_unit.die.getAttrString(di, std.dwarf.AT.name, di.section(.debug_str), compile_unit.*) catch |err| switch (err) {
                 error.MissingDebugInfo, error.InvalidDebugInfo => "???",
             },
-            .source_location = di.getLineNumberInfo(allocator, compile_unit.*, address) catch |err| switch (err) {
+            .source_location = di.getLineNumberInfo(allocator, compile_unit, address) catch |err| switch (err) {
                 error.MissingDebugInfo, error.InvalidDebugInfo => null,
                 else => return err,
             },
lib/std/debug/FixedBufferReader.zig
@@ -1,4 +1,6 @@
-const std = @import("std.zig");
+//! Optimized for performance in debug builds.
+
+const std = @import("../std.zig");
 const MemoryAccessor = std.debug.MemoryAccessor;
 
 const FixedBufferReader = @This();
lib/std/debug.zig
@@ -762,7 +762,7 @@ pub fn writeCurrentStackTrace(
         // an overflow. We do not need to signal `StackIterator` as it will correctly detect this
         // condition on the subsequent iteration and return `null` thus terminating the loop.
         // same behaviour for x86-windows-msvc
-        const address = if (return_address == 0) return_address else return_address - 1;
+        const address = return_address -| 1;
         try printSourceAtAddress(debug_info, out_stream, address, tty_config);
     } else printLastUnwindError(&it, debug_info, out_stream, tty_config);
 }