blender/tools/debug/gdb/blender_gdb_extension.py
Hans Goudey 328fe42df9 Node Tools: Registration cleanup and drawing refactor
Instead of storing a static asset item tree for every possible context,
store a tree of the registered operator types and check which should be
visible when drawing using the actual poll function rather than re-
implementing its logic.

Also rewrite the registration process to make it easier to follow and
add a more obvious early return when there are no changes to the node
tools that must be registered.

By construction this changes drawing behavior for duplicate idnames.
Now, when drawing the tool menus, don't draw multiple tools with the
same operator idname. This resolves #158802.

Overall this goes beyond a minimal fix for that report, but I'm much
happier with the state of the code now.

Pull Request: https://projects.blender.org/blender/blender/pulls/158857
2026-05-25 21:26:01 +02:00

684 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`.
'''
__all__ = (
# Not used externally but functions as a `main`.
"register",
)
import gdb
import gdb.printing
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(length=size)
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 ex:
return f"Error: {ex!s}"
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",
"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
if __name__ == "__main__":
register()