Skip to content

Commit f6aa5ba

Browse files
committed
Merge pull request #97210 from AleksLitynski/object-snapshot-debugger
Add an ObjectDB Profiling Tool
2 parents 6dfe5de + 3528e83 commit f6aa5ba

34 files changed

+3885
-24
lines changed

core/object/object.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,12 +2374,12 @@ void postinitialize_handler(Object *p_object) {
23742374
p_object->_postinitialize();
23752375
}
23762376

2377-
void ObjectDB::debug_objects(DebugFunc p_func) {
2377+
void ObjectDB::debug_objects(DebugFunc p_func, void *p_user_data) {
23782378
spin_lock.lock();
23792379

23802380
for (uint32_t i = 0, count = slot_count; i < slot_max && count != 0; i++) {
23812381
if (object_slots[i].validator) {
2382-
p_func(object_slots[i].object);
2382+
p_func(object_slots[i].object, p_user_data);
23832383
count--;
23842384
}
23852385
}
@@ -2547,6 +2547,9 @@ void ObjectDB::cleanup() {
25472547
if (obj->is_class("Resource")) {
25482548
extra_info = " - Resource path: " + String(resource_get_path->call(obj, nullptr, 0, call_error));
25492549
}
2550+
if (obj->is_class("RefCounted")) {
2551+
extra_info = " - Reference count: " + itos((static_cast<RefCounted *>(obj))->get_reference_count());
2552+
}
25502553

25512554
uint64_t id = uint64_t(i) | (uint64_t(object_slots[i].validator) << OBJECTDB_SLOT_MAX_COUNT_BITS) | (object_slots[i].is_ref_counted ? OBJECTDB_REFERENCE_BIT : 0);
25522555
DEV_ASSERT(id == (uint64_t)obj->get_instance_id()); // We could just use the id from the object, but this check may help catching memory corruption catastrophes.

core/object/object.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@ class ObjectDB {
10941094
static void setup();
10951095

10961096
public:
1097-
typedef void (*DebugFunc)(Object *p_obj);
1097+
typedef void (*DebugFunc)(Object *p_obj, void *p_user_data);
10981098

10991099
_ALWAYS_INLINE_ static Object *get_instance(ObjectID p_instance_id) {
11001100
uint64_t id = p_instance_id;
@@ -1126,6 +1126,6 @@ class ObjectDB {
11261126
template <typename T>
11271127
_ALWAYS_INLINE_ static Ref<T> get_ref(ObjectID p_instance_id); // Defined in ref_counted.h
11281128

1129-
static void debug_objects(DebugFunc p_func);
1129+
static void debug_objects(DebugFunc p_func, void *p_user_data);
11301130
static int get_object_count();
11311131
};

doc/classes/Tree.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@
124124
Returns column title language code.
125125
</description>
126126
</method>
127+
<method name="get_column_title_tooltip_text" qualifiers="const">
128+
<return type="String" />
129+
<param index="0" name="column" type="int" />
130+
<description>
131+
Returns the column title's tooltip text.
132+
</description>
133+
</method>
127134
<method name="get_column_width" qualifiers="const">
128135
<return type="int" />
129136
<param index="0" name="column" type="int" />
@@ -322,6 +329,14 @@
322329
Sets language code of column title used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
323330
</description>
324331
</method>
332+
<method name="set_column_title_tooltip_text">
333+
<return type="void" />
334+
<param index="0" name="column" type="int" />
335+
<param index="1" name="tooltip_text" type="String" />
336+
<description>
337+
Sets the column title's tooltip text.
338+
</description>
339+
</method>
325340
<method name="set_selected">
326341
<return type="void" />
327342
<param index="0" name="item" type="TreeItem" />

modules/objectdb_profiler/SCsub

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python
2+
from misc.utility.scons_hints import *
3+
4+
Import("env")
5+
Import("env_modules")
6+
7+
env_objdb = env_modules.Clone()
8+
9+
module_obj = []
10+
11+
# Only include in editor and debug builds.
12+
if env_objdb.debug_features:
13+
env_objdb.add_source_files(module_obj, "*.cpp")
14+
15+
# Only the editor needs these files, don't include them in the game.
16+
if env.editor_build:
17+
env_objdb.add_source_files(module_obj, "editor/*.cpp")
18+
env_objdb.add_source_files(module_obj, "editor/data_viewers/*.cpp")
19+
20+
env.modules_sources += module_obj
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def can_build(env, platform):
2+
return env.debug_features
3+
4+
5+
def configure(env):
6+
pass
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/**************************************************************************/
2+
/* class_view.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "class_view.h"
32+
33+
#include "shared_controls.h"
34+
35+
#include "editor/editor_node.h"
36+
#include "editor/themes/editor_scale.h"
37+
#include "scene/gui/split_container.h"
38+
39+
int ClassData::instance_count(GameStateSnapshot *p_snapshot) {
40+
int count = 0;
41+
for (const SnapshotDataObject *instance : instances) {
42+
if (!p_snapshot || instance->snapshot == p_snapshot) {
43+
count += 1;
44+
}
45+
}
46+
return count;
47+
}
48+
49+
int ClassData::get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot) {
50+
if (!recursive_instance_count_cache.has(p_snapshot)) {
51+
recursive_instance_count_cache[p_snapshot] = instance_count(p_snapshot);
52+
for (const String &child : child_classes) {
53+
recursive_instance_count_cache[p_snapshot] += p_all_classes[child].get_recursive_instance_count(p_all_classes, p_snapshot);
54+
}
55+
}
56+
return recursive_instance_count_cache[p_snapshot];
57+
}
58+
59+
SnapshotClassView::SnapshotClassView() {
60+
set_name(TTRC("Classes"));
61+
}
62+
63+
void SnapshotClassView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
64+
SnapshotView::show_snapshot(p_data, p_diff_data);
65+
66+
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
67+
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
68+
69+
HSplitContainer *classes_view = memnew(HSplitContainer);
70+
add_child(classes_view);
71+
classes_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
72+
classes_view->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
73+
classes_view->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
74+
classes_view->set_split_offset(0);
75+
76+
VBoxContainer *class_list_column = memnew(VBoxContainer);
77+
class_list_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
78+
class_list_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
79+
classes_view->add_child(class_list_column);
80+
81+
class_tree = memnew(Tree);
82+
83+
TreeSortAndFilterBar *filter_bar = memnew(TreeSortAndFilterBar(class_tree, TTRC("Filter Classes")));
84+
filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
85+
86+
TreeSortAndFilterBar::SortOptionIndexes default_sort;
87+
if (!diff_data) {
88+
default_sort = filter_bar->add_sort_option(TTRC("Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
89+
} else {
90+
filter_bar->add_sort_option(TTRC("A Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
91+
filter_bar->add_sort_option(TTRC("B Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 2);
92+
default_sort = filter_bar->add_sort_option(TTRC("Delta"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 3);
93+
}
94+
class_list_column->add_child(filter_bar);
95+
96+
class_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
97+
class_tree->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
98+
class_tree->set_hide_folding(false);
99+
class_tree->set_hide_root(true);
100+
class_tree->set_columns(diff_data ? 4 : 2);
101+
class_tree->set_column_titles_visible(true);
102+
class_tree->set_column_title(0, TTRC("Class"));
103+
class_tree->set_column_expand(0, true);
104+
class_tree->set_column_custom_minimum_width(0, 200 * EDSCALE);
105+
class_tree->set_column_title(1, diff_data ? TTRC("A Count") : TTRC("Count"));
106+
class_tree->set_column_expand(1, false);
107+
if (diff_data) {
108+
class_tree->set_column_title_tooltip_text(1, vformat(TTR("A: %s"), snapshot_data->name));
109+
class_tree->set_column_title_tooltip_text(2, vformat(TTR("B: %s"), diff_data->name));
110+
class_tree->set_column_title(2, TTRC("B Count"));
111+
class_tree->set_column_expand(2, false);
112+
class_tree->set_column_title(3, TTRC("Delta"));
113+
class_tree->set_column_expand(3, false);
114+
}
115+
class_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_class_selected));
116+
class_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
117+
class_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
118+
class_tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
119+
class_list_column->add_child(class_tree);
120+
121+
VSplitContainer *object_lists = memnew(VSplitContainer);
122+
classes_view->add_child(object_lists);
123+
object_lists->set_custom_minimum_size(Size2(150 * EDSCALE, 0));
124+
object_lists->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
125+
object_lists->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
126+
if (!diff_data) {
127+
object_lists->add_child(object_list = _make_object_list_tree(TTRC("Objects")));
128+
} else {
129+
object_lists->add_child(object_list = _make_object_list_tree(TTRC("A Objects")));
130+
object_lists->add_child(diff_object_list = _make_object_list_tree(TTRC("B Objects")));
131+
}
132+
133+
HashMap<String, ClassData> grouped_by_class;
134+
grouped_by_class["Object"] = ClassData("Object", "");
135+
_add_objects_to_class_map(grouped_by_class, snapshot_data);
136+
if (diff_data != nullptr) {
137+
_add_objects_to_class_map(grouped_by_class, diff_data);
138+
}
139+
140+
grouped_by_class[""].tree_node = class_tree->create_item();
141+
List<String> classes_todo;
142+
for (const String &c : grouped_by_class[""].child_classes) {
143+
classes_todo.push_front(c);
144+
}
145+
while (classes_todo.size() > 0) {
146+
String next_class_name = classes_todo.front()->get();
147+
classes_todo.pop_front();
148+
ClassData &next = grouped_by_class[next_class_name];
149+
ClassData &nexts_parent = grouped_by_class[next.parent_class_name];
150+
next.tree_node = class_tree->create_item(nexts_parent.tree_node);
151+
next.tree_node->set_text(0, next_class_name + " (" + String::num_int64(next.instance_count(snapshot_data)) + ")");
152+
next.tree_node->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
153+
int a_count = next.get_recursive_instance_count(grouped_by_class, snapshot_data);
154+
next.tree_node->set_text(1, String::num_int64(a_count));
155+
if (diff_data) {
156+
int b_count = next.get_recursive_instance_count(grouped_by_class, diff_data);
157+
next.tree_node->set_text(2, String::num_int64(b_count));
158+
next.tree_node->set_text(3, String::num_int64(a_count - b_count));
159+
}
160+
next.tree_node->set_metadata(0, next_class_name);
161+
for (const String &c : next.child_classes) {
162+
classes_todo.push_front(c);
163+
}
164+
}
165+
166+
// Icons won't load until the frame after show_snapshot is called. Not sure why, but just defer the load.
167+
callable_mp(this, &SnapshotClassView::_notification).call_deferred(NOTIFICATION_THEME_CHANGED);
168+
169+
// Default to sort by descending count. Putting the biggest groups at the top is generally pretty interesting.
170+
filter_bar->select_sort(default_sort.descending);
171+
filter_bar->apply();
172+
}
173+
174+
Tree *SnapshotClassView::_make_object_list_tree(const String &p_column_name) {
175+
Tree *list = memnew(Tree);
176+
list->set_select_mode(Tree::SelectMode::SELECT_ROW);
177+
list->set_hide_folding(true);
178+
list->set_hide_root(true);
179+
list->set_columns(1);
180+
list->set_column_titles_visible(true);
181+
list->set_column_title(0, p_column_name);
182+
list->set_column_expand(0, true);
183+
list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_object_selected).bind(list));
184+
list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
185+
list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
186+
return list;
187+
}
188+
189+
void SnapshotClassView::_add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects) {
190+
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_objects->objects) {
191+
StringName class_name = pair.value->type_name;
192+
StringName parent_class_name = !class_name.is_empty() && ClassDB::class_exists(class_name) ? ClassDB::get_parent_class(class_name) : "";
193+
194+
p_class_map[class_name].instances.push_back(pair.value);
195+
196+
// Go up the tree and insert all parents/grandparents.
197+
while (!class_name.is_empty()) {
198+
if (!p_class_map.has(class_name)) {
199+
p_class_map[class_name] = ClassData(class_name, parent_class_name);
200+
}
201+
202+
if (!p_class_map.has(parent_class_name)) {
203+
// Leave our grandparent blank for now. Next iteration of the while loop will fill it in.
204+
p_class_map[parent_class_name] = ClassData(parent_class_name, "");
205+
}
206+
p_class_map[class_name].parent_class_name = parent_class_name;
207+
p_class_map[parent_class_name].child_classes.insert(class_name);
208+
209+
class_name = parent_class_name;
210+
parent_class_name = !class_name.is_empty() ? ClassDB::get_parent_class(class_name) : "";
211+
}
212+
}
213+
}
214+
215+
void SnapshotClassView::_object_selected(Tree *p_tree) {
216+
GameStateSnapshot *snapshot = snapshot_data;
217+
if (diff_data) {
218+
Tree *other = p_tree == diff_object_list ? object_list : diff_object_list;
219+
TreeItem *selected = other->get_selected();
220+
if (selected) {
221+
selected->deselect(0);
222+
}
223+
if (p_tree == diff_object_list) {
224+
snapshot = diff_data;
225+
}
226+
}
227+
ObjectID object_id = p_tree->get_selected()->get_metadata(0);
228+
EditorNode::get_singleton()->push_item(static_cast<Object *>(snapshot->objects[object_id]));
229+
}
230+
231+
void SnapshotClassView::_class_selected() {
232+
_update_lists();
233+
}
234+
235+
void SnapshotClassView::_populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base) {
236+
p_list->clear();
237+
238+
TreeItem *selected_item = class_tree->get_selected();
239+
if (selected_item == nullptr) {
240+
p_list->set_column_title(0, vformat("%s (0)", TTR(p_name_base)));
241+
return;
242+
}
243+
244+
String class_name = selected_item->get_metadata(0);
245+
TreeItem *root = p_list->create_item();
246+
int object_count = 0;
247+
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
248+
if (pair.value->type_name == class_name) {
249+
TreeItem *item = p_list->create_item(root);
250+
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
251+
item->set_text(0, pair.value->get_name());
252+
item->set_metadata(0, pair.value->remote_object_id);
253+
item->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
254+
object_count++;
255+
}
256+
}
257+
258+
p_list->set_column_title(0, vformat("%s (%d)", TTR(p_name_base), object_count));
259+
}
260+
261+
void SnapshotClassView::_update_lists() {
262+
if (snapshot_data == nullptr) {
263+
return;
264+
}
265+
266+
if (!diff_data) {
267+
_populate_object_list(snapshot_data, object_list, TTRC("Objects"));
268+
} else {
269+
_populate_object_list(snapshot_data, object_list, TTRC("A Objects"));
270+
_populate_object_list(diff_data, diff_object_list, TTRC("B Objects"));
271+
}
272+
}
273+
274+
void SnapshotClassView::_notification(int p_what) {
275+
if (p_what == NOTIFICATION_THEME_CHANGED) {
276+
for (TreeItem *item : _get_children_recursive(class_tree)) {
277+
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(item->get_metadata(0), ""));
278+
}
279+
} else if (p_what == NOTIFICATION_TRANSLATION_CHANGED) {
280+
_update_lists();
281+
}
282+
}

0 commit comments

Comments
 (0)