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