677 lines
20 KiB
Python
677 lines
20 KiB
Python
# 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
|
|
import functools
|
|
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)
|
|
|
|
|
|
@contextmanager
|
|
def ensure_unwind_on_signal():
|
|
'''
|
|
Creates a context in which gdb can attempt evaluating functions with
|
|
less risk of crashing the program. If there is an error (e.g. a segfault)
|
|
gdb will unwind the stack, raise a Python exception, but otherwise allows
|
|
continuing the debugging session.
|
|
'''
|
|
was_on = "is on." in gdb.execute("show unwindonsignal", to_string=True)
|
|
if not was_on:
|
|
gdb.execute("set unwindonsignal on")
|
|
try:
|
|
yield
|
|
finally:
|
|
if not was_on:
|
|
gdb.execute("set unwindonsignal off")
|
|
|
|
|
|
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}]"
|
|
|
|
def display_hint(self):
|
|
return None
|
|
|
|
|
|
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 ErrorHandlingPrinter:
|
|
'''
|
|
This class can wrap another printer and adds additional error handling on top.
|
|
'''
|
|
|
|
def __init__(self, printer):
|
|
self.printer = printer
|
|
|
|
def to_string(self):
|
|
try:
|
|
with ensure_unwind_on_signal():
|
|
return self.printer.to_string()
|
|
except Exception as e:
|
|
return "Error"
|
|
|
|
def children(self):
|
|
try:
|
|
children_generator = self.printer.children()
|
|
while True:
|
|
with ensure_unwind_on_signal():
|
|
next_child = next(children_generator)
|
|
yield next_child
|
|
except:
|
|
return
|
|
|
|
def display_hint(self):
|
|
return self.printer.display_hint()
|
|
|
|
|
|
class BlenderPrettyPrinters(gdb.printing.PrettyPrinter):
|
|
def __init__(self):
|
|
super().__init__("blender-pretty-printers")
|
|
|
|
def get_pretty_printer(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
|
|
|
|
def __call__(self, value: gdb.Value):
|
|
if printer := self.get_pretty_printer(value):
|
|
return ErrorHandlingPrinter(printer)
|
|
return None
|
|
|
|
|
|
class ForeachIndexFilter:
|
|
filename_pattern = r".*index_mask.*"
|
|
|
|
@staticmethod
|
|
def frame_to_name(frame):
|
|
function_name = frame.function()
|
|
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")
|
|
or function_name.startswith("blender::fn::lazy_function::Executor::push_to_task_pool")):
|
|
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"
|
|
if function_name.startswith("blender::threading::isolate_task"):
|
|
return "Isolate Task"
|
|
|
|
|
|
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:
|
|
file_name = frame.filename()
|
|
if file_name is None:
|
|
yield frame
|
|
continue
|
|
if current_filter and re.match(
|
|
current_filter.filename_pattern, file_name
|
|
):
|
|
current_frames.append(frame)
|
|
continue
|
|
|
|
yield from handle_gathered_frames()
|
|
|
|
for f in frame_filters:
|
|
if re.match(f.filename_pattern, file_name):
|
|
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()
|