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 cfb60c98be 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
This commit is contained in:
Jacques Lucke
2024-08-08 10:28:17 +02:00
parent 8edae047e8
commit 51f94d6234
6 changed files with 877 additions and 0 deletions

View File

@@ -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<long, short>",
"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()

View File

@@ -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.

View File

@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .gdb import *
import printing
import types

221
tools/debug/gdb/gdb/gdb.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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