From 51f94d6234149dbf9c7272303112e910b97d144f Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Thu, 8 Aug 2024 10:28:17 +0200 Subject: [PATCH] Tools: add GDB debug extension This adds a Python file that can be loaded into GDB to improve the debugging experience. This is similar to cfb60c98be7d6ccc48400df94ba5d509e28c9b97 which contains a subset of the features provided here. While not being compatible with other debuggers, specializing on just GDB allows us to use the GDB Python API which is quite feature rich. Besides adding quite a few pretty-printers, this also adds some frame filters which simplify backtraces. I've been using the pretty-printers for quite some time myself already. I added a basic guide on how to set it up in `blender_gdb_extension.py` but that only covers the case when using `gdb` directly. If this is accepted, I can add some more detailed guides on how to configure e.g. vscode to the developer docs. The patch also contains some type hints for GDB which I wrote, which simplify working with the GDB API. Pull Request: https://projects.blender.org/blender/blender/pulls/126062 --- tools/debug/gdb/blender_gdb_extension.py | 616 +++++++++++++++++++++++ tools/debug/gdb/gdb/README.md | 1 + tools/debug/gdb/gdb/__init__.py | 7 + tools/debug/gdb/gdb/gdb.py | 221 ++++++++ tools/debug/gdb/gdb/printing.py | 14 + tools/debug/gdb/gdb/types.py | 18 + 6 files changed, 877 insertions(+) create mode 100644 tools/debug/gdb/blender_gdb_extension.py create mode 100644 tools/debug/gdb/gdb/README.md create mode 100644 tools/debug/gdb/gdb/__init__.py create mode 100644 tools/debug/gdb/gdb/gdb.py create mode 100644 tools/debug/gdb/gdb/printing.py create mode 100644 tools/debug/gdb/gdb/types.py diff --git a/tools/debug/gdb/blender_gdb_extension.py b/tools/debug/gdb/blender_gdb_extension.py new file mode 100644 index 00000000000..523c96de72d --- /dev/null +++ b/tools/debug/gdb/blender_gdb_extension.py @@ -0,0 +1,616 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +''' +This file can be loaded into GDB to improve the debugging experience. + +It has the following features: +* Pretty printers for various types like `blender::Map` and `blender::IndexMask`. +* Frame filters to reduce the complexity of backtraces. + +The basic setup is simple. Add the following line to a `~/.gdbinit` file. +Everything in this file is run by GDB when it is started. +``` +source ~/blender-git/blender/tools/debug/gdb/blender_gdb_extension.py +``` + +To validate that things are registered correctly: +1. Start `gdb`. +2. Run `info pretty-printer` and check for `blender-pretty-printers`. +3. Run `info frame-filter` and check for `blender-frame-filters`. +''' + +import gdb +from contextlib import contextmanager +from gdb.FrameDecorator import FrameDecorator + + +@contextmanager +def eval_var(variable_name, value): + ''' + Creates a context in which the given variable name has the given value. + ''' + gdb.set_convenience_variable(variable_name, value) + try: + yield + finally: + gdb.set_convenience_variable(variable_name, None) + + +class VectorPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + vec_begin = self.value["begin_"] + vec_end = self.value["end_"] + vec_capacity_end = self.value["end_"] + vec_size = vec_end - vec_begin + vec_capacity = vec_capacity_end - vec_begin + return f"Size: {vec_size}" + + def children(self): + begin = self.value["begin_"] + end = self.value["end_"] + size = end - begin + for i in range(size): + yield str(i), begin[i] + + def display_hint(self): + return "array" + + +class SetPrinter: + def __init__(self, value: gdb.Value): + self.value = value + self.key_type = value.type.template_argument(0) + + def to_string(self): + size = int( + self.value["occupied_and_removed_slots_"] - self.value["removed_slots_"] + ) + return f"Size: {size}" + + def children(self): + slots = self.value["slots_"]["data_"] + slots_num = int(self.value["slots_"]["size_"]) + for i in range(slots_num): + slot = slots[i] + if self.key_type.code == gdb.TYPE_CODE_PTR: + key = slot["key_"] + key_int = int(key) + # The key has two special values for an empty and removed slot. + is_occupied = key_int < 2**64 - 2 + if is_occupied: + yield str(i), key + else: + slot_state = int(slot["state_"]) + is_occupied = slot_state == 1 + if is_occupied: + key = slot["key_buffer_"].cast(self.key_type) + yield str(i), key + + def display_hint(self): + return "array" + + +class MapPrinter: + def __init__(self, value: gdb.Value): + self.value = value + self.key_type = value.type.template_argument(0) + self.value_type = value.type.template_argument(1) + + def to_string(self): + size = int( + self.value["occupied_and_removed_slots_"] - self.value["removed_slots_"] + ) + return f"Size: {size}" + + def children(self): + slots = self.value["slots_"]["data_"] + slots_num = int(self.value["slots_"]["size_"]) + for i in range(slots_num): + slot = slots[i] + if self.key_type.code == gdb.TYPE_CODE_PTR: + key = slot["key_"] + key_int = int(key) + # The key has two special values for an empty and removed slot. + is_occupied = key_int < 2**64 - 2 + if is_occupied: + value = slot["value_buffer_"].cast(self.value_type) + yield "Key", key + yield "Value", value + else: + slot_state = int(slot["state_"]) + is_occupied = slot_state == 1 + if is_occupied: + key = slot["key_buffer_"].cast(self.key_type) + value = slot["value_buffer_"].cast(self.value_type) + yield "Key", key + yield "Value", value + + def display_hint(self): + return "map" + + +class MultiValueMapPrinter: + def __init__(self, value: gdb.Value): + self.value = value + self.map_value = value["map_"] + + def to_string(self): + return MapPrinter(self.map_value).to_string() + + def children(self): + return MapPrinter(self.map_value).children() + + def display_hint(self): + return MapPrinter(self.map_value).display_hint() + + +class TypedBufferPrinter: + def __init__(self, value: gdb.Value): + self.value = value + self.type = value.type.template_argument(0) + self.size = value.type.template_argument(1) + + def children(self): + data = self.value.cast(self.type).address + for i in range(self.size): + yield str(i), data[i] + + def display_hint(self): + return "array" + + +class ArrayPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + size = self.value["size_"] + return f"Size: {size}" + + def children(self): + data = self.value["data_"] + size = self.value["size_"] + for i in range(size): + yield str(i), data[i] + + def display_hint(self): + return "array" + + +class VectorSetPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def get_size(self): + return int( + self.value["occupied_and_removed_slots_"] - self.value["removed_slots_"] + ) + + def to_string(self): + size = self.get_size() + return f"Size: {size}" + + def children(self): + data = self.value["keys_"] + size = self.get_size() + for i in range(size): + yield str(i), data[i] + + def display_hint(self): + return "array" + + +class VArrayPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def get_size(self): + impl = self.value["impl_"] + size = int(impl["size_"]) + return size + + def to_string(self): + size = self.get_size() + with eval_var("varray", self.value.address): + is_single = gdb.parse_and_eval("$varray->is_single()") + if is_single and size >= 1: + with eval_var("varray", self.value.address): + single_value = gdb.parse_and_eval("$varray->get_internal_single()") + return f"Size: {size}, Single Value: {single_value}" + return f"Size: {size}" + + def children(self): + size = self.get_size() + impl = self.value["impl_"] + for i in range(size): + with eval_var("varray_impl", impl): + value_at_index = gdb.parse_and_eval(f"$varray_impl->get({i})") + yield str(i), value_at_index + + def display_hint(self): + return "array" + + +class MathVectorPrinter: + def __init__(self, value: gdb.Value): + self.value = value + self.base_type = value.type.template_argument(0) + self.size = value.type.template_argument(1) + + def to_string(self): + values = [str(self.get(i)) for i in range(self.size)] + return "(" + ", ".join(values) + ")" + + def children(self): + for i in range(self.size): + yield str(i), self.get(i) + + def get(self, i): + # Avoid taking pointer of value in case the pointer is not available. + if 2 <= self.size <= 4: + if i == 0: + return self.value["x"] + if i == 1: + return self.value["y"] + if i == 2: + return self.value["z"] + if i == 3: + return self.value["w"] + return self.value["values"][i] + + def display_hint(self): + return "array" + + +class SpanPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + size = self.value["size_"] + return f"Size: {size}" + + def children(self): + data = self.value["data_"] + size = self.value["size_"] + for i in range(size): + yield str(i), data[i] + + def display_hint(self): + return "array" + + +class StringRefPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + data = self.value["data_"] + size = int(self.value["size_"]) + if size == 0: + return "" + return data.string() + + def display_hint(self): + return "string" + + +class IndexRangePrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + start = int(self.value["start_"]) + size = int(self.value["size_"]) + if size == 0: + return "Size: 0" + return f"Size: {size}, [{start} - {start + size - 1}]" + + +class IndexMaskPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + size = int(self.value["indices_num_"]) + if size == 0: + return "Size: 0" + + segments_num = int(self.value["segments_num_"]) + + with eval_var("mask", self.value.address): + first = int(gdb.parse_and_eval("$mask->first()")) + last = int(gdb.parse_and_eval("$mask->last()")) + + is_range = last - first + 1 == size + if is_range: + return f"Size: {size}, [{first} - {last}], Segments: {segments_num}" + return f"Size: {size}, Segments: {segments_num}" + + def children(self): + segments_num = int(self.value["segments_num_"]) + prev_cumulative_size = int(self.value["cumulative_segment_sizes_"][0]) + for segment_i in range(segments_num): + cumulative_size = int( + self.value["cumulative_segment_sizes_"][segment_i + 1] + ) + full_segment_size = cumulative_size - prev_cumulative_size + indices_ptr = self.value["indices_by_segment_"][segment_i] + offset = self.value["segment_offsets_"][segment_i] + with eval_var("mask", self.value.address): + segment = gdb.parse_and_eval(f"$mask->segment({segment_i})") + yield str(segment_i), segment + prev_cumulative_size = cumulative_size + + def display_hint(self): + return "array" + + +class IndexMaskSegmentPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + size = int(self.value["data_"]["size_"]) + if size == 0: + return "Size: 0" + + offset = int(self.value["offset_"]) + first = int(self.value["data_"]["data_"][0]) + offset + last = int(self.value["data_"]["data_"][size - 1]) + offset + is_range = last - first + 1 == size + if is_range: + return f"Size: {size}, [{first} - {last}]" + + return f"Size: {size}" + + def children(self): + size = int(self.value["data_"]["size_"]) + offset = int(self.value["offset_"]) + for i in range(size): + element = int(self.value["data_"]["data_"][i]) + index = element + offset + yield str(i), index + + def display_hint(self): + return "array" + + +class OffsetIndicesPrinter: + def __init__(self, value: gdb.Value): + self.value = value + + def to_string(self): + with eval_var("indices", self.value.address): + size = gdb.parse_and_eval("$indices->size()") + return f"Size: {size}" + + def children(self): + with eval_var("indices", self.value.address): + size = gdb.parse_and_eval("$indices->size()") + for i in range(size): + with eval_var("indices", self.value.address): + element = gdb.parse_and_eval(f"(*$indices)[{i}]") + yield str(i), element + + def display_hint(self): + return "array" + + +class BlenderPrettyPrinters(gdb.printing.PrettyPrinter): + def __init__(self): + super().__init__("blender-pretty-printers") + + def __call__(self, value: gdb.Value): + value_type = value.type + if value_type is None: + return None + if value_type.code == gdb.TYPE_CODE_PTR: + return None + type_name = value_type.strip_typedefs().name + if type_name is None: + return None + if type_name.startswith("blender::Vector<"): + return VectorPrinter(value) + if type_name.startswith("blender::Set<"): + return SetPrinter(value) + if type_name.startswith("blender::Map<"): + return MapPrinter(value) + if type_name.startswith("blender::MultiValueMap<"): + return MultiValueMapPrinter(value) + if type_name.startswith("blender::TypedBuffer<"): + return TypedBufferPrinter(value) + if type_name.startswith("blender::Array<"): + return ArrayPrinter(value) + if type_name.startswith("blender::VectorSet<"): + return VectorSetPrinter(value) + if type_name.startswith("blender::VArray<"): + return VArrayPrinter(value) + if type_name.startswith("blender::VMutableArray<"): + return VArrayPrinter(value) + if type_name.startswith("blender::VecBase<"): + return MathVectorPrinter(value) + if type_name.startswith("blender::Span<"): + return SpanPrinter(value) + if type_name.startswith("blender::MutableSpan<"): + return SpanPrinter(value) + if type_name.startswith("blender::VArraySpan<"): + return SpanPrinter(value) + if type_name == "blender::StringRef": + return StringRefPrinter(value) + if type_name == "blender::StringRefNull": + return StringRefPrinter(value) + if type_name == "blender::IndexRange": + return IndexRangePrinter(value) + if type_name == "blender::index_mask::IndexMask": + return IndexMaskPrinter(value) + if type_name in ( + "blender::OffsetSpan", + "blender::index_mask::IndexMaskSegment", + ): + return IndexMaskSegmentPrinter(value) + if type_name.startswith("blender::offset_indices::OffsetIndices<"): + return OffsetIndicesPrinter(value) + return None + + +class ForeachIndexFilter: + filename_pattern = r".*index_mask.*" + + @staticmethod + def frame_to_name(frame): + function_name = frame.function() + print(function_name) + if function_name.startswith("blender::index_mask::IndexMask::foreach_index"): + return "Foreach Index" + + +class LazyFunctionEvalFilter: + filename_pattern = r".*functions.*lazy_function.*" + + @staticmethod + def frame_to_name(frame): + function_name = frame.function() + if function_name.startswith( + "blender::fn::lazy_function::LazyFunction::execute" + ): + return "Execute Lazy Function" + + +class ThreadingFilter: + filename_pattern = ( + r".*(include/tbb|libtbb|task_pool.cc|BLI_task.hh|task_range.cc).*" + ) + + @staticmethod + def frame_to_name(frame): + function_name = frame.function() + if function_name.startswith("BLI_task_pool_work_and_wait"): + return "Task Pool work and wait" + if function_name.startswith( + "tbb::internal::rml::private_worker::thread_routine" + ): + return "TBB Worker Thread" + if function_name.startswith("blender::threading::parallel_for"): + return "Parallel For" + + +class StdFilter: + filename_pattern = r".*/include/c\+\+/13.*" + + @staticmethod + def frame_to_name(frame): + function_name = frame.function() + if function_name.startswith("std::function") and "operator()" in function_name: + return "Call std::function" + + +class FunctionRefFilter: + filename_pattern = r".*BLI_function_ref.hh" + + @staticmethod + def frame_to_name(frame): + function_name = frame.function() + if "operator()" in function_name: + return "Call FunctionRef" + + +frame_filters = [ + ForeachIndexFilter(), + LazyFunctionEvalFilter(), + ThreadingFilter(), + StdFilter(), + FunctionRefFilter(), +] + + +class FrameFilter: + def __init__(self): + self.name = "blender-frame-filters" + self.priority = 100 + self.enabled = True + + def filter(self, frame_iter): + import re + + current_filter = None + current_frames = [] + + def handle_gathered_frames(): + nonlocal current_frames + nonlocal current_filter + + if current_frames: + top_frame = current_frames[-1] + bottom_frame = current_frames[0] + name = current_filter.frame_to_name(top_frame) + if name is None: + yield from current_frames + else: + yield SimpleFrameDecorator(name, bottom_frame, current_frames[1:]) + + current_frames = [] + current_filter = None + + for frame in frame_iter: + if current_filter and re.match( + current_filter.filename_pattern, frame.filename() + ): + current_frames.append(frame) + continue + + yield from handle_gathered_frames() + + for f in frame_filters: + if re.match(f.filename_pattern, frame.filename()): + current_filter = f + current_frames = [frame] + break + else: + yield frame + + yield from handle_gathered_frames() + + +class SimpleFrameDecorator(FrameDecorator): + def __init__(self, name, frame, elided_frames): + super().__init__(frame) + self.name = name + self.frame = frame + self.elided_frames = elided_frames + + def elided(self): + return iter(self.elided_frames) + + def function(self): + return self.name + + def frame_args(self): + return None + + def address(self): + return None + + def line(self): + return None + + def filename(self): + return None + + def frame_locals(self): + return None + + +def register(): + gdb.printing.register_pretty_printer(None, BlenderPrettyPrinters(), replace=True) + + frame_filter = FrameFilter() + gdb.frame_filters[frame_filter.name] = frame_filter + + +register() diff --git a/tools/debug/gdb/gdb/README.md b/tools/debug/gdb/gdb/README.md new file mode 100644 index 00000000000..d673395b15d --- /dev/null +++ b/tools/debug/gdb/gdb/README.md @@ -0,0 +1 @@ +This folder contains (incomplete) type hints for the `gdb` [Python API](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html). This simplifies working with the API. diff --git a/tools/debug/gdb/gdb/__init__.py b/tools/debug/gdb/gdb/__init__.py new file mode 100644 index 00000000000..8df6c0614f8 --- /dev/null +++ b/tools/debug/gdb/gdb/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from .gdb import * +import printing +import types diff --git a/tools/debug/gdb/gdb/gdb.py b/tools/debug/gdb/gdb/gdb.py new file mode 100644 index 00000000000..30ff8fc438a --- /dev/null +++ b/tools/debug/gdb/gdb/gdb.py @@ -0,0 +1,221 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from __future__ import annotations +import enum +import typing as t + + +# Does not actually exist. +class TypeCode: + pass + + +TYPE_CODE_PTR: TypeCode +TYPE_CODE_ARRAY: TypeCode +TYPE_CODE_STRUCT: TypeCode +TYPE_CODE_UNION: TypeCode +TYPE_CODE_ENUM: TypeCode +TYPE_CODE_FLAGS: TypeCode +TYPE_CODE_FUNC: TypeCode +TYPE_CODE_INT: TypeCode +TYPE_CODE_FLT: TypeCode +TYPE_CODE_VOID: TypeCode +TYPE_CODE_SET: TypeCode +TYPE_CODE_RANGE: TypeCode +TYPE_CODE_STRING: TypeCode +TYPE_CODE_BITSTRING: TypeCode +TYPE_CODE_ERROR: TypeCode +TYPE_CODE_METHOD: TypeCode +TYPE_CODE_METHODPTR: TypeCode +TYPE_CODE_MEMBERPTR: TypeCode +TYPE_CODE_REF: TypeCode +TYPE_CODE_RVALUE_REF: TypeCode +TYPE_CODE_CHAR: TypeCode +TYPE_CODE_BOOL: TypeCode +TYPE_CODE_COMPLEX: TypeCode +TYPE_CODE_TYPEDEF: TypeCode +TYPE_CODE_NAMESPACE: TypeCode +TYPE_CODE_DECFLOAT: TypeCode +TYPE_CODE_INTERNAL_FUNCTION: TypeCode + + +class Objfile: + pass + + +class Type: + alignof: int + sizeof: int + code: TypeCode + dynamic: bool + name: t.Optional[str] + tag: t.Optional[str] + objfile: t.Optional[Objfile] + + def fields(self) -> t.List[Field]: + pass + + def array(self, n1, n2=None) -> Type: + pass + + def vector(self, n1, n2=None) -> Type: + pass + + def const(self) -> Type: + pass + + def volatile(self) -> Type: + pass + + def unqualified(self) -> Type: + pass + + def range(self): + pass + + def reference(self) -> Type: + pass + + def pointer(self) -> Type: + pass + + def strip_typedefs(self) -> Type: + pass + + def target(self) -> Type: + pass + + def template_argument(self, n, block=None) -> t.Union[Type, Value]: + pass + + def optimized_out(self) -> Value: + pass + + +class Field: + bitpos: int + enumval: int + name: t.Optional[str] + artificial: bool + is_base_class: bool + bitsize: int + type: Type + parent_type: Type + + +class Value: + type: Type + address: t.Optional[Value] + is_optimized_out: bool + dynamic_type: Type + is_lazy: bool + + def dereference(self) -> Value: + pass + + def referenced_value(self) -> Value: + pass + + def reference_value(self) -> Value: + pass + + def const_value(self) -> Value: + pass + + def format_string(self, *args, **kwargs) -> str: + pass + + def string(self, encoding=None, errors=None, length=None) -> str: + pass + + def lazy_string(self, encoding=None, length=None) -> str: + pass + + def fetch_lazy(self): + pass + + def cast(self, type: Type) -> Value: + pass + + def reinterpret_cast(self, type: Type) -> Value: + pass + + def __getitem__(self, subscript: t.Union[int, str]) -> Value: + pass + + +def lookup_type(name: str, block=None) -> Type: + pass + + +def lookup_global_symbol(name: str): + pass + + +def execute(command: str, from_tty=None, to_string=None): + pass + + +def parse_and_eval(expression: str) -> Value: + pass + + +def set_convenience_variable(name: str, value: Value): + pass + + +def convenience_variable(name: str) -> Value | None: + pass + + +class MemoryError(Exception): + pass + + +class Command: + def __init__(self, name, command_class, complete_class=None, prefix=None): + pass + + def dont_repeat(self): + pass + + def invoke(self, argument: str, from_tty: bool): + pass + + def complete(self, text: str, word: str) -> t.Union[t.Sequence[str], CompleteCode]: + pass + + +# Does not actually exist. +class CommandCode: + pass + + +COMMAND_NONE: CommandCode +COMMAND_RUNNING: CommandCode +COMMAND_DATA: CommandCode +COMMAND_STACK: CommandCode +COMMAND_FILES: CommandCode +COMMAND_SUPPORT: CommandCode +COMMAND_STATUS: CommandCode +COMMAND_BREAKPOINTS: CommandCode +COMMAND_TRACEPOINTS: CommandCode +COMMAND_TUI: CommandCode +COMMAND_USER: CommandCode +COMMAND_OBSCURE: CommandCode +COMMAND_MAINTENANCE: CommandCode + + +# Does not actually exist. +class CompleteCode: + pass + + +COMPLETE_NONE: CompleteCode +COMPLETE_FILENAME: CompleteCode +COMPLETE_LOCATION: CompleteCode +COMPLETE_COMMAND: CompleteCode +COMPLETE_SYMBOL: CompleteCode +COMPLETE_EXPRESSION: CompleteCode diff --git a/tools/debug/gdb/gdb/printing.py b/tools/debug/gdb/gdb/printing.py new file mode 100644 index 00000000000..39c26f78e73 --- /dev/null +++ b/tools/debug/gdb/gdb/printing.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from .gdb import Value +import typing as t + + +class PrettyPrinter: + def __init__(self, name: str, subprinters=None): + pass + + def __call__(self, value: Value): + pass diff --git a/tools/debug/gdb/gdb/types.py b/tools/debug/gdb/gdb/types.py new file mode 100644 index 00000000000..2bcbec03c60 --- /dev/null +++ b/tools/debug/gdb/gdb/types.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from gdb import Type, Field +import typing as t + + +def get_basic_type(type: Type) -> Type: + pass + + +def has_field(type: Type) -> bool: + pass + + +def make_enum_dict(enum_type: Type) -> t.Dict[str, int]: + pass