Merge pull request #97210 from AleksLitynski/object-snapshot-debugger

Add an ObjectDB Profiling Tool
This commit is contained in:
Thaddeus Crews 2025-10-03 12:01:11 -05:00
commit f6aa5ba23c
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
34 changed files with 3885 additions and 24 deletions

View file

@ -2374,12 +2374,12 @@ void postinitialize_handler(Object *p_object) {
p_object->_postinitialize();
}
void ObjectDB::debug_objects(DebugFunc p_func) {
void ObjectDB::debug_objects(DebugFunc p_func, void *p_user_data) {
spin_lock.lock();
for (uint32_t i = 0, count = slot_count; i < slot_max && count != 0; i++) {
if (object_slots[i].validator) {
p_func(object_slots[i].object);
p_func(object_slots[i].object, p_user_data);
count--;
}
}
@ -2547,6 +2547,9 @@ void ObjectDB::cleanup() {
if (obj->is_class("Resource")) {
extra_info = " - Resource path: " + String(resource_get_path->call(obj, nullptr, 0, call_error));
}
if (obj->is_class("RefCounted")) {
extra_info = " - Reference count: " + itos((static_cast<RefCounted *>(obj))->get_reference_count());
}
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);
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.

View file

@ -1094,7 +1094,7 @@ class ObjectDB {
static void setup();
public:
typedef void (*DebugFunc)(Object *p_obj);
typedef void (*DebugFunc)(Object *p_obj, void *p_user_data);
_ALWAYS_INLINE_ static Object *get_instance(ObjectID p_instance_id) {
uint64_t id = p_instance_id;
@ -1126,6 +1126,6 @@ public:
template <typename T>
_ALWAYS_INLINE_ static Ref<T> get_ref(ObjectID p_instance_id); // Defined in ref_counted.h
static void debug_objects(DebugFunc p_func);
static void debug_objects(DebugFunc p_func, void *p_user_data);
static int get_object_count();
};

View file

@ -124,6 +124,13 @@
Returns column title language code.
</description>
</method>
<method name="get_column_title_tooltip_text" qualifiers="const">
<return type="String" />
<param index="0" name="column" type="int" />
<description>
Returns the column title's tooltip text.
</description>
</method>
<method name="get_column_width" qualifiers="const">
<return type="int" />
<param index="0" name="column" type="int" />
@ -322,6 +329,14 @@
Sets language code of column title used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
</description>
</method>
<method name="set_column_title_tooltip_text">
<return type="void" />
<param index="0" name="column" type="int" />
<param index="1" name="tooltip_text" type="String" />
<description>
Sets the column title's tooltip text.
</description>
</method>
<method name="set_selected">
<return type="void" />
<param index="0" name="item" type="TreeItem" />

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python
from misc.utility.scons_hints import *
Import("env")
Import("env_modules")
env_objdb = env_modules.Clone()
module_obj = []
# Only include in editor and debug builds.
if env_objdb.debug_features:
env_objdb.add_source_files(module_obj, "*.cpp")
# Only the editor needs these files, don't include them in the game.
if env.editor_build:
env_objdb.add_source_files(module_obj, "editor/*.cpp")
env_objdb.add_source_files(module_obj, "editor/data_viewers/*.cpp")
env.modules_sources += module_obj

View file

@ -0,0 +1,6 @@
def can_build(env, platform):
return env.debug_features
def configure(env):
pass

View file

@ -0,0 +1,282 @@
/**************************************************************************/
/* class_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "class_view.h"
#include "shared_controls.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/split_container.h"
int ClassData::instance_count(GameStateSnapshot *p_snapshot) {
int count = 0;
for (const SnapshotDataObject *instance : instances) {
if (!p_snapshot || instance->snapshot == p_snapshot) {
count += 1;
}
}
return count;
}
int ClassData::get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot) {
if (!recursive_instance_count_cache.has(p_snapshot)) {
recursive_instance_count_cache[p_snapshot] = instance_count(p_snapshot);
for (const String &child : child_classes) {
recursive_instance_count_cache[p_snapshot] += p_all_classes[child].get_recursive_instance_count(p_all_classes, p_snapshot);
}
}
return recursive_instance_count_cache[p_snapshot];
}
SnapshotClassView::SnapshotClassView() {
set_name(TTRC("Classes"));
}
void SnapshotClassView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
HSplitContainer *classes_view = memnew(HSplitContainer);
add_child(classes_view);
classes_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
classes_view->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->set_split_offset(0);
VBoxContainer *class_list_column = memnew(VBoxContainer);
class_list_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_list_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->add_child(class_list_column);
class_tree = memnew(Tree);
TreeSortAndFilterBar *filter_bar = memnew(TreeSortAndFilterBar(class_tree, TTRC("Filter Classes")));
filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
TreeSortAndFilterBar::SortOptionIndexes default_sort;
if (!diff_data) {
default_sort = filter_bar->add_sort_option(TTRC("Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
} else {
filter_bar->add_sort_option(TTRC("A Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
filter_bar->add_sort_option(TTRC("B Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 2);
default_sort = filter_bar->add_sort_option(TTRC("Delta"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 3);
}
class_list_column->add_child(filter_bar);
class_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
class_tree->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
class_tree->set_hide_folding(false);
class_tree->set_hide_root(true);
class_tree->set_columns(diff_data ? 4 : 2);
class_tree->set_column_titles_visible(true);
class_tree->set_column_title(0, TTRC("Class"));
class_tree->set_column_expand(0, true);
class_tree->set_column_custom_minimum_width(0, 200 * EDSCALE);
class_tree->set_column_title(1, diff_data ? TTRC("A Count") : TTRC("Count"));
class_tree->set_column_expand(1, false);
if (diff_data) {
class_tree->set_column_title_tooltip_text(1, vformat(TTR("A: %s"), snapshot_data->name));
class_tree->set_column_title_tooltip_text(2, vformat(TTR("B: %s"), diff_data->name));
class_tree->set_column_title(2, TTRC("B Count"));
class_tree->set_column_expand(2, false);
class_tree->set_column_title(3, TTRC("Delta"));
class_tree->set_column_expand(3, false);
}
class_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_class_selected));
class_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
class_list_column->add_child(class_tree);
VSplitContainer *object_lists = memnew(VSplitContainer);
classes_view->add_child(object_lists);
object_lists->set_custom_minimum_size(Size2(150 * EDSCALE, 0));
object_lists->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_lists->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
if (!diff_data) {
object_lists->add_child(object_list = _make_object_list_tree(TTRC("Objects")));
} else {
object_lists->add_child(object_list = _make_object_list_tree(TTRC("A Objects")));
object_lists->add_child(diff_object_list = _make_object_list_tree(TTRC("B Objects")));
}
HashMap<String, ClassData> grouped_by_class;
grouped_by_class["Object"] = ClassData("Object", "");
_add_objects_to_class_map(grouped_by_class, snapshot_data);
if (diff_data != nullptr) {
_add_objects_to_class_map(grouped_by_class, diff_data);
}
grouped_by_class[""].tree_node = class_tree->create_item();
List<String> classes_todo;
for (const String &c : grouped_by_class[""].child_classes) {
classes_todo.push_front(c);
}
while (classes_todo.size() > 0) {
String next_class_name = classes_todo.front()->get();
classes_todo.pop_front();
ClassData &next = grouped_by_class[next_class_name];
ClassData &nexts_parent = grouped_by_class[next.parent_class_name];
next.tree_node = class_tree->create_item(nexts_parent.tree_node);
next.tree_node->set_text(0, next_class_name + " (" + String::num_int64(next.instance_count(snapshot_data)) + ")");
next.tree_node->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
int a_count = next.get_recursive_instance_count(grouped_by_class, snapshot_data);
next.tree_node->set_text(1, String::num_int64(a_count));
if (diff_data) {
int b_count = next.get_recursive_instance_count(grouped_by_class, diff_data);
next.tree_node->set_text(2, String::num_int64(b_count));
next.tree_node->set_text(3, String::num_int64(a_count - b_count));
}
next.tree_node->set_metadata(0, next_class_name);
for (const String &c : next.child_classes) {
classes_todo.push_front(c);
}
}
// Icons won't load until the frame after show_snapshot is called. Not sure why, but just defer the load.
callable_mp(this, &SnapshotClassView::_notification).call_deferred(NOTIFICATION_THEME_CHANGED);
// Default to sort by descending count. Putting the biggest groups at the top is generally pretty interesting.
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
}
Tree *SnapshotClassView::_make_object_list_tree(const String &p_column_name) {
Tree *list = memnew(Tree);
list->set_select_mode(Tree::SelectMode::SELECT_ROW);
list->set_hide_folding(true);
list->set_hide_root(true);
list->set_columns(1);
list->set_column_titles_visible(true);
list->set_column_title(0, p_column_name);
list->set_column_expand(0, true);
list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_object_selected).bind(list));
list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
return list;
}
void SnapshotClassView::_add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_objects->objects) {
StringName class_name = pair.value->type_name;
StringName parent_class_name = !class_name.is_empty() && ClassDB::class_exists(class_name) ? ClassDB::get_parent_class(class_name) : "";
p_class_map[class_name].instances.push_back(pair.value);
// Go up the tree and insert all parents/grandparents.
while (!class_name.is_empty()) {
if (!p_class_map.has(class_name)) {
p_class_map[class_name] = ClassData(class_name, parent_class_name);
}
if (!p_class_map.has(parent_class_name)) {
// Leave our grandparent blank for now. Next iteration of the while loop will fill it in.
p_class_map[parent_class_name] = ClassData(parent_class_name, "");
}
p_class_map[class_name].parent_class_name = parent_class_name;
p_class_map[parent_class_name].child_classes.insert(class_name);
class_name = parent_class_name;
parent_class_name = !class_name.is_empty() ? ClassDB::get_parent_class(class_name) : "";
}
}
}
void SnapshotClassView::_object_selected(Tree *p_tree) {
GameStateSnapshot *snapshot = snapshot_data;
if (diff_data) {
Tree *other = p_tree == diff_object_list ? object_list : diff_object_list;
TreeItem *selected = other->get_selected();
if (selected) {
selected->deselect(0);
}
if (p_tree == diff_object_list) {
snapshot = diff_data;
}
}
ObjectID object_id = p_tree->get_selected()->get_metadata(0);
EditorNode::get_singleton()->push_item(static_cast<Object *>(snapshot->objects[object_id]));
}
void SnapshotClassView::_class_selected() {
_update_lists();
}
void SnapshotClassView::_populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base) {
p_list->clear();
TreeItem *selected_item = class_tree->get_selected();
if (selected_item == nullptr) {
p_list->set_column_title(0, vformat("%s (0)", TTR(p_name_base)));
return;
}
String class_name = selected_item->get_metadata(0);
TreeItem *root = p_list->create_item();
int object_count = 0;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->type_name == class_name) {
TreeItem *item = p_list->create_item(root);
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
item->set_text(0, pair.value->get_name());
item->set_metadata(0, pair.value->remote_object_id);
item->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
object_count++;
}
}
p_list->set_column_title(0, vformat("%s (%d)", TTR(p_name_base), object_count));
}
void SnapshotClassView::_update_lists() {
if (snapshot_data == nullptr) {
return;
}
if (!diff_data) {
_populate_object_list(snapshot_data, object_list, TTRC("Objects"));
} else {
_populate_object_list(snapshot_data, object_list, TTRC("A Objects"));
_populate_object_list(diff_data, diff_object_list, TTRC("B Objects"));
}
}
void SnapshotClassView::_notification(int p_what) {
if (p_what == NOTIFICATION_THEME_CHANGED) {
for (TreeItem *item : _get_children_recursive(class_tree)) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(item->get_metadata(0), ""));
}
} else if (p_what == NOTIFICATION_TRANSLATION_CHANGED) {
_update_lists();
}
}

View file

@ -0,0 +1,74 @@
/**************************************************************************/
/* class_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "snapshot_view.h"
class Tree;
class TreeItem;
struct ClassData {
ClassData() {}
ClassData(const String &p_name, const String &p_parent) :
class_name(p_name), parent_class_name(p_parent) {}
String class_name;
String parent_class_name;
HashSet<String> child_classes;
LocalVector<SnapshotDataObject *> instances;
TreeItem *tree_node = nullptr;
HashMap<GameStateSnapshot *, int> recursive_instance_count_cache;
int instance_count(GameStateSnapshot *p_snapshot = nullptr);
int get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot = nullptr);
};
class SnapshotClassView : public SnapshotView {
GDCLASS(SnapshotClassView, SnapshotView);
protected:
Tree *class_tree = nullptr;
Tree *object_list = nullptr;
Tree *diff_object_list = nullptr;
void _object_selected(Tree *p_tree);
void _class_selected();
void _add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects);
void _notification(int p_what);
Tree *_make_object_list_tree(const String &p_column_name);
void _populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base);
void _update_lists();
public:
SnapshotClassView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};

View file

@ -0,0 +1,275 @@
/**************************************************************************/
/* node_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "node_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/check_button.h"
#include "scene/gui/popup_menu.h"
#include "scene/gui/split_container.h"
SnapshotNodeView::SnapshotNodeView() {
set_name(TTRC("Nodes"));
}
void SnapshotNodeView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
HSplitContainer *diff_sides = memnew(HSplitContainer);
diff_sides->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
add_child(diff_sides);
main_tree = _make_node_tree(diff_data && !combined_diff_view ? TTRC("A Nodes") : TTRC("Nodes"));
diff_sides->add_child(main_tree.root);
_add_snapshot_to_tree(main_tree.tree, snapshot_data, diff_data && combined_diff_view ? DIFF_GROUP_REMOVED : DIFF_GROUP_NONE);
if (diff_data) {
CheckButton *diff_mode_toggle = memnew(CheckButton(TTRC("Combine Diff")));
diff_mode_toggle->set_pressed(combined_diff_view);
diff_mode_toggle->connect(SceneStringName(toggled), callable_mp(this, &SnapshotNodeView::_toggle_diff_mode));
main_tree.filter_bar->add_child(diff_mode_toggle);
main_tree.filter_bar->move_child(diff_mode_toggle, 0);
if (combined_diff_view) {
// Merge the snapshots together and add a diff.
_add_snapshot_to_tree(main_tree.tree, diff_data, DIFF_GROUP_ADDED);
} else {
// Add a second column with the diff snapshot.
diff_tree = _make_node_tree(TTRC("B Nodes"));
diff_sides->add_child(diff_tree.root);
_add_snapshot_to_tree(diff_tree.tree, diff_data, DIFF_GROUP_NONE);
}
}
_refresh_icons();
main_tree.filter_bar->apply();
if (diff_tree.filter_bar) {
diff_tree.filter_bar->apply();
diff_sides->set_split_offset(diff_sides->get_size().x * 0.5);
}
choose_object_menu = memnew(PopupMenu);
add_child(choose_object_menu);
choose_object_menu->connect(SceneStringName(id_pressed), callable_mp(this, &SnapshotNodeView::_choose_object_pressed).bind(false));
}
NodeTreeElements SnapshotNodeView::_make_node_tree(const String &p_tree_name) {
NodeTreeElements elements;
elements.root = memnew(VBoxContainer);
elements.root->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
elements.tree = memnew(Tree);
elements.filter_bar = memnew(TreeSortAndFilterBar(elements.tree, TTRC("Filter Nodes")));
elements.root->add_child(elements.filter_bar);
elements.tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
elements.tree->set_custom_minimum_size(Size2(150, 0) * EDSCALE);
elements.tree->set_hide_folding(false);
elements.root->add_child(elements.tree);
elements.tree->set_hide_root(true);
elements.tree->set_allow_reselect(true);
elements.tree->set_columns(1);
elements.tree->set_column_titles_visible(true);
elements.tree->set_column_title(0, p_tree_name);
elements.tree->set_column_expand(0, true);
elements.tree->set_column_clip_content(0, false);
elements.tree->set_column_custom_minimum_width(0, 150 * EDSCALE);
elements.tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotNodeView::_node_selected).bind(elements.tree));
elements.tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
elements.tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
elements.tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
elements.tree->create_item();
return elements;
}
void SnapshotNodeView::_node_selected(Tree *p_tree_selected_from) {
active_tree = p_tree_selected_from;
if (diff_tree.tree) {
// Deselect nodes in non-active tree, if needed.
if (active_tree == main_tree.tree) {
diff_tree.tree->deselect_all();
}
if (active_tree == diff_tree.tree) {
main_tree.tree->deselect_all();
}
}
const LocalVector<SnapshotDataObject *> &item_data = tree_item_data[p_tree_selected_from->get_selected()];
if (item_data.is_empty()) {
return;
} else if (item_data.size() == 1) {
EditorNode::get_singleton()->push_item(static_cast<Object *>(item_data[0]));
} else if (item_data.size() == 2) {
// This happens if we're in the combined diff view and the node exists in both trees
// The user has to specify which version of the node they want to see in the inspector.
_show_choose_object_menu();
}
}
void SnapshotNodeView::_toggle_diff_mode(bool p_state) {
combined_diff_view = p_state;
show_snapshot(snapshot_data, diff_data); // Redraw everything when we toggle views.
}
void SnapshotNodeView::_notification(int p_what) {
if (p_what == NOTIFICATION_THEME_CHANGED) {
_refresh_icons();
}
}
void SnapshotNodeView::_add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, DiffGroup p_diff_group) {
SnapshotDataObject *scene_root = nullptr;
LocalVector<SnapshotDataObject *> orphan_nodes;
for (const KeyValue<ObjectID, SnapshotDataObject *> &kv : p_snapshot->objects) {
if (kv.value->is_node() && !kv.value->extra_debug_data.has("node_parent")) {
if (kv.value->extra_debug_data["node_is_scene_root"]) {
scene_root = kv.value;
} else {
orphan_nodes.push_back(kv.value);
}
}
}
if (scene_root != nullptr) {
TreeItem *root_item = _add_item_to_tree(p_tree, p_tree->get_root(), scene_root, p_diff_group);
_add_children_to_tree(root_item, scene_root, p_diff_group);
}
if (!orphan_nodes.is_empty()) {
TreeItem *orphans_item = _add_item_to_tree(p_tree, p_tree->get_root(), TTRC("Orphan Nodes"), p_diff_group);
for (SnapshotDataObject *orphan_node : orphan_nodes) {
TreeItem *orphan_item = _add_item_to_tree(p_tree, orphans_item, orphan_node, p_diff_group);
_add_children_to_tree(orphan_item, orphan_node, p_diff_group);
}
}
}
void SnapshotNodeView::_add_children_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, DiffGroup p_diff_group) {
for (const Variant &child_id : (Array)p_data->extra_debug_data["node_children"]) {
SnapshotDataObject *child_object = p_data->snapshot->objects[ObjectID((uint64_t)child_id)];
TreeItem *child_item = _add_item_to_tree(p_parent_item->get_tree(), p_parent_item, child_object, p_diff_group);
_add_children_to_tree(child_item, child_object, p_diff_group);
}
}
TreeItem *SnapshotNodeView::_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, const String &p_item_name, DiffGroup p_diff_group) {
// Find out if this node already exists.
TreeItem *item = nullptr;
if (p_diff_group != DIFF_GROUP_NONE) {
for (int idx = 0; idx < p_parent->get_child_count(); idx++) {
TreeItem *child = p_parent->get_child(idx);
if (child->get_text(0) == p_item_name) {
item = child;
break;
}
}
}
if (item) {
// If it exists, clear the background color because we now know it exists in both trees.
item->clear_custom_bg_color(0);
} else {
// Add the new node and set its background color to green or red depending on which snapshot it's a part of.
item = p_tree->create_item(p_parent);
if (p_diff_group == DIFF_GROUP_ADDED) {
item->set_custom_bg_color(0, Color(0, 1, 0, 0.1));
} else if (p_diff_group == DIFF_GROUP_REMOVED) {
item->set_custom_bg_color(0, Color(1, 0, 0, 0.1));
}
}
item->set_text(0, p_item_name);
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
return item;
}
TreeItem *SnapshotNodeView::_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, SnapshotDataObject *p_data, DiffGroup p_diff_group) {
String node_name = p_data->extra_debug_data["node_name"];
TreeItem *child_item = _add_item_to_tree(p_tree, p_parent, node_name, p_diff_group);
tree_item_data[child_item].push_back(p_data);
return child_item;
}
void SnapshotNodeView::_refresh_icons() {
for (TreeItem *item : _get_children_recursive(main_tree.tree)) {
HashMap<TreeItem *, LocalVector<SnapshotDataObject *>>::Iterator E = tree_item_data.find(item);
if (E && !E->value.is_empty()) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(E->value[0]->type_name));
} else {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon("MissingNode"));
}
}
if (diff_tree.tree) {
for (TreeItem *item : _get_children_recursive(diff_tree.tree)) {
HashMap<TreeItem *, LocalVector<SnapshotDataObject *>>::Iterator E = tree_item_data.find(item);
if (E && !E->value.is_empty()) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(E->value[0]->type_name));
} else {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon("MissingNode"));
}
}
}
}
void SnapshotNodeView::clear_snapshot() {
SnapshotView::clear_snapshot();
tree_item_data.clear();
main_tree.tree = nullptr;
main_tree.filter_bar = nullptr;
main_tree.root = nullptr;
diff_tree.tree = nullptr;
diff_tree.filter_bar = nullptr;
diff_tree.root = nullptr;
active_tree = nullptr;
}
void SnapshotNodeView::_choose_object_pressed(int p_object_idx, bool p_confirm_override) {
EditorNode::get_singleton()->push_item(static_cast<Object *>(tree_item_data[active_tree->get_selected()][p_object_idx]));
}
void SnapshotNodeView::_show_choose_object_menu() {
remove_child(choose_object_menu);
add_child(choose_object_menu);
choose_object_menu->clear(false);
choose_object_menu->add_item(TTRC("Snapshot A"), 0);
choose_object_menu->add_item(TTRC("Snapshot B"), 1);
choose_object_menu->reset_size();
choose_object_menu->set_position(get_screen_position() + get_local_mouse_position());
choose_object_menu->popup();
}

View file

@ -0,0 +1,86 @@
/**************************************************************************/
/* node_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
// When diffing in split view, we have two trees/filters
// so this struct is used to group their properties together.
struct NodeTreeElements {
NodeTreeElements() {
tree = nullptr;
filter_bar = nullptr;
root = nullptr;
}
Tree *tree = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
VBoxContainer *root = nullptr;
};
class SnapshotNodeView : public SnapshotView {
GDCLASS(SnapshotNodeView, SnapshotView);
enum DiffGroup {
DIFF_GROUP_NONE,
DIFF_GROUP_ADDED,
DIFF_GROUP_REMOVED
};
NodeTreeElements main_tree;
NodeTreeElements diff_tree;
Tree *active_tree = nullptr;
PopupMenu *choose_object_menu = nullptr;
bool combined_diff_view = true;
HashMap<TreeItem *, LocalVector<SnapshotDataObject *>> tree_item_data;
void _node_selected(Tree *p_tree_selected_from);
void _notification(int p_what);
NodeTreeElements _make_node_tree(const String &p_tree_name);
void _apply_filters();
void _refresh_icons();
void _toggle_diff_mode(bool p_state);
void _choose_object_pressed(int p_object_idx, bool p_confirm_override);
void _show_choose_object_menu();
void _add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, DiffGroup p_diff_group = DIFF_GROUP_NONE);
void _add_children_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, DiffGroup p_diff_group = DIFF_GROUP_NONE);
TreeItem *_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, const String &p_item_name, DiffGroup p_diff_group = DIFF_GROUP_NONE);
TreeItem *_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, SnapshotDataObject *p_data, DiffGroup p_diff_group = DIFF_GROUP_NONE);
public:
SnapshotNodeView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
virtual void clear_snapshot() override;
};

View file

@ -0,0 +1,260 @@
/**************************************************************************/
/* object_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "object_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/split_container.h"
SnapshotObjectView::SnapshotObjectView() {
set_name(TTRC("Objects"));
}
void SnapshotObjectView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
item_data_map.clear();
data_item_map.clear();
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
objects_view = memnew(HSplitContainer);
add_child(objects_view);
objects_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VBoxContainer *object_column = memnew(VBoxContainer);
object_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
objects_view->add_child(object_column);
object_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list = memnew(Tree);
filter_bar = memnew(TreeSortAndFilterBar(object_list, TTRC("Filter Objects")));
object_column->add_child(filter_bar);
int sort_idx = 0;
if (diff_data) {
filter_bar->add_sort_option(TTRC("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
}
filter_bar->add_sort_option(TTRC("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
filter_bar->add_sort_option(TTRC("Inbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
TTRC("Outbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
// Tree of objects.
object_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
object_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
object_list->set_hide_folding(false);
object_column->add_child(object_list);
object_list->set_hide_root(true);
object_list->set_columns(diff_data ? 5 : 4);
object_list->set_column_titles_visible(true);
int offset = 0;
if (diff_data) {
object_list->set_column_title(0, TTRC("Snapshot"));
object_list->set_column_expand(0, false);
object_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
offset++;
}
object_list->set_column_title(offset + 0, TTRC("Class"));
object_list->set_column_expand(offset + 0, true);
object_list->set_column_title_tooltip_text(offset + 0, TTRC("Object's class"));
object_list->set_column_title(offset + 1, TTRC("Object"));
object_list->set_column_expand(offset + 1, true);
object_list->set_column_expand_ratio(offset + 1, 2);
object_list->set_column_title_tooltip_text(offset + 1, TTRC("Object's name"));
object_list->set_column_title(offset + 2, TTRC("In"));
object_list->set_column_expand(offset + 2, false);
object_list->set_column_clip_content(offset + 2, false);
object_list->set_column_title_tooltip_text(offset + 2, TTRC("Number of inbound references"));
object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
object_list->set_column_title(offset + 3, TTRC("Out"));
object_list->set_column_expand(offset + 3, false);
object_list->set_column_clip_content(offset + 3, false);
object_list->set_column_title_tooltip_text(offset + 3, TTRC("Number of outbound references"));
object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
object_list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_object_selected));
object_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details = memnew(VBoxContainer);
object_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
objects_view->add_child(object_details);
object_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list->create_item();
_insert_data(snapshot_data, TTRC("A"));
if (diff_data) {
_insert_data(diff_data, TTRC("B"));
}
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
object_list->set_selected(object_list->get_root()->get_first_child());
// Expand the left panel as wide as we can. Passing `INT_MAX` or any very large int will have the opposite effect
// and shrink the left panel as small as it can go. So, pass an int we know is larger than the current panel, but not
// 'very' large (whatever that exact number is).
objects_view->set_split_offset(get_viewport_rect().size.x);
}
void SnapshotObjectView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
TreeItem *item = object_list->create_item(object_list->get_root());
int offset = 0;
if (diff_data) {
item->set_text(0, p_name);
item->set_tooltip_text(0, p_snapshot->name);
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
offset = 1;
}
item->set_auto_translate_mode(offset + 0, AUTO_TRANSLATE_MODE_DISABLED);
item->set_auto_translate_mode(offset + 1, AUTO_TRANSLATE_MODE_DISABLED);
item->set_text(offset + 0, pair.value->type_name);
item->set_text(offset + 1, pair.value->get_name());
item->set_text(offset + 2, String::num_uint64(pair.value->inbound_references.size()));
item->set_text(offset + 3, String::num_uint64(pair.value->outbound_references.size()));
item_data_map[item] = pair.value;
data_item_map[pair.value] = item;
}
}
void SnapshotObjectView::_object_selected() {
reference_item_map.clear();
for (int i = 0; i < object_details->get_child_count(); i++) {
object_details->get_child(i)->queue_free();
}
SnapshotDataObject *d = item_data_map[object_list->get_selected()];
EditorNode::get_singleton()->push_item(static_cast<Object *>(d));
DarkPanelContainer *object_panel = memnew(DarkPanelContainer);
VBoxContainer *object_panel_content = memnew(VBoxContainer);
object_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details->add_child(object_panel);
object_panel->add_child(object_panel_content);
object_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
ScrollContainer *properties_scroll = memnew(ScrollContainer);
properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_panel_content->add_child(properties_scroll);
VBoxContainer *properties_container = memnew(VBoxContainer);
properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->add_child(properties_container);
properties_container->add_theme_constant_override("separation", 8);
inbound_tree = _make_references_list(properties_container, TTRC("Inbound References"), TTRC("Source"), TTRC("Other object referencing this object"), TTRC("Property"), TTRC("Property of other object referencing this object"));
inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(inbound_tree));
TreeItem *ib_root = inbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
TreeItem *i = inbound_tree->create_item(ib_root);
i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, target->get_name());
i->set_text(1, ob.key);
reference_item_map[i] = data_item_map[target];
}
outbound_tree = _make_references_list(properties_container, TTRC("Outbound References"), TTRC("Property"), TTRC("Property of this object referencing other object"), TTRC("Target"), TTRC("Other object being referenced"));
outbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(outbound_tree));
TreeItem *ob_root = outbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->outbound_references) {
TreeItem *i = outbound_tree->create_item(ob_root);
i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, ob.key);
i->set_text(1, target->get_name());
reference_item_map[i] = data_item_map[target];
}
}
void SnapshotObjectView::_reference_selected(Tree *p_source_tree) {
TreeItem *ref_item = p_source_tree->get_selected();
Tree *other_tree = p_source_tree == inbound_tree ? outbound_tree : inbound_tree;
other_tree->deselect_all();
TreeItem *other = reference_item_map[ref_item];
if (other) {
if (!other->is_visible()) {
// Clear the filter if we can't see the node we just chose.
filter_bar->clear_filter();
}
other->get_tree()->deselect_all();
other->get_tree()->set_selected(other);
other->get_tree()->ensure_cursor_is_visible();
}
}
Tree *SnapshotObjectView::_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip) {
VBoxContainer *vbox = memnew(VBoxContainer);
vbox->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
vbox->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
vbox->add_theme_constant_override("separation", 4);
p_container->add_child(vbox);
vbox->set_custom_minimum_size(Vector2(300, 0) * EDSCALE);
RichTextLabel *lbl = memnew(RichTextLabel("[center]" + p_name + "[center]"));
lbl->set_fit_content(true);
lbl->set_use_bbcode(true);
vbox->add_child(lbl);
Tree *tree = memnew(Tree);
tree->set_hide_folding(true);
vbox->add_child(tree);
tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
tree->set_hide_root(true);
tree->set_columns(2);
tree->set_column_titles_visible(true);
tree->set_column_title(0, p_col_1);
tree->set_column_expand(0, true);
tree->set_column_title_tooltip_text(0, p_col_1_tooltip);
tree->set_column_clip_content(0, false);
tree->set_column_title(1, p_col_2);
tree->set_column_expand(1, true);
tree->set_column_clip_content(1, false);
tree->set_column_title_tooltip_text(1, p_col_2_tooltip);
tree->set_v_scroll_enabled(false);
tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
return tree;
}

View file

@ -0,0 +1,63 @@
/**************************************************************************/
/* object_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
class HSplitContainer;
class SnapshotObjectView : public SnapshotView {
GDCLASS(SnapshotObjectView, SnapshotView);
protected:
Tree *object_list = nullptr;
Tree *inbound_tree = nullptr;
Tree *outbound_tree = nullptr;
VBoxContainer *object_details = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
HSplitContainer *objects_view = nullptr;
HashMap<TreeItem *, SnapshotDataObject *> item_data_map;
HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
HashMap<TreeItem *, TreeItem *> reference_item_map;
void _object_selected();
void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
Tree *_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip);
void _reference_selected(Tree *p_source_tree);
public:
SnapshotObjectView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};

View file

@ -0,0 +1,323 @@
/**************************************************************************/
/* refcounted_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "refcounted_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/split_container.h"
SnapshotRefCountedView::SnapshotRefCountedView() {
set_name(TTRC("RefCounted"));
}
void SnapshotRefCountedView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
item_data_map.clear();
data_item_map.clear();
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_view = memnew(HSplitContainer);
add_child(refs_view);
refs_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VBoxContainer *refs_column = memnew(VBoxContainer);
refs_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
refs_view->add_child(refs_column);
// Tree of Refs.
refs_list = memnew(Tree);
filter_bar = memnew(TreeSortAndFilterBar(refs_list, TTRC("Filter RefCounteds")));
refs_column->add_child(filter_bar);
int offset = diff_data ? 1 : 0;
if (diff_data) {
filter_bar->add_sort_option(TTRC("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
}
filter_bar->add_sort_option(TTRC("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 0);
filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 1);
TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
TTRC("Native Refs"),
TreeSortAndFilterBar::SortType::NUMERIC_SORT,
offset + 2);
filter_bar->add_sort_option(TTRC("ObjectDB Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 3);
filter_bar->add_sort_option(TTRC("Total Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 4);
filter_bar->add_sort_option(TTRC("ObjectDB Cycles"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 5);
refs_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
refs_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
refs_list->set_hide_folding(false);
refs_column->add_child(refs_list);
refs_list->set_hide_root(true);
refs_list->set_columns(diff_data ? 7 : 6);
refs_list->set_column_titles_visible(true);
if (diff_data) {
refs_list->set_column_title(0, TTRC("Snapshot"));
refs_list->set_column_expand(0, false);
refs_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
}
refs_list->set_column_title(offset + 0, TTRC("Class"));
refs_list->set_column_expand(offset + 0, true);
refs_list->set_column_title_tooltip_text(offset + 0, TTRC("Object's class"));
refs_list->set_column_title(offset + 1, TTRC("Name"));
refs_list->set_column_expand(offset + 1, true);
refs_list->set_column_expand_ratio(offset + 1, 2);
refs_list->set_column_title_tooltip_text(offset + 1, TTRC("Object's name"));
refs_list->set_column_title(offset + 2, TTRC("Native Refs"));
refs_list->set_column_expand(offset + 2, false);
refs_list->set_column_title_tooltip_text(offset + 2, TTRC("References not owned by the ObjectDB"));
refs_list->set_column_title(offset + 3, TTRC("ObjectDB Refs"));
refs_list->set_column_expand(offset + 3, false);
refs_list->set_column_title_tooltip_text(offset + 3, TTRC("References owned by the ObjectDB"));
refs_list->set_column_title(offset + 4, TTRC("Total Refs"));
refs_list->set_column_expand(offset + 4, false);
refs_list->set_column_title_tooltip_text(offset + 4, TTRC("ObjectDB References + Native References"));
refs_list->set_column_title(offset + 5, TTRC("ObjectDB Cycles"));
refs_list->set_column_expand(offset + 5, false);
refs_list->set_column_title_tooltip_text(offset + 5, TTRC("Cycles detected in the ObjectDB"));
refs_list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotRefCountedView::_refcounted_selected));
refs_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
// View of the selected refcounted.
ref_details = memnew(VBoxContainer);
ref_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
refs_view->add_child(ref_details);
ref_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
ref_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_list->create_item();
_insert_data(snapshot_data, TTRC("A"));
if (diff_data) {
_insert_data(diff_data, TTRC("B"));
}
// Push the split as far right as possible.
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
refs_list->set_selected(refs_list->get_root()->get_first_child());
callable_mp(this, &SnapshotRefCountedView::_set_split_to_center).call_deferred();
}
void SnapshotRefCountedView::_set_split_to_center() {
refs_view->set_split_offset(refs_view->get_size().x * 0.5);
}
void SnapshotRefCountedView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (!pair.value->is_refcounted()) {
continue;
}
TreeItem *item = refs_list->create_item(refs_list->get_root());
item_data_map[item] = pair.value;
data_item_map[pair.value] = item;
int total_refs = pair.value->extra_debug_data.has("ref_count") ? (uint64_t)pair.value->extra_debug_data["ref_count"] : 0;
int objectdb_refs = pair.value->get_unique_inbound_references().size();
int native_refs = total_refs - objectdb_refs;
Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
int offset = 0;
if (diff_data) {
item->set_text(0, p_name);
item->set_tooltip_text(0, p_snapshot->name);
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
offset = 1;
}
item->set_text(offset + 0, pair.value->type_name);
item->set_auto_translate_mode(offset + 0, AUTO_TRANSLATE_MODE_DISABLED);
item->set_text(offset + 1, pair.value->get_name());
item->set_auto_translate_mode(offset + 1, AUTO_TRANSLATE_MODE_DISABLED);
item->set_text(offset + 2, String::num_uint64(native_refs));
item->set_text(offset + 3, String::num_uint64(objectdb_refs));
item->set_text(offset + 4, String::num_uint64(total_refs));
item->set_text(offset + 5, String::num_uint64(ref_cycles.size())); // Compute cycles and attach it to refcounted object.
if (total_refs == ref_cycles.size()) {
// Often, references are held by the engine so we can't know if we're stuck in a cycle or not
// But if the full cycle is visible in the ObjectDB,
// tell the user by highlighting the cells in red.
item->set_custom_bg_color(offset + 5, Color(1, 0, 0, 0.1));
}
}
}
void SnapshotRefCountedView::_refcounted_selected() {
for (int i = 0; i < ref_details->get_child_count(); i++) {
ref_details->get_child(i)->queue_free();
}
SnapshotDataObject *d = item_data_map[refs_list->get_selected()];
EditorNode::get_singleton()->push_item(static_cast<Object *>(d));
DarkPanelContainer *refcounted_panel = memnew(DarkPanelContainer);
VBoxContainer *refcounted_panel_content = memnew(VBoxContainer);
refcounted_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refcounted_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
ref_details->add_child(refcounted_panel);
refcounted_panel->add_child(refcounted_panel_content);
refcounted_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
ScrollContainer *properties_scroll = memnew(ScrollContainer);
properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refcounted_panel_content->add_child(properties_scroll);
VBoxContainer *properties_container = memnew(VBoxContainer);
properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->add_child(properties_container);
properties_container->add_theme_constant_override("separation", 5);
properties_container->add_theme_constant_override("margin_left", 2);
properties_container->add_theme_constant_override("margin_right", 2);
properties_container->add_theme_constant_override("margin_top", 2);
properties_container->add_theme_constant_override("margin_bottom", 2);
int total_refs = d->extra_debug_data.has("ref_count") ? (uint64_t)d->extra_debug_data["ref_count"] : 0;
int objectdb_refs = d->get_unique_inbound_references().size();
int native_refs = total_refs - objectdb_refs;
Array ref_cycles = (Array)d->extra_debug_data["ref_cycles"];
String count_str = "[ul]\n";
count_str += vformat(TTRC("Native References: %d\n"), native_refs);
count_str += vformat(TTRC("ObjectDB References: %d\n"), objectdb_refs);
count_str += vformat(TTRC("Total References: %d\n"), total_refs);
count_str += vformat(TTRC("ObjectDB Cycles: %d\n"), ref_cycles.size());
count_str += "[/ul]\n";
RichTextLabel *counts = memnew(RichTextLabel(count_str));
counts->set_use_bbcode(true);
counts->set_fit_content(true);
counts->add_theme_constant_override("line_separation", 6);
properties_container->add_child(counts);
if (d->inbound_references.size() > 0) {
RichTextLabel *inbound_lbl = memnew(RichTextLabel(TTRC("[center]ObjectDB References[center]")));
inbound_lbl->set_fit_content(true);
inbound_lbl->set_use_bbcode(true);
properties_container->add_child(inbound_lbl);
Tree *inbound_tree = memnew(Tree);
inbound_tree->set_hide_folding(true);
properties_container->add_child(inbound_tree);
inbound_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
inbound_tree->set_hide_root(true);
inbound_tree->set_columns(3);
inbound_tree->set_column_titles_visible(true);
inbound_tree->set_column_title(0, TTRC("Source"));
inbound_tree->set_column_expand(0, true);
inbound_tree->set_column_clip_content(0, false);
inbound_tree->set_column_title_tooltip_text(0, TTRC("Other object referencing this object"));
inbound_tree->set_column_title(1, TTRC("Property"));
inbound_tree->set_column_expand(1, true);
inbound_tree->set_column_clip_content(1, true);
inbound_tree->set_column_title_tooltip_text(1, TTRC("Property of other object referencing this object"));
inbound_tree->set_column_title(2, TTRC("Duplicate?"));
inbound_tree->set_column_expand(2, false);
inbound_tree->set_column_title_tooltip_text(2, TTRC("Was the same reference returned by multiple getters on the source object?"));
inbound_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
inbound_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
inbound_tree->set_v_scroll_enabled(false);
inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotRefCountedView::_ref_selected).bind(inbound_tree));
// The same reference can exist as multiple properties of an object (for example, gdscript `@export` properties exist twice).
// We flag for the user if a property is exposed multiple times so it's clearer why there are more references in the list
// than the ObjectDB References count would suggest.
HashMap<ObjectID, int> property_repeat_count;
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
if (!property_repeat_count.has(ob.value)) {
property_repeat_count.insert(ob.value, 0);
}
property_repeat_count[ob.value]++;
}
TreeItem *root = inbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
TreeItem *i = inbound_tree->create_item(root);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, target->get_name());
i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
i->set_text(1, ob.key);
i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
i->set_text(2, property_repeat_count[ob.value] > 1 ? TTRC("Yes") : TTRC("No"));
reference_item_map[i] = data_item_map[target];
}
}
if (ref_cycles.size() > 0) {
properties_container->add_child(memnew(SpanningHeader(TTRC("ObjectDB Cycles"))));
Tree *cycles_tree = memnew(Tree);
cycles_tree->set_hide_folding(true);
properties_container->add_child(cycles_tree);
cycles_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
cycles_tree->set_hide_root(true);
cycles_tree->set_columns(1);
cycles_tree->set_column_titles_visible(false);
cycles_tree->set_column_expand(0, true);
cycles_tree->set_column_clip_content(0, false);
cycles_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
cycles_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
cycles_tree->set_v_scroll_enabled(false);
TreeItem *root = cycles_tree->create_item();
for (const Variant &cycle : ref_cycles) {
TreeItem *i = cycles_tree->create_item(root);
i->set_text(0, cycle);
i->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
}
}
}
void SnapshotRefCountedView::_ref_selected(Tree *p_source_tree) {
TreeItem *target = reference_item_map[p_source_tree->get_selected()];
if (target) {
if (!target->is_visible()) {
// Clear the filter if we can't see the node we just chose.
filter_bar->clear_filter();
}
target->get_tree()->deselect_all();
target->get_tree()->set_selected(target);
target->get_tree()->ensure_cursor_is_visible();
}
}

View file

@ -0,0 +1,61 @@
/**************************************************************************/
/* refcounted_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
class HSplitContainer;
class SnapshotRefCountedView : public SnapshotView {
GDCLASS(SnapshotRefCountedView, SnapshotView);
protected:
Tree *refs_list = nullptr;
VBoxContainer *ref_details = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
HSplitContainer *refs_view = nullptr;
HashMap<TreeItem *, SnapshotDataObject *> item_data_map;
HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
HashMap<TreeItem *, TreeItem *> reference_item_map;
void _refcounted_selected();
void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
void _ref_selected(Tree *p_source_tree);
void _set_split_to_center();
public:
SnapshotRefCountedView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};

View file

@ -0,0 +1,241 @@
/**************************************************************************/
/* shared_controls.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "shared_controls.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/menu_button.h"
#include "scene/resources/style_box_flat.h"
SpanningHeader::SpanningHeader(const String &p_text) {
Ref<StyleBoxFlat> title_sbf;
title_sbf.instantiate();
title_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
add_theme_style_override(SceneStringName(panel), title_sbf);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
Label *title = memnew(Label(p_text));
add_child(title);
title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
}
DarkPanelContainer::DarkPanelContainer() {
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
Ref<StyleBoxFlat> content_wrapper_sbf;
content_wrapper_sbf.instantiate();
content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
add_theme_style_override(SceneStringName(panel), content_wrapper_sbf);
}
void TreeSortAndFilterBar::_apply_filter(TreeItem *p_current_node) {
if (!p_current_node) {
p_current_node = managed_tree->get_root();
}
if (!p_current_node) {
return;
}
// Reset ourselves to default state.
p_current_node->set_visible(true);
p_current_node->clear_custom_color(0);
// Go through each child and filter them.
bool any_child_visible = false;
for (TreeItem *child = p_current_node->get_first_child(); child; child = child->get_next()) {
_apply_filter(child);
if (child->is_visible()) {
any_child_visible = true;
}
}
// Check if we match the filter.
String filter_str = filter_edit->get_text().strip_edges(true, true).to_lower();
// We are visible.
bool matches_filter = false;
for (int i = 0; i < managed_tree->get_columns(); i++) {
if (p_current_node->get_text(i).to_lower().contains(filter_str)) {
matches_filter = true;
break;
}
}
if (matches_filter || filter_str.is_empty()) {
p_current_node->set_visible(true);
} else if (any_child_visible) {
// We have a visible child.
p_current_node->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
} else {
// We and our children are not visible.
p_current_node->set_visible(false);
}
}
void TreeSortAndFilterBar::_apply_sort() {
if (!sort_button->is_visible()) {
return;
}
for (int i = 0; i != sort_button->get_popup()->get_item_count(); i++) {
// Update the popup buttons to be checked/unchecked.
sort_button->get_popup()->set_item_checked(i, (i == (int)current_sort));
}
SortItem sort = sort_items[current_sort];
List<TreeItem *> items_to_sort;
items_to_sort.push_back(managed_tree->get_root());
while (items_to_sort.size() > 0) {
TreeItem *to_sort = items_to_sort.front()->get();
items_to_sort.pop_front();
LocalVector<TreeItemColumn> items;
items.reserve(to_sort->get_child_count());
for (int i = 0; i < to_sort->get_child_count(); i++) {
items.push_back(TreeItemColumn(to_sort->get_child(i), sort.column));
}
if (sort.type == ALPHA_SORT && sort.ascending == true) {
items.sort_custom<TreeItemAlphaComparator>();
}
if (sort.type == ALPHA_SORT && sort.ascending == false) {
items.sort_custom<TreeItemAlphaComparator>();
items.reverse();
}
if (sort.type == NUMERIC_SORT && sort.ascending == true) {
items.sort_custom<TreeItemNumericComparator>();
}
if (sort.type == NUMERIC_SORT && sort.ascending == false) {
items.sort_custom<TreeItemNumericComparator>();
items.reverse();
}
TreeItem *previous = nullptr;
for (const TreeItemColumn &item : items) {
if (previous != nullptr) {
item.item->move_after(previous);
} else {
item.item->move_before(to_sort->get_first_child());
}
previous = item.item;
items_to_sort.push_back(item.item);
}
}
}
void TreeSortAndFilterBar::_sort_changed(int p_id) {
current_sort = p_id;
_apply_sort();
}
void TreeSortAndFilterBar::_filter_changed(const String &p_filter) {
_apply_filter();
}
TreeSortAndFilterBar::TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text) :
managed_tree(p_managed_tree) {
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
add_theme_constant_override("h_separation", 10 * EDSCALE);
filter_edit = memnew(LineEdit);
filter_edit->set_clear_button_enabled(true);
filter_edit->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
filter_edit->set_placeholder(p_filter_placeholder_text);
add_child(filter_edit);
filter_edit->connect(SceneStringName(text_changed), callable_mp(this, &TreeSortAndFilterBar::_filter_changed));
sort_button = memnew(MenuButton);
sort_button->set_visible(false);
sort_button->set_flat(false);
sort_button->set_theme_type_variation("FlatMenuButton");
PopupMenu *p = sort_button->get_popup();
p->connect(SceneStringName(id_pressed), callable_mp(this, &TreeSortAndFilterBar::_sort_changed));
add_child(sort_button);
}
void TreeSortAndFilterBar::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE:
case NOTIFICATION_THEME_CHANGED: {
filter_edit->set_right_icon(get_editor_theme_icon(SNAME("Search")));
sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
apply();
} break;
}
}
TreeSortAndFilterBar::SortOptionIndexes TreeSortAndFilterBar::add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default) {
sort_button->set_visible(true);
bool is_first_item = sort_items.is_empty();
SortItem item_ascending(sort_items.size(), vformat(TTRC("Sort By %s (Ascending)"), p_new_option), p_sort_type, true, p_sort_column);
sort_items[item_ascending.id] = item_ascending;
sort_button->get_popup()->add_radio_check_item(item_ascending.label, item_ascending.id);
SortItem item_descending(sort_items.size(), vformat(TTRC("Sort By %s (Descending)"), p_new_option), p_sort_type, false, p_sort_column);
sort_items[item_descending.id] = item_descending;
sort_button->get_popup()->add_radio_check_item(item_descending.label, item_descending.id);
if (is_first_item) {
sort_button->get_popup()->set_item_checked(0, true);
}
SortOptionIndexes indexes;
indexes.ascending = item_ascending.id;
indexes.descending = item_descending.id;
return indexes;
}
void TreeSortAndFilterBar::clear_filter() {
filter_edit->clear();
}
void TreeSortAndFilterBar::clear() {
sort_button->set_visible(false);
sort_button->get_popup()->clear();
filter_edit->clear();
}
void TreeSortAndFilterBar::select_sort(int p_item_id) {
_sort_changed(p_item_id);
}
void TreeSortAndFilterBar::apply() {
if (!managed_tree || !managed_tree->get_root()) {
return;
}
_apply_sort();
_apply_filter();
}

View file

@ -0,0 +1,127 @@
/**************************************************************************/
/* shared_controls.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/box_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/tree.h"
class LineEdit;
class MenuButton;
class SpanningHeader : public PanelContainer {
GDCLASS(SpanningHeader, PanelContainer);
public:
SpanningHeader(const String &p_text);
};
class DarkPanelContainer : public PanelContainer {
GDCLASS(DarkPanelContainer, PanelContainer);
public:
DarkPanelContainer();
};
// Utility class that creates a filter text box and a sort menu.
// Takes a reference to a tree and applies the sort and filter to the tree.
class TreeSortAndFilterBar : public HBoxContainer {
GDCLASS(TreeSortAndFilterBar, HBoxContainer);
public:
// The ways a column can be sorted, either alphabetically or numerically.
enum SortType {
NUMERIC_SORT = 0,
ALPHA_SORT,
SORT_TYPE_MAX
};
// Returned when a new sort is added. Each new sort can be either ascending or descending,
// so we return the index of each sort option.
struct SortOptionIndexes {
int ascending;
int descending;
};
protected:
// Context needed to sort the tree in a certain way.
// Combines a sort type, the column to apply it, and if it's ascending or descending.
struct SortItem {
SortItem() {}
SortItem(int p_id, const String &p_label, SortType p_type, bool p_ascending, int p_column) :
id(p_id), label(p_label), type(p_type), ascending(p_ascending), column(p_column) {}
int id = 0;
String label;
SortType type = SortType::NUMERIC_SORT;
bool ascending = false;
int column = 0;
};
struct TreeItemColumn {
TreeItemColumn() {}
TreeItemColumn(TreeItem *p_item, int p_column) :
item(p_item), column(p_column) {}
TreeItem *item = nullptr;
int column;
};
struct TreeItemAlphaComparator {
bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
return NoCaseComparator()(p_a.item->get_text(p_a.column), p_b.item->get_text(p_b.column));
}
};
struct TreeItemNumericComparator {
bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
return p_a.item->get_text(p_a.column).to_int() < p_b.item->get_text(p_b.column).to_int();
}
};
LineEdit *filter_edit = nullptr;
MenuButton *sort_button = nullptr;
Tree *managed_tree = nullptr;
HashMap<int, SortItem> sort_items;
int current_sort = 0;
void _apply_filter(TreeItem *p_current_node = nullptr);
void _apply_sort();
void _sort_changed(int p_id);
void _filter_changed(const String &p_filter);
public:
TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text);
void _notification(int p_what);
SortOptionIndexes add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default = false);
void clear_filter();
void clear();
void select_sort(int p_item_id);
void apply();
};

View file

@ -0,0 +1,68 @@
/**************************************************************************/
/* snapshot_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "snapshot_view.h"
#include "scene/gui/tree.h"
void SnapshotView::clear_snapshot() {
snapshot_data = nullptr;
diff_data = nullptr;
for (int i = 0; i < get_child_count(); i++) {
get_child(i)->queue_free();
}
}
void SnapshotView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
clear_snapshot();
snapshot_data = p_data;
diff_data = p_diff_data;
}
bool SnapshotView::is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
return p_data == snapshot_data && p_diff_data == diff_data;
}
Vector<TreeItem *> SnapshotView::_get_children_recursive(Tree *p_tree) {
Vector<TreeItem *> found_items;
List<TreeItem *> items_to_check;
if (p_tree && p_tree->get_root()) {
items_to_check.push_back(p_tree->get_root());
}
while (items_to_check.size() > 0) {
TreeItem *to_check = items_to_check.front()->get();
items_to_check.pop_front();
found_items.push_back(to_check);
for (int i = 0; i < to_check->get_child_count(); i++) {
items_to_check.push_back(to_check->get_child(i));
}
}
return found_items;
}

View file

@ -0,0 +1,55 @@
/**************************************************************************/
/* snapshot_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "scene/gui/control.h"
class Tree;
class TreeItem;
class SnapshotView : public Control {
GDCLASS(SnapshotView, Control);
protected:
GameStateSnapshot *snapshot_data = nullptr;
GameStateSnapshot *diff_data = nullptr;
Vector<TreeItem *> _get_children_recursive(Tree *p_tree);
public:
String view_name;
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data = nullptr);
virtual void clear_snapshot();
bool is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data);
};

View file

@ -0,0 +1,284 @@
/**************************************************************************/
/* summary_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "summary_view.h"
#include "core/os/time.h"
#include "editor/editor_node.h"
#include "scene/gui/center_container.h"
#include "scene/gui/label.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/rich_text_label.h"
#include "scene/resources/style_box_flat.h"
SnapshotSummaryView::SnapshotSummaryView() {
set_name(TTRC("Summary"));
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
MarginContainer *mc = memnew(MarginContainer);
mc->add_theme_constant_override("margin_left", 5);
mc->add_theme_constant_override("margin_right", 5);
mc->add_theme_constant_override("margin_top", 5);
mc->add_theme_constant_override("margin_bottom", 5);
mc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
PanelContainer *content_wrapper = memnew(PanelContainer);
content_wrapper->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
Ref<StyleBoxFlat> content_wrapper_sbf;
content_wrapper_sbf.instantiate();
content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
content_wrapper->add_theme_style_override(SceneStringName(panel), content_wrapper_sbf);
content_wrapper->add_child(mc);
add_child(content_wrapper);
VBoxContainer *content = memnew(VBoxContainer);
mc->add_child(content);
content->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
PanelContainer *pc = memnew(PanelContainer);
Ref<StyleBoxFlat> sbf;
sbf.instantiate();
sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
pc->add_theme_style_override("panel", sbf);
content->add_child(pc);
pc->set_anchors_preset(LayoutPreset::PRESET_TOP_WIDE);
Label *title = memnew(Label(TTRC("ObjectDB Snapshot Summary")));
pc->add_child(title);
title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
explainer_text = memnew(CenterContainer);
explainer_text->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
explainer_text->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
content->add_child(explainer_text);
VBoxContainer *explainer_lines = memnew(VBoxContainer);
explainer_text->add_child(explainer_lines);
Label *l1 = memnew(Label(TTRC("Press 'Take ObjectDB Snapshot' to snapshot the ObjectDB.")));
Label *l2 = memnew(Label(TTRC("Memory in Godot is either owned natively by the engine or owned by the ObjectDB.")));
Label *l3 = memnew(Label(TTRC("ObjectDB Snapshots capture only memory owned by the ObjectDB.")));
l1->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
l2->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
l3->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
explainer_lines->add_child(l1);
explainer_lines->add_child(l2);
explainer_lines->add_child(l3);
ScrollContainer *sc = memnew(ScrollContainer);
sc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
sc->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
sc->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
content->add_child(sc);
blurb_list = memnew(VBoxContainer);
sc->add_child(blurb_list);
blurb_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
blurb_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
}
void SnapshotSummaryView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
explainer_text->set_visible(false);
String snapshot_a_name = diff_data == nullptr ? TTRC("Snapshot") : TTRC("Snapshot A");
String snapshot_b_name = TTRC("Snapshot B");
_push_overview_blurb(snapshot_a_name + " " + TTRC("Overview"), snapshot_data);
if (diff_data) {
_push_overview_blurb(snapshot_b_name + " " + TTRC("Overview"), diff_data);
}
_push_node_blurb(snapshot_a_name + " " + TTRC("Nodes"), snapshot_data);
if (diff_data) {
_push_node_blurb(snapshot_b_name + " " + TTRC("Nodes"), diff_data);
}
_push_refcounted_blurb(snapshot_a_name + " " + TTRC("RefCounteds"), snapshot_data);
if (diff_data) {
_push_refcounted_blurb(snapshot_b_name + " " + TTRC("RefCounteds"), diff_data);
}
_push_object_blurb(snapshot_a_name + " " + TTRC("Objects"), snapshot_data);
if (diff_data) {
_push_object_blurb(snapshot_b_name + " " + TTRC("Objects"), diff_data);
}
}
void SnapshotSummaryView::clear_snapshot() {
// Just clear out the blurbs and leave the explainer.
for (int i = 0; i < blurb_list->get_child_count(); i++) {
blurb_list->get_child(i)->queue_free();
}
snapshot_data = nullptr;
diff_data = nullptr;
explainer_text->set_visible(true);
}
SummaryBlurb::SummaryBlurb(const String &p_title, const String &p_rtl_content) {
add_theme_constant_override("margin_left", 2);
add_theme_constant_override("margin_right", 2);
add_theme_constant_override("margin_top", 2);
add_theme_constant_override("margin_bottom", 2);
label = memnew(RichTextLabel);
label->add_theme_constant_override(SceneStringName(line_separation), 6);
label->set_text_direction(Control::TEXT_DIRECTION_INHERITED);
label->set_fit_content(true);
label->set_use_bbcode(true);
label->add_newline();
label->push_bold();
label->add_text(p_title);
label->pop();
label->add_newline();
label->add_newline();
label->append_text(p_rtl_content);
add_child(label);
}
void SnapshotSummaryView::_push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
String c = "";
c += "[ul]\n";
c += vformat(" [i]%s[/i] %s\n", TTR("Name:"), p_snapshot->name);
if (p_snapshot->snapshot_context.has("timestamp")) {
c += vformat(" [i]%s[/i] %s\n", TTR("Timestamp:"), Time::get_singleton()->get_datetime_string_from_unix_time((double)p_snapshot->snapshot_context["timestamp"]));
}
if (p_snapshot->snapshot_context.has("game_version")) {
c += vformat(" [i]%s[/i] %s\n", TTR("Game Version:"), (String)p_snapshot->snapshot_context["game_version"]);
}
if (p_snapshot->snapshot_context.has("editor_version")) {
c += vformat(" [i]%s[/i] %s\n", TTR("Editor Version:"), (String)p_snapshot->snapshot_context["editor_version"]);
}
double bytes_to_mb = 0.000001;
if (p_snapshot->snapshot_context.has("mem_usage")) {
c += vformat(" [i]%s[/i] %s\n", TTR("Memory Used:"), String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_usage"]) * bytes_to_mb, 3) + " MB");
}
if (p_snapshot->snapshot_context.has("mem_max_usage")) {
c += vformat(" [i]%s[/i] %s\n", TTR("Max Memory Used:"), String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_max_usage"]) * bytes_to_mb, 3) + " MB");
}
c += vformat(" [i]%s[/i] %s\n", TTR("Total Objects:"), itos(p_snapshot->objects.size()));
int node_count = 0;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->is_node()) {
node_count++;
}
}
c += vformat(" [i]%s[/i] %s\n", TTR("Total Nodes:"), itos(node_count));
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
LocalVector<String> nodes;
nodes.reserve(p_snapshot->objects.size());
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
// if it's a node AND it doesn't have a parent node
if (pair.value->is_node() && !pair.value->extra_debug_data.has("node_parent") && pair.value->extra_debug_data.has("node_is_scene_root") && !pair.value->extra_debug_data["node_is_scene_root"]) {
String node_name = pair.value->extra_debug_data["node_name"];
nodes.push_back(node_name != "" ? node_name : pair.value->get_name());
}
}
if (nodes.size() <= 1) {
return;
}
String c = TTRC("Multiple root nodes [i](possible call to 'remove_child' without 'queue_free')[/i]\n");
c += "[ul]\n";
for (const String &node : nodes) {
c += " " + node + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
LocalVector<String> rcs;
rcs.reserve(p_snapshot->objects.size());
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->is_refcounted()) {
int ref_count = (uint64_t)pair.value->extra_debug_data["ref_count"];
Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
if (ref_count == ref_cycles.size()) {
rcs.push_back(pair.value->get_name());
}
}
}
if (rcs.is_empty()) {
return;
}
String c = TTRC("RefCounted objects only referenced in cycles [i](cycles often indicate a memory leaks)[/i]\n");
c += "[ul]\n";
for (const String &rc : rcs) {
c += " " + rc + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
LocalVector<String> objects;
objects.reserve(p_snapshot->objects.size());
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->inbound_references.is_empty() && pair.value->outbound_references.is_empty()) {
if (!pair.value->get_script().is_null()) {
// This blurb will have a lot of false positives, but we can at least suppress false positives
// from unreferenced nodes that are part of the scene tree.
if (pair.value->is_node() && (bool)pair.value->extra_debug_data["node_is_scene_root"]) {
objects.push_back(pair.value->get_name());
}
}
}
}
if (objects.is_empty()) {
return;
}
String c = TTRC("Scripted objects not referenced by any other objects [i](unreferenced objects may indicate a memory leak)[/i]\n");
c += "[ul]\n";
for (const String &object : objects) {
c += " " + object + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}

View file

@ -0,0 +1,67 @@
/**************************************************************************/
/* summary_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "snapshot_view.h"
#include "scene/gui/margin_container.h"
class CenterContainer;
class RichTextLabel;
class SummaryBlurb : public MarginContainer {
GDCLASS(SummaryBlurb, MarginContainer);
public:
RichTextLabel *label = nullptr;
SummaryBlurb(const String &p_title, const String &p_rtl_content);
};
class SnapshotSummaryView : public SnapshotView {
GDCLASS(SnapshotSummaryView, SnapshotView);
protected:
VBoxContainer *blurb_list = nullptr;
CenterContainer *explainer_text = nullptr;
void _push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
public:
SnapshotSummaryView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
virtual void clear_snapshot() override;
};

View file

@ -0,0 +1,464 @@
/**************************************************************************/
/* objectdb_profiler_panel.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "objectdb_profiler_panel.h"
#include "../snapshot_collector.h"
#include "data_viewers/class_view.h"
#include "data_viewers/node_view.h"
#include "data_viewers/object_view.h"
#include "data_viewers/refcounted_view.h"
#include "data_viewers/summary_view.h"
#include "core/config/project_settings.h"
#include "core/os/time.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
#include "scene/gui/option_button.h"
#include "scene/gui/split_container.h"
#include "scene/gui/tab_container.h"
// ObjectDB snapshots are very large. In remote_debugger_peer.cpp, the max in_buf and out_buf size is 8mb.
// Snapshots are typically larger than that, so we send them 6mb at a time. Leaving 2mb for other data.
const int SNAPSHOT_CHUNK_SIZE = 6 << 20;
void ObjectDBProfilerPanel::_request_object_snapshot() {
take_snapshot->set_disabled(true);
take_snapshot->set_text(TTRC("Generating Snapshot"));
// Pause the game while the snapshot is taken so the state of the game isn't modified as we capture the snapshot.
if (EditorDebuggerNode::get_singleton()->get_current_debugger()->is_breaked()) {
requested_break_for_snapshot = false;
_begin_object_snapshot();
} else {
awaiting_debug_break = true;
requested_break_for_snapshot = true; // We only need to resume the game if we are the ones who paused it.
EditorDebuggerNode::get_singleton()->debug_break();
}
}
void ObjectDBProfilerPanel::_on_debug_breaked(bool p_reallydid, bool p_can_debug, const String &p_reason, bool p_has_stackdump) {
if (p_reallydid && awaiting_debug_break) {
awaiting_debug_break = false;
_begin_object_snapshot();
}
}
void ObjectDBProfilerPanel::_begin_object_snapshot() {
Array args = { next_request_id++, SnapshotCollector::get_godot_version_string() };
EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_prepare_snapshot", args);
}
bool ObjectDBProfilerPanel::handle_debug_message(const String &p_message, const Array &p_data, int p_index) {
if (p_message == "snapshot:snapshot_prepared") {
int request_id = p_data[0];
int total_size = p_data[1];
partial_snapshots[request_id] = PartialSnapshot();
partial_snapshots[request_id].total_size = total_size;
Array args = { request_id, 0, SNAPSHOT_CHUNK_SIZE };
take_snapshot->set_text(vformat(TTRC("Receiving Snapshot (0/%s MiB)"), _to_mb(total_size)));
EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);
return true;
}
if (p_message == "snapshot:snapshot_chunk") {
int request_id = p_data[0];
PartialSnapshot &chunk = partial_snapshots[request_id];
chunk.data.append_array(p_data[1]);
take_snapshot->set_text(vformat(TTRC("Receiving Snapshot (%s/%s MiB)"), _to_mb(chunk.data.size()), _to_mb(chunk.total_size)));
if (chunk.data.size() != chunk.total_size) {
Array args = { request_id, chunk.data.size(), chunk.data.size() + SNAPSHOT_CHUNK_SIZE };
EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);
return true;
}
take_snapshot->set_text(TTRC("Visualizing Snapshot"));
// Wait a frame just so the button has a chance to update its text so the user knows what's going on.
get_tree()->connect("process_frame", callable_mp(this, &ObjectDBProfilerPanel::receive_snapshot).bind(request_id), CONNECT_ONE_SHOT);
return true;
}
return false;
}
void ObjectDBProfilerPanel::receive_snapshot(int request_id) {
const Vector<uint8_t> &in_data = partial_snapshots[request_id].data;
String snapshot_file_name = Time::get_singleton()->get_datetime_string_from_system(false).replace_char('T', '_').replace_char(':', '-');
Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
if (snapshot_dir.is_valid()) {
Error err;
String current_dir = snapshot_dir->get_current_dir();
String joined_dir = current_dir.path_join(snapshot_file_name) + ".odb_snapshot";
Ref<FileAccess> file = FileAccess::open(joined_dir, FileAccess::WRITE, &err);
if (err == OK) {
file->store_buffer(in_data);
file->close(); // RAII could do this typically, but we want to read the file in _show_selected_snapshot, so we have to finalize the write before that.
_add_snapshot_button(snapshot_file_name, joined_dir);
snapshot_list->deselect_all();
snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());
snapshot_list->ensure_cursor_is_visible();
_show_selected_snapshot();
} else {
ERR_PRINT("Could not persist ObjectDB Snapshot: " + String(error_names[err]));
}
}
partial_snapshots.erase(request_id);
if (requested_break_for_snapshot) {
EditorDebuggerNode::get_singleton()->debug_continue();
}
take_snapshot->set_disabled(false);
take_snapshot->set_text("Take ObjectDB Snapshot");
}
Ref<DirAccess> ObjectDBProfilerPanel::_get_and_create_snapshot_storage_dir() {
String profiles_dir = "user://";
Ref<DirAccess> da = DirAccess::open(profiles_dir);
ERR_FAIL_COND_V_MSG(da.is_null(), nullptr, vformat("Could not open 'user://' directory: '%s'.", profiles_dir));
Error err = da->change_dir("objectdb_snapshots");
if (err != OK) {
Error err_mk = da->make_dir("objectdb_snapshots");
Error err_ch = da->change_dir("objectdb_snapshots");
ERR_FAIL_COND_V_MSG(err_mk != OK || err_ch != OK, nullptr, "Could not create ObjectDB Snapshots directory: " + da->get_current_dir());
}
return da;
}
TreeItem *ObjectDBProfilerPanel::_add_snapshot_button(const String &p_snapshot_file_name, const String &p_full_file_path) {
TreeItem *item = snapshot_list->create_item(snapshot_list->get_root());
item->set_text(0, p_snapshot_file_name);
item->set_metadata(0, p_full_file_path);
item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
item->move_before(snapshot_list->get_root()->get_first_child());
_update_diff_items();
_update_enabled_diff_items();
return item;
}
void ObjectDBProfilerPanel::_show_selected_snapshot() {
if (snapshot_list->get_selected()->get_text(0) == (String)diff_button->get_selected_metadata()) {
for (int i = 0; i < diff_button->get_item_count(); i++) {
if (diff_button->get_item_text(i) == current_snapshot->get_snapshot()->name) {
diff_button->select(i);
break;
}
}
}
show_snapshot(snapshot_list->get_selected()->get_text(0), diff_button->get_selected_metadata());
_update_enabled_diff_items();
}
void ObjectDBProfilerPanel::_on_snapshot_deselected() {
snapshot_list->deselect_all();
diff_button->select(0);
clear_snapshot();
_update_enabled_diff_items();
}
Ref<GameStateSnapshotRef> ObjectDBProfilerPanel::get_snapshot(const String &p_snapshot_file_name) {
if (snapshot_cache.has(p_snapshot_file_name)) {
return snapshot_cache.get(p_snapshot_file_name);
}
Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
ERR_FAIL_COND_V_MSG(snapshot_dir.is_null(), nullptr, "Could not access ObjectDB Snapshot directory");
String full_file_path = snapshot_dir->get_current_dir().path_join(p_snapshot_file_name) + ".odb_snapshot";
Error err;
Ref<FileAccess> snapshot_file = FileAccess::open(full_file_path, FileAccess::READ, &err);
ERR_FAIL_COND_V_MSG(err != OK, nullptr, "Could not open ObjectDB Snapshot file: " + full_file_path);
Vector<uint8_t> content = snapshot_file->get_buffer(snapshot_file->get_length()); // We want to split on newlines, so normalize them.
ERR_FAIL_COND_V_MSG(content.is_empty(), nullptr, "ObjectDB Snapshot file is empty: " + full_file_path);
Ref<GameStateSnapshotRef> snapshot = GameStateSnapshot::create_ref(p_snapshot_file_name, content);
if (snapshot.is_valid()) {
snapshot_cache.insert(p_snapshot_file_name, snapshot);
}
return snapshot;
}
void ObjectDBProfilerPanel::show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name) {
clear_snapshot(false);
current_snapshot = get_snapshot(p_snapshot_file_name);
if (!p_snapshot_diff_file_name.is_empty()) {
diff_snapshot = get_snapshot(p_snapshot_diff_file_name);
}
_update_view_tabs();
_view_tab_changed(view_tabs->get_current_tab());
}
void ObjectDBProfilerPanel::_view_tab_changed(int p_tab_idx) {
// Populating tabs only on tab changed because we're handling a lot of data,
// and the editor freezes for a while if we try to populate every tab at once.
SnapshotView *view = cast_to<SnapshotView>(view_tabs->get_current_tab_control());
GameStateSnapshot *snapshot = current_snapshot.is_null() ? nullptr : current_snapshot->get_snapshot();
GameStateSnapshot *diff = diff_snapshot.is_null() ? nullptr : diff_snapshot->get_snapshot();
if (snapshot != nullptr && !view->is_showing_snapshot(snapshot, diff)) {
view->show_snapshot(snapshot, diff);
}
}
void ObjectDBProfilerPanel::clear_snapshot(bool p_update_view_tabs) {
for (SnapshotView *view : views) {
view->clear_snapshot();
}
current_snapshot.unref();
diff_snapshot.unref();
if (p_update_view_tabs) {
_update_view_tabs();
}
}
void ObjectDBProfilerPanel::set_enabled(bool p_enabled) {
take_snapshot->set_text(TTRC("Take ObjectDB Snapshot"));
take_snapshot->set_disabled(!p_enabled);
}
void ObjectDBProfilerPanel::_snapshot_rmb(const Vector2 &p_pos, MouseButton p_button) {
if (p_button != MouseButton::RIGHT) {
return;
}
rmb_menu->clear(false);
rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Rename")), TTRC("Rename"), OdbProfilerMenuOptions::ODB_MENU_RENAME);
rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("Show in File Manager"), OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER);
rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTRC("Delete"), OdbProfilerMenuOptions::ODB_MENU_DELETE);
rmb_menu->set_position(snapshot_list->get_screen_position() + p_pos);
rmb_menu->reset_size();
rmb_menu->popup();
}
void ObjectDBProfilerPanel::_rmb_menu_pressed(int p_tool, bool p_confirm_override) {
String file_path = snapshot_list->get_selected()->get_metadata(0);
String global_path = ProjectSettings::get_singleton()->globalize_path(file_path);
switch (rmb_menu->get_item_id(p_tool)) {
case OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER: {
OS::get_singleton()->shell_show_in_file_manager(global_path, true);
break;
}
case OdbProfilerMenuOptions::ODB_MENU_DELETE: {
DirAccess::remove_file_or_error(global_path);
snapshot_list->get_root()->remove_child(snapshot_list->get_selected());
if (snapshot_list->get_root()->get_child_count() > 0) {
snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());
} else {
// If we deleted the last snapshot, jump back to the summary tab and clear everything out.
clear_snapshot();
}
_update_diff_items();
break;
}
case OdbProfilerMenuOptions::ODB_MENU_RENAME: {
snapshot_list->edit_selected(true);
break;
}
}
}
void ObjectDBProfilerPanel::_edit_snapshot_name() {
String new_snapshot_name = snapshot_list->get_selected()->get_text(0);
String full_file_with_path = snapshot_list->get_selected()->get_metadata(0);
Vector<String> full_path_parts = full_file_with_path.rsplit("/", false, 1);
String full_file_path = full_path_parts[0];
String file_name = full_path_parts[1];
String old_snapshot_name = file_name.split(".")[0];
String new_full_file_path = full_file_path.path_join(new_snapshot_name) + ".odb_snapshot";
bool name_taken = false;
for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {
TreeItem *item = snapshot_list->get_root()->get_child(i);
if (item != snapshot_list->get_selected()) {
if (item->get_text(0) == new_snapshot_name) {
name_taken = true;
break;
}
}
}
if (name_taken || new_snapshot_name.contains_char(':') || new_snapshot_name.contains_char('\\') || new_snapshot_name.contains_char('/') || new_snapshot_name.begins_with(".") || new_snapshot_name.is_empty()) {
EditorNode::get_singleton()->show_warning(TTRC("Invalid snapshot name."));
snapshot_list->get_selected()->set_text(0, old_snapshot_name);
return;
}
Error err = DirAccess::rename_absolute(full_file_with_path, new_full_file_path);
if (err != OK) {
EditorNode::get_singleton()->show_warning(TTRC("Snapshot rename failed"));
snapshot_list->get_selected()->set_text(0, old_snapshot_name);
} else {
snapshot_list->get_selected()->set_metadata(0, new_full_file_path);
}
_update_diff_items();
_show_selected_snapshot();
}
ObjectDBProfilerPanel::ObjectDBProfilerPanel() {
set_name(TTRC("ObjectDB Profiler"));
snapshot_cache = LRUCache<String, Ref<GameStateSnapshotRef>>(SNAPSHOT_CACHE_MAX_SIZE);
EditorDebuggerNode::get_singleton()->get_current_debugger()->connect("breaked", callable_mp(this, &ObjectDBProfilerPanel::_on_debug_breaked));
HSplitContainer *root_container = memnew(HSplitContainer);
root_container->set_anchors_preset(Control::LayoutPreset::PRESET_FULL_RECT);
root_container->set_v_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);
root_container->set_h_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);
root_container->set_split_offset(300 * EDSCALE);
add_child(root_container);
VBoxContainer *snapshot_column = memnew(VBoxContainer);
root_container->add_child(snapshot_column);
take_snapshot = memnew(Button(TTRC("Take ObjectDB Snapshot")));
snapshot_column->add_child(take_snapshot);
take_snapshot->connect(SceneStringName(pressed), callable_mp(this, &ObjectDBProfilerPanel::_request_object_snapshot));
snapshot_list = memnew(Tree);
snapshot_list->create_item();
snapshot_list->set_hide_folding(true);
snapshot_column->add_child(snapshot_list);
snapshot_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
snapshot_list->set_hide_root(true);
snapshot_list->set_columns(1);
snapshot_list->set_column_titles_visible(true);
snapshot_list->set_column_title(0, "Snapshots");
snapshot_list->set_column_expand(0, true);
snapshot_list->set_column_clip_content(0, true);
snapshot_list->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot));
snapshot_list->connect("nothing_selected", callable_mp(this, &ObjectDBProfilerPanel::_on_snapshot_deselected));
snapshot_list->connect("item_edited", callable_mp(this, &ObjectDBProfilerPanel::_edit_snapshot_name));
snapshot_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
snapshot_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
snapshot_list->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
snapshot_list->set_allow_rmb_select(true);
snapshot_list->connect("item_mouse_selected", callable_mp(this, &ObjectDBProfilerPanel::_snapshot_rmb));
rmb_menu = memnew(PopupMenu);
add_child(rmb_menu);
rmb_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ObjectDBProfilerPanel::_rmb_menu_pressed).bind(false));
HBoxContainer *diff_button_and_label = memnew(HBoxContainer);
diff_button_and_label->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
snapshot_column->add_child(diff_button_and_label);
Label *diff_against = memnew(Label(TTRC("Diff Against:")));
diff_button_and_label->add_child(diff_against);
diff_button = memnew(OptionButton);
diff_button->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
diff_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
diff_button->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot).unbind(1));
diff_button_and_label->add_child(diff_button);
// Tabs of various views right for each snapshot.
view_tabs = memnew(TabContainer);
root_container->add_child(view_tabs);
view_tabs->set_custom_minimum_size(Size2(300 * EDSCALE, 0));
view_tabs->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
view_tabs->connect("tab_changed", callable_mp(this, &ObjectDBProfilerPanel::_view_tab_changed));
add_view(memnew(SnapshotSummaryView));
add_view(memnew(SnapshotClassView));
add_view(memnew(SnapshotObjectView));
add_view(memnew(SnapshotNodeView));
add_view(memnew(SnapshotRefCountedView));
set_enabled(false);
// Load all the snapshot names from disk.
Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
if (snapshot_dir.is_valid()) {
for (const String &file_name : snapshot_dir->get_files()) {
Vector<String> name_parts = file_name.split(".");
ERR_CONTINUE_MSG(name_parts.size() != 2 || name_parts[1] != "odb_snapshot", "ObjectDB snapshot file did not have .odb_snapshot extension. Skipping: " + file_name);
_add_snapshot_button(name_parts[0], snapshot_dir->get_current_dir().path_join(file_name));
}
}
}
void ObjectDBProfilerPanel::add_view(SnapshotView *p_to_add) {
views.push_back(p_to_add);
view_tabs->add_child(p_to_add);
_update_view_tabs();
}
void ObjectDBProfilerPanel::_update_view_tabs() {
bool has_snapshot = current_snapshot.is_valid();
for (int i = 1; i < view_tabs->get_tab_count(); i++) {
view_tabs->set_tab_disabled(i, !has_snapshot);
}
if (!has_snapshot) {
view_tabs->set_current_tab(0);
}
}
void ObjectDBProfilerPanel::_update_diff_items() {
diff_button->clear();
diff_button->add_item(TTRC("None"), 0);
diff_button->set_item_metadata(0, String());
diff_button->set_item_auto_translate_mode(0, Node::AUTO_TRANSLATE_MODE_ALWAYS);
for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {
String name = snapshot_list->get_root()->get_child(i)->get_text(0);
diff_button->add_item(name);
diff_button->set_item_metadata(i + 1, name);
}
}
void ObjectDBProfilerPanel::_update_enabled_diff_items() {
TreeItem *selected_snapshot = snapshot_list->get_selected();
if (selected_snapshot == nullptr) {
diff_button->set_disabled(true);
return;
}
diff_button->set_disabled(false);
String snapshot_name = selected_snapshot->get_text(0);
for (int i = 0; i < diff_button->get_item_count(); i++) {
diff_button->set_item_disabled(i, diff_button->get_item_text(i) == snapshot_name);
}
}
String ObjectDBProfilerPanel::_to_mb(int p_x) {
return String::num((double)p_x / (double)(1 << 20), 2);
}

View file

@ -0,0 +1,103 @@
/**************************************************************************/
/* objectdb_profiler_panel.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "data_viewers/snapshot_view.h"
#include "snapshot_data.h"
#include "core/io/dir_access.h"
#include "core/templates/lru.h"
class TabContainer;
class Tree;
// UI loaded by the debugger.
class ObjectDBProfilerPanel : public Control {
GDCLASS(ObjectDBProfilerPanel, Control);
protected:
static constexpr int SNAPSHOT_CACHE_MAX_SIZE = 10;
enum OdbProfilerMenuOptions {
ODB_MENU_RENAME,
ODB_MENU_SHOW_IN_FOLDER,
ODB_MENU_DELETE,
};
struct PartialSnapshot {
int total_size;
Vector<uint8_t> data;
};
int next_request_id = 0;
bool awaiting_debug_break = false;
bool requested_break_for_snapshot = false;
Tree *snapshot_list = nullptr;
Button *take_snapshot = nullptr;
TabContainer *view_tabs = nullptr;
PopupMenu *rmb_menu = nullptr;
OptionButton *diff_button = nullptr;
HashMap<int, PartialSnapshot> partial_snapshots;
LocalVector<SnapshotView *> views;
Ref<GameStateSnapshotRef> current_snapshot;
Ref<GameStateSnapshotRef> diff_snapshot;
LRUCache<String, Ref<GameStateSnapshotRef>> snapshot_cache;
void _request_object_snapshot();
void _begin_object_snapshot();
void _on_debug_breaked(bool p_reallydid, bool p_can_debug, const String &p_reason, bool p_has_stackdump);
void _show_selected_snapshot();
void _on_snapshot_deselected();
Ref<DirAccess> _get_and_create_snapshot_storage_dir();
TreeItem *_add_snapshot_button(const String &p_snapshot_file_name, const String &p_full_file_path);
void _snapshot_rmb(const Vector2 &p_pos, MouseButton p_button);
void _rmb_menu_pressed(int p_tool, bool p_confirm_override);
void _update_view_tabs();
void _update_diff_items();
void _update_enabled_diff_items();
void _edit_snapshot_name();
void _view_tab_changed(int p_tab_idx);
String _to_mb(int p_x);
public:
ObjectDBProfilerPanel();
void receive_snapshot(int p_request_id);
void show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name);
void clear_snapshot(bool p_update_view_tabs = true);
Ref<GameStateSnapshotRef> get_snapshot(const String &p_snapshot_file_name);
void set_enabled(bool p_enabled);
void add_view(SnapshotView *p_to_add);
bool handle_debug_message(const String &p_message, const Array &p_data, int p_index);
};

View file

@ -0,0 +1,66 @@
/**************************************************************************/
/* objectdb_profiler_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "objectdb_profiler_plugin.h"
#include "objectdb_profiler_panel.h"
bool ObjectDBProfilerDebuggerPlugin::has_capture(const String &p_capture) const {
return p_capture == "snapshot";
}
bool ObjectDBProfilerDebuggerPlugin::capture(const String &p_message, const Array &p_data, int p_index) {
ERR_FAIL_NULL_V(debugger_panel, false);
return debugger_panel->handle_debug_message(p_message, p_data, p_index);
}
void ObjectDBProfilerDebuggerPlugin::setup_session(int p_session_id) {
Ref<EditorDebuggerSession> session = get_session(p_session_id);
ERR_FAIL_COND(session.is_null());
debugger_panel = memnew(ObjectDBProfilerPanel);
session->connect("started", callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(true));
session->connect("stopped", callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(false));
session->add_session_tab(debugger_panel);
}
ObjectDBProfilerPlugin::ObjectDBProfilerPlugin() {
debugger.instantiate();
}
void ObjectDBProfilerPlugin::_notification(int p_what) {
switch (p_what) {
case Node::NOTIFICATION_ENTER_TREE: {
add_debugger_plugin(debugger);
} break;
case Node::NOTIFICATION_EXIT_TREE: {
remove_debugger_plugin(debugger);
}
}
}

View file

@ -0,0 +1,65 @@
/**************************************************************************/
/* objectdb_profiler_plugin.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "editor/debugger/editor_debugger_plugin.h"
#include "editor/plugins/editor_plugin.h"
class ObjectDBProfilerPanel;
class ObjectDBProfilerDebuggerPlugin;
// First, ObjectDBProfilerPlugin is loaded. Then it loads ObjectDBProfilerDebuggerPlugin.
class ObjectDBProfilerPlugin : public EditorPlugin {
GDCLASS(ObjectDBProfilerPlugin, EditorPlugin);
protected:
Ref<ObjectDBProfilerDebuggerPlugin> debugger;
void _notification(int p_what);
public:
ObjectDBProfilerPlugin();
};
class ObjectDBProfilerDebuggerPlugin : public EditorDebuggerPlugin {
GDCLASS(ObjectDBProfilerDebuggerPlugin, EditorDebuggerPlugin);
protected:
ObjectDBProfilerPanel *debugger_panel = nullptr;
void _request_object_snapshot(int p_request_id);
public:
ObjectDBProfilerDebuggerPlugin() {}
virtual bool has_capture(const String &p_capture) const override;
virtual bool capture(const String &p_message, const Array &p_data, int p_index) override;
virtual void setup_session(int p_session_id) override;
};

View file

@ -0,0 +1,374 @@
/**************************************************************************/
/* snapshot_data.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "snapshot_data.h"
#include "core/core_bind.h"
#include "core/object/script_language.h"
#include "scene/debugger/scene_debugger.h"
#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED)
#include "modules/gdscript/gdscript.h"
#endif
SnapshotDataObject::SnapshotDataObject(SceneDebuggerObject &p_obj, GameStateSnapshot *p_snapshot, ResourceCache &resource_cache) :
snapshot(p_snapshot) {
remote_object_id = p_obj.id;
type_name = p_obj.class_name;
for (const SceneDebuggerObject::SceneDebuggerProperty &prop : p_obj.properties) {
PropertyInfo pinfo = prop.first;
Variant pvalue = prop.second;
if (pinfo.type == Variant::OBJECT && pvalue.is_string()) {
String path = pvalue;
// If a resource is followed by a ::, it is a nested resource (like a sub_resource in a .tscn file).
// To get a reference to it, first we load the parent resource (the .tscn, for example), then,
// we load the child resource. The parent resource (dependency) should not be destroyed before the child
// resource (pvalue) is loaded.
if (path.is_resource_file()) {
// Built-in resource.
String base_path = path.get_slice("::", 0);
if (!resource_cache.cache.has(base_path)) {
resource_cache.cache[base_path] = ResourceLoader::load(base_path);
resource_cache.misses++;
} else {
resource_cache.hits++;
}
}
if (!resource_cache.cache.has(path)) {
resource_cache.cache[path] = ResourceLoader::load(path);
resource_cache.misses++;
} else {
resource_cache.hits++;
}
pvalue = resource_cache.cache[path];
if (pinfo.hint_string == "Script") {
if (get_script() != pvalue) {
set_script(Ref<RefCounted>());
Ref<Script> scr(pvalue);
if (scr.is_valid()) {
ScriptInstance *scr_instance = scr->placeholder_instance_create(this);
if (scr_instance) {
set_script_and_instance(pvalue, scr_instance);
}
}
}
}
}
prop_list.push_back(pinfo);
prop_values[pinfo.name] = pvalue;
}
}
bool SnapshotDataObject::_get(const StringName &p_name, Variant &r_ret) const {
String name = p_name;
if (name.begins_with("Metadata/")) {
name = name.replace_first("Metadata/", "metadata/");
}
if (!prop_values.has(name)) {
return false;
}
r_ret = prop_values[p_name];
return true;
}
void SnapshotDataObject::_get_property_list(List<PropertyInfo> *p_list) const {
p_list->clear(); // Sorry, don't want any categories.
for (const PropertyInfo &prop : prop_list) {
if (prop.name == "script") {
// Skip the script property, it's always added by the non-virtual method.
continue;
}
p_list->push_back(prop);
}
}
void SnapshotDataObject::_bind_methods() {
ClassDB::bind_method(D_METHOD("_is_read_only"), &SnapshotDataObject::_is_read_only);
}
String SnapshotDataObject::get_node_path() {
if (!is_node()) {
return "";
}
SnapshotDataObject *current = this;
String path;
while (true) {
String current_node_name = current->extra_debug_data["node_name"];
if (current_node_name != "") {
if (path != "") {
path = current_node_name + "/" + path;
} else {
path = current_node_name;
}
}
if (!current->extra_debug_data.has("node_parent")) {
break;
}
current = snapshot->objects[current->extra_debug_data["node_parent"]];
}
return path;
}
String SnapshotDataObject::_get_script_name(Ref<Script> p_script) {
#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED)
// GDScripts have more specific names than base scripts, so use those names if possible.
return GDScript::debug_get_script_name(p_script);
#else
// Otherwise fallback to the base script's name.
return p_script->get_global_name();
#endif
}
String SnapshotDataObject::get_name() {
String found_type_name = type_name;
// Ideally, we will name it after the script attached to it.
Ref<Script> maybe_script = get_script();
if (maybe_script.is_valid()) {
String full_name;
while (maybe_script.is_valid()) {
String global_name = _get_script_name(maybe_script);
if (global_name != "") {
if (full_name != "") {
full_name = global_name + "/" + full_name;
} else {
full_name = global_name;
}
}
maybe_script = maybe_script->get_base_script().ptr();
}
found_type_name = type_name + "/" + full_name;
}
return found_type_name + "_" + uitos(remote_object_id);
}
bool SnapshotDataObject::is_refcounted() {
return is_class(RefCounted::get_class_static());
}
bool SnapshotDataObject::is_node() {
return is_class(Node::get_class_static());
}
bool SnapshotDataObject::is_class(const String &p_base_class) {
return ClassDB::is_parent_class(type_name, p_base_class);
}
HashSet<ObjectID> SnapshotDataObject::_unique_references(const HashMap<String, ObjectID> &p_refs) {
HashSet<ObjectID> obj_set;
for (const KeyValue<String, ObjectID> &pair : p_refs) {
obj_set.insert(pair.value);
}
return obj_set;
}
HashSet<ObjectID> SnapshotDataObject::get_unique_outbound_refernces() {
return _unique_references(outbound_references);
}
HashSet<ObjectID> SnapshotDataObject::get_unique_inbound_references() {
return _unique_references(inbound_references);
}
void GameStateSnapshot::_get_outbound_references(Variant &p_var, HashMap<String, ObjectID> &r_ret_val, const String &p_current_path) {
String path_divider = p_current_path.size() > 0 ? "/" : ""; // Make sure we don't start with a /.
switch (p_var.get_type()) {
case Variant::Type::INT:
case Variant::Type::OBJECT: { // Means ObjectID.
ObjectID as_id = ObjectID((uint64_t)p_var);
if (!objects.has(as_id)) {
return;
}
r_ret_val[p_current_path] = as_id;
break;
}
case Variant::Type::DICTIONARY: {
Dictionary dict = (Dictionary)p_var;
LocalVector<Variant> keys = dict.get_key_list();
for (Variant &k : keys) {
// The dictionary key _could be_ an object. If it is, we name the key property with the same name as the value, but with _key appended to it.
_get_outbound_references(k, r_ret_val, p_current_path + path_divider + (String)k + "_key");
Variant v = dict.get(k, Variant());
_get_outbound_references(v, r_ret_val, p_current_path + path_divider + (String)k);
}
break;
}
case Variant::Type::ARRAY: {
Array arr = (Array)p_var;
int i = 0;
for (Variant &v : arr) {
_get_outbound_references(v, r_ret_val, p_current_path + path_divider + itos(i));
i++;
}
break;
}
default: {
break;
}
}
}
void GameStateSnapshot::_get_rc_cycles(
SnapshotDataObject *p_obj,
SnapshotDataObject *p_source_obj,
HashSet<SnapshotDataObject *> p_traversed_objs,
LocalVector<String> &r_ret_val,
const String &p_current_path) {
// We're at the end of this branch and it was a cycle.
if (p_obj == p_source_obj && p_current_path != "") {
r_ret_val.push_back(p_current_path);
return;
}
// Go through each of our children and try traversing them.
for (const KeyValue<String, ObjectID> &next_child : p_obj->outbound_references) {
SnapshotDataObject *next_obj = p_obj->snapshot->objects[next_child.value];
String next_name = next_obj == p_source_obj ? "self" : next_obj->get_name();
String current_name = p_obj == p_source_obj ? "self" : p_obj->get_name();
String child_path = current_name + "[\"" + next_child.key + "\"] -> " + next_name;
if (p_current_path != "") {
child_path = p_current_path + "\n" + child_path;
}
SnapshotDataObject *next = objects[next_child.value];
if (next != nullptr && next->is_class(RefCounted::get_class_static()) && !next->is_class(WeakRef::get_class_static()) && !p_traversed_objs.has(next)) {
HashSet<SnapshotDataObject *> traversed_copy = p_traversed_objs;
if (p_obj != p_source_obj) {
traversed_copy.insert(p_obj);
}
_get_rc_cycles(next, p_source_obj, traversed_copy, r_ret_val, child_path);
}
}
}
void GameStateSnapshot::recompute_references() {
for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : objects) {
Dictionary values;
for (const KeyValue<StringName, Variant> &kv : obj.value->prop_values) {
// Should only ever be one entry in this context.
values[kv.key] = kv.value;
}
Variant values_variant(values);
HashMap<String, ObjectID> refs;
_get_outbound_references(values_variant, refs);
obj.value->outbound_references = refs;
for (const KeyValue<String, ObjectID> &kv : refs) {
// Get the guy we are pointing to, and indicate the name of _our_ property that is pointing to them.
if (objects.has(kv.value)) {
objects[kv.value]->inbound_references[kv.key] = obj.key;
}
}
}
for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : objects) {
if (!obj.value->is_class(RefCounted::get_class_static()) || obj.value->is_class(WeakRef::get_class_static())) {
continue;
}
HashSet<SnapshotDataObject *> traversed_objs;
LocalVector<String> cycles;
_get_rc_cycles(obj.value, obj.value, traversed_objs, cycles, "");
Array cycles_array;
for (const String &cycle : cycles) {
cycles_array.push_back(cycle);
}
obj.value->extra_debug_data["ref_cycles"] = cycles_array;
}
}
Ref<GameStateSnapshotRef> GameStateSnapshot::create_ref(const String &p_snapshot_name, const Vector<uint8_t> &p_snapshot_buffer) {
// A ref to a refcounted object which is a wrapper of a non-refcounted object.
Ref<GameStateSnapshotRef> sn;
sn.instantiate(memnew(GameStateSnapshot));
GameStateSnapshot *snapshot = sn->get_snapshot();
snapshot->name = p_snapshot_name;
// Snapshots may have been created by an older version of the editor. Handle parsing old snapshot versions here based on the version number.
Vector<uint8_t> snapshot_buffer_decompressed;
int success = Compression::decompress_dynamic(&snapshot_buffer_decompressed, -1, p_snapshot_buffer.ptr(), p_snapshot_buffer.size(), Compression::MODE_DEFLATE);
ERR_FAIL_COND_V_MSG(success != Z_OK, nullptr, "ObjectDB Snapshot could not be parsed. Failed to decompress snapshot.");
CoreBind::Marshalls *m = CoreBind::Marshalls::get_singleton();
Array snapshot_data = m->base64_to_variant(m->raw_to_base64(snapshot_buffer_decompressed));
ERR_FAIL_COND_V_MSG(snapshot_data.is_empty(), nullptr, "ObjectDB Snapshot could not be parsed. Variant array is empty.");
const Variant &first_item = snapshot_data[0];
ERR_FAIL_COND_V_MSG(first_item.get_type() != Variant::DICTIONARY, nullptr, "ObjectDB Snapshot could not be parsed. First item is not a Dictionary.");
snapshot->snapshot_context = first_item;
SnapshotDataObject::ResourceCache resource_cache;
for (int i = 1; i < snapshot_data.size(); i += 4) {
SceneDebuggerObject obj;
obj.deserialize(uint64_t(snapshot_data[i + 0]), snapshot_data[i + 1], snapshot_data[i + 2]);
ERR_FAIL_COND_V_MSG(snapshot_data[i + 3].get_type() != Variant::DICTIONARY, nullptr, "ObjectDB Snapshot could not be parsed. Extra debug data is not a Dictionary.");
if (obj.id.is_null()) {
continue;
}
snapshot->objects[obj.id] = memnew(SnapshotDataObject(obj, snapshot, resource_cache));
snapshot->objects[obj.id]->extra_debug_data = (Dictionary)snapshot_data[i + 3];
}
snapshot->recompute_references();
print_verbose("Resource cache hits: " + String::num(resource_cache.hits) + ". Resource cache misses: " + String::num(resource_cache.misses));
return sn;
}
GameStateSnapshot::~GameStateSnapshot() {
for (const KeyValue<ObjectID, SnapshotDataObject *> &item : objects) {
memdelete(item.value);
}
}
bool GameStateSnapshotRef::unreference() {
bool die = RefCounted::unreference();
if (die) {
memdelete(gamestate_snapshot);
}
return die;
}
GameStateSnapshot *GameStateSnapshotRef::get_snapshot() {
return gamestate_snapshot;
}

View file

@ -0,0 +1,111 @@
/**************************************************************************/
/* snapshot_data.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "editor/debugger/editor_debugger_inspector.h"
class GameStateSnapshot;
class GameStateSnapshotRef;
class SnapshotDataObject : public Object {
GDCLASS(SnapshotDataObject, Object);
HashSet<ObjectID> _unique_references(const HashMap<String, ObjectID> &p_refs);
String _get_script_name(Ref<Script> p_script);
public:
GameStateSnapshot *snapshot = nullptr;
Dictionary extra_debug_data;
HashMap<String, ObjectID> outbound_references;
HashMap<String, ObjectID> inbound_references;
HashSet<ObjectID> get_unique_outbound_refernces();
HashSet<ObjectID> get_unique_inbound_references();
uint64_t remote_object_id = 0;
String type_name;
LocalVector<PropertyInfo> prop_list;
HashMap<StringName, Variant> prop_values;
bool _get(const StringName &p_name, Variant &r_ret) const;
void _get_property_list(List<PropertyInfo> *p_list) const;
struct ResourceCache {
HashMap<String, Ref<Resource>> cache;
int misses = 0;
int hits = 0;
};
SnapshotDataObject(SceneDebuggerObject &p_obj, GameStateSnapshot *p_snapshot, ResourceCache &resource_cache);
String get_name();
String get_node_path();
bool is_refcounted();
bool is_node();
bool is_class(const String &p_base_class);
protected:
// Snapshots are inherently read-only. Can't edit the past.
bool _is_read_only() { return true; }
static void _bind_methods();
};
class GameStateSnapshot : public Object {
GDCLASS(GameStateSnapshot, Object);
void _get_outbound_references(Variant &p_var, HashMap<String, ObjectID> &r_ret_val, const String &p_current_path = "");
void _get_rc_cycles(SnapshotDataObject *p_obj, SnapshotDataObject *p_source_obj, HashSet<SnapshotDataObject *> p_traversed_objs, LocalVector<String> &r_ret_val, const String &p_current_path = "");
public:
String name;
HashMap<ObjectID, SnapshotDataObject *> objects;
Dictionary snapshot_context;
// Ideally, this would extend EditorDebuggerRemoteObject and be refcounted, but we can't have it both ways.
// So, instead we have this static 'constructor' that returns a RefCounted wrapper around a GameStateSnapshot.
static Ref<GameStateSnapshotRef> create_ref(const String &p_snapshot_name, const Vector<uint8_t> &p_snapshot_buffer);
~GameStateSnapshot();
void recompute_references();
};
// Thin RefCounted wrapper around a GameStateSnapshot.
class GameStateSnapshotRef : public RefCounted {
GDCLASS(GameStateSnapshotRef, RefCounted);
GameStateSnapshot *gamestate_snapshot = nullptr;
public:
GameStateSnapshotRef(GameStateSnapshot *p_gss) :
gamestate_snapshot(p_gss) {}
bool unreference();
GameStateSnapshot *get_snapshot();
};

View file

@ -0,0 +1,54 @@
/**************************************************************************/
/* register_types.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "register_types.h"
#include "snapshot_collector.h"
#ifdef TOOLS_ENABLED
#include "editor/objectdb_profiler_plugin.h"
#endif // TOOLS_ENABLED
void initialize_objectdb_profiler_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
SnapshotCollector::initialize();
}
#ifdef TOOLS_ENABLED
else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
EditorPlugins::add_by_type<ObjectDBProfilerPlugin>();
}
#endif // TOOLS_ENABLED
}
void uninitialize_objectdb_profiler_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
SnapshotCollector::deinitialize();
}
}

View file

@ -0,0 +1,36 @@
/**************************************************************************/
/* register_types.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "modules/register_module_types.h"
void initialize_objectdb_profiler_module(ModuleInitializationLevel p_level);
void uninitialize_objectdb_profiler_module(ModuleInitializationLevel p_level);

View file

@ -0,0 +1,175 @@
/**************************************************************************/
/* snapshot_collector.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "snapshot_collector.h"
#include "core/core_bind.h"
#include "core/debugger/engine_debugger.h"
#include "core/os/time.h"
#include "core/version.h"
#include "scene/main/node.h"
#include "scene/main/window.h"
void SnapshotCollector::initialize() {
pending_snapshots.clear();
EngineDebugger::register_message_capture("snapshot", EngineDebugger::Capture(nullptr, SnapshotCollector::parse_message));
}
void SnapshotCollector::deinitialize() {
EngineDebugger::unregister_message_capture("snapshot");
pending_snapshots.clear();
}
void SnapshotCollector::snapshot_objects(Array *p_arr, Dictionary &p_snapshot_context) {
print_verbose("Starting to snapshot");
p_arr->clear();
// Gather all ObjectIDs first. The ObjectDB will be locked in debug_objects, so we can't serialize until it exits.
// In rare cases, the object may be deleted as the snapshot is taken. So, we store the object's class name to give users a clue about what went wrong.
LocalVector<Pair<ObjectID, StringName>> debugger_object_ids;
debugger_object_ids.reserve(ObjectDB::get_object_count());
ObjectDB::debug_objects(
[](Object *p_obj, void *p_user_data) {
LocalVector<Pair<ObjectID, StringName>> *debugger_object_ids_ptr = (LocalVector<Pair<ObjectID, StringName>> *)p_user_data;
debugger_object_ids_ptr->push_back(Pair<ObjectID, StringName>(p_obj->get_instance_id(), p_obj->get_class_name()));
},
(void *)&debugger_object_ids);
// Get SnapshotDataTransportObject from ObjectID list now that DB is unlocked.
LocalVector<SnapshotDataTransportObject> debugger_objects;
debugger_objects.reserve(debugger_object_ids.size());
for (Pair<ObjectID, StringName> ids : debugger_object_ids) {
ObjectID oid = ids.first;
Object *obj = ObjectDB::get_instance(oid);
if (unlikely(obj == nullptr)) {
print_verbose(vformat("Object of class '%s' with ID %ud was found to be deleted after ObjectDB was snapshotted.", ids.second, (uint64_t)oid));
continue;
}
if (ids.second == SNAME("EditorInterface")) {
// The EditorInterface + EditorNode is _kind of_ constructed in a debug game, but many properties are null
// We can prevent it from being constructed, but that would break other projects so better to just skip it.
continue;
}
// This is the same way objects in the remote scene tree are serialized,
// but here we add a few extra properties via the extra_debug_data dictionary.
SnapshotDataTransportObject debug_data(obj);
// If we're RefCounted, send over our RefCount too. Could add code here to add a few other interesting properties.
RefCounted *ref = Object::cast_to<RefCounted>(obj);
if (ref) {
debug_data.extra_debug_data["ref_count"] = ref->get_reference_count();
}
Node *node = Object::cast_to<Node>(obj);
if (node) {
debug_data.extra_debug_data["node_name"] = node->get_name();
if (node->get_parent() != nullptr) {
debug_data.extra_debug_data["node_parent"] = node->get_parent()->get_instance_id();
}
debug_data.extra_debug_data["node_is_scene_root"] = SceneTree::get_singleton()->get_root() == node;
Array children;
for (int i = 0; i < node->get_child_count(); i++) {
children.push_back(node->get_child(i)->get_instance_id());
}
debug_data.extra_debug_data["node_children"] = children;
}
debugger_objects.push_back(debug_data);
}
// Add a header to the snapshot with general data about the state of the game, not tied to any particular object.
p_snapshot_context["mem_usage"] = Memory::get_mem_usage();
p_snapshot_context["mem_max_usage"] = Memory::get_mem_max_usage();
p_snapshot_context["timestamp"] = Time::get_singleton()->get_unix_time_from_system();
p_snapshot_context["game_version"] = get_godot_version_string();
p_arr->push_back(p_snapshot_context);
for (SnapshotDataTransportObject &debug_data : debugger_objects) {
debug_data.serialize(*p_arr);
p_arr->push_back(debug_data.extra_debug_data);
}
print_verbose("Snapshot size: " + String::num_uint64(p_arr->size()));
}
Error SnapshotCollector::parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured) {
r_captured = true;
if (p_msg == "request_prepare_snapshot") {
int request_id = p_args[0];
Dictionary snapshot_context;
snapshot_context["editor_version"] = (String)p_args[1];
Array objects;
snapshot_objects(&objects, snapshot_context);
// Debugger networking has a limit on both how many objects can be queued to send and how
// many bytes can be queued to send. Serializing to a string means we never hit the object
// limit, and only have to deal with the byte limit.
// Compress the snapshot in the game client to make sending the snapshot from game to editor a little faster.
CoreBind::Marshalls *m = CoreBind::Marshalls::get_singleton();
Vector<uint8_t> objs_buffer = m->base64_to_raw(m->variant_to_base64(objects));
Vector<uint8_t> objs_buffer_compressed;
objs_buffer_compressed.resize(objs_buffer.size());
int new_size = Compression::compress(objs_buffer_compressed.ptrw(), objs_buffer.ptrw(), objs_buffer.size(), Compression::MODE_DEFLATE);
objs_buffer_compressed.resize(new_size);
pending_snapshots[request_id] = objs_buffer_compressed;
// Tell the editor how long the snapshot is.
Array resp = { request_id, pending_snapshots[request_id].size() };
EngineDebugger::get_singleton()->send_message("snapshot:snapshot_prepared", resp);
} else if (p_msg == "request_snapshot_chunk") {
int request_id = p_args[0];
int begin = p_args[1];
int end = p_args[2];
Array resp = { request_id, pending_snapshots[request_id].slice(begin, end) };
EngineDebugger::get_singleton()->send_message("snapshot:snapshot_chunk", resp);
// If we sent the last part of the string, delete it locally.
if (end >= pending_snapshots[request_id].size()) {
pending_snapshots.erase(request_id);
}
} else {
r_captured = false;
}
return OK;
}
String SnapshotCollector::get_godot_version_string() {
String hash = String(VERSION_HASH);
if (hash.length() != 0) {
hash = " " + vformat("[%s]", hash.left(9));
}
return "v" VERSION_FULL_BUILD + hash;
}

View file

@ -0,0 +1,53 @@
/**************************************************************************/
/* snapshot_collector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/debugger/scene_debugger.h"
struct SnapshotDataTransportObject : public SceneDebuggerObject {
SnapshotDataTransportObject() :
SceneDebuggerObject() {}
SnapshotDataTransportObject(Object *p_obj) :
SceneDebuggerObject(p_obj) {}
Dictionary extra_debug_data;
};
class SnapshotCollector {
inline static HashMap<int, Vector<uint8_t>> pending_snapshots;
public:
static void snapshot_objects(Array *p_arr, Dictionary &p_snapshot_context);
static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
static void initialize();
static void deinitialize();
static String get_godot_version_string();
};

View file

@ -727,18 +727,20 @@ void SceneDebugger::reload_cached_files(const PackedStringArray &p_files) {
}
}
SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) :
SceneDebuggerObject(ObjectDB::get_instance(p_id)) {
}
/// SceneDebuggerObject
SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
id = ObjectID();
Object *obj = ObjectDB::get_instance(p_id);
if (!obj) {
SceneDebuggerObject::SceneDebuggerObject(Object *p_obj) {
if (!p_obj) {
return;
}
id = p_id;
class_name = obj->get_class();
id = p_obj->get_instance_id();
class_name = p_obj->get_class();
if (ScriptInstance *si = obj->get_script_instance()) {
if (ScriptInstance *si = p_obj->get_script_instance()) {
// Read script instance constants and variables
if (!si->get_script().is_null()) {
Script *s = si->get_script().ptr();
@ -746,7 +748,7 @@ SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
}
}
if (Node *node = Object::cast_to<Node>(obj)) {
if (Node *node = Object::cast_to<Node>(p_obj)) {
// For debugging multiplayer.
{
PropertyInfo pi(Variant::INT, String("Node/multiplayer_authority"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY);
@ -761,17 +763,17 @@ SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
PropertyInfo pi(Variant::STRING, String("Node/path"));
properties.push_back(SceneDebuggerProperty(pi, "[Orphan]"));
}
} else if (Script *s = Object::cast_to<Script>(obj)) {
} else if (Script *s = Object::cast_to<Script>(p_obj)) {
// Add script constants (no instance).
_parse_script_properties(s, nullptr);
}
// Add base object properties.
List<PropertyInfo> pinfo;
obj->get_property_list(&pinfo, true);
p_obj->get_property_list(&pinfo, true);
for (const PropertyInfo &E : pinfo) {
if (E.usage & (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CATEGORY)) {
properties.push_back(SceneDebuggerProperty(E, obj->get(E.name)));
properties.push_back(SceneDebuggerProperty(E, p_obj->get(E.name)));
}
}
}
@ -870,13 +872,16 @@ void SceneDebuggerObject::deserialize(const Array &p_arr) {
CHECK_TYPE(p_arr[1], STRING);
CHECK_TYPE(p_arr[2], ARRAY);
id = uint64_t(p_arr[0]);
class_name = p_arr[1];
Array props = p_arr[2];
deserialize(uint64_t(p_arr[0]), p_arr[1], p_arr[2]);
}
for (int i = 0; i < props.size(); i++) {
CHECK_TYPE(props[i], ARRAY);
Array prop = props[i];
void SceneDebuggerObject::deserialize(uint64_t p_id, const String &p_class_name, const Array &p_props) {
id = p_id;
class_name = p_class_name;
for (int i = 0; i < p_props.size(); i++) {
CHECK_TYPE(p_props[i], ARRAY);
Array prop = p_props[i];
ERR_FAIL_COND(prop.size() != 6);
CHECK_TYPE(prop[0], STRING);

View file

@ -143,10 +143,12 @@ public:
List<SceneDebuggerProperty> properties;
SceneDebuggerObject(ObjectID p_id);
SceneDebuggerObject(Object *p_obj);
SceneDebuggerObject() {}
void serialize(Array &r_arr, int p_max_size = 1 << 20);
void deserialize(const Array &p_arr);
void deserialize(uint64_t p_id, const String &p_class_name, const Array &p_props);
};
class SceneDebuggerTree {

View file

@ -5807,6 +5807,16 @@ String Tree::get_column_title(int p_column) const {
return columns[p_column].title;
}
void Tree::set_column_title_tooltip_text(int p_column, const String &p_tooltip) {
ERR_FAIL_INDEX(p_column, columns.size());
columns.write[p_column].title_tooltip = p_tooltip;
}
String Tree::get_column_title_tooltip_text(int p_column) const {
ERR_FAIL_INDEX_V(p_column, columns.size(), "");
return columns[p_column].title_tooltip;
}
void Tree::set_column_title_alignment(int p_column, HorizontalAlignment p_alignment) {
ERR_FAIL_INDEX(p_column, columns.size());
@ -6377,7 +6387,33 @@ int Tree::get_button_id_at_position(const Point2 &p_pos) const {
String Tree::get_tooltip(const Point2 &p_pos) const {
Point2 pos = p_pos - theme_cache.panel_style->get_offset();
pos.y -= _get_title_button_height();
// `pos.y` less than 0 indicates we're in the header.
if (pos.y < 0) {
// Get the x position of the cursor.
real_t pos_x = p_pos.x;
if (is_layout_rtl()) {
pos_x = get_size().width - pos_x;
}
pos_x -= theme_cache.panel_style->get_offset().x;
if (h_scroll->is_visible_in_tree()) {
pos_x += h_scroll->get_value();
}
// Walk forwards until we know which column we're in.
int next_edge = 0;
int i = 0;
for (; i < columns.size(); i++) {
if (pos_x < next_edge) {
break;
}
next_edge += get_column_width(i);
}
if (!columns[i - 1].title_tooltip.is_empty()) {
return columns[i - 1].title_tooltip;
}
// If the column has no tooltip, use the default.
return Control::get_tooltip(p_pos);
}
@ -6528,6 +6564,9 @@ void Tree::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_column_title", "column", "title"), &Tree::set_column_title);
ClassDB::bind_method(D_METHOD("get_column_title", "column"), &Tree::get_column_title);
ClassDB::bind_method(D_METHOD("set_column_title_tooltip_text", "column", "tooltip_text"), &Tree::set_column_title_tooltip_text);
ClassDB::bind_method(D_METHOD("get_column_title_tooltip_text", "column"), &Tree::get_column_title_tooltip_text);
ClassDB::bind_method(D_METHOD("set_column_title_alignment", "column", "title_alignment"), &Tree::set_column_title_alignment);
ClassDB::bind_method(D_METHOD("get_column_title_alignment", "column"), &Tree::get_column_title_alignment);

View file

@ -511,6 +511,7 @@ private:
int expand_ratio = 1;
bool expand = true;
bool clip_content = false;
String title_tooltip;
String title;
String xl_title;
HorizontalAlignment title_alignment = HORIZONTAL_ALIGNMENT_CENTER;
@ -839,6 +840,9 @@ public:
void set_column_title(int p_column, const String &p_title);
String get_column_title(int p_column) const;
void set_column_title_tooltip_text(int p_column, const String &p_tooltip);
String get_column_title_tooltip_text(int p_column) const;
void set_column_title_alignment(int p_column, HorizontalAlignment p_alignment);
HorizontalAlignment get_column_title_alignment(int p_column) const;

View file

@ -3371,7 +3371,7 @@ void Node::_set_tree(SceneTree *p_tree) {
#ifdef DEBUG_ENABLED
static HashMap<ObjectID, List<String>> _print_orphan_nodes_map;
static void _print_orphan_nodes_routine(Object *p_obj) {
static void _print_orphan_nodes_routine(Object *p_obj, void *p_user_data) {
Node *n = Object::cast_to<Node>(p_obj);
if (!n) {
return;
@ -3415,7 +3415,7 @@ void Node::print_orphan_nodes() {
_print_orphan_nodes_map.clear();
// Collect and print information about orphan nodes.
ObjectDB::debug_objects(_print_orphan_nodes_routine);
ObjectDB::debug_objects(_print_orphan_nodes_routine, nullptr);
for (const KeyValue<ObjectID, List<String>> &E : _print_orphan_nodes_map) {
print_line(itos(E.key) + " - Stray Node: " + E.value.get(0) + " (Type: " + E.value.get(1) + ") (Source:" + E.value.get(2) + ")");
@ -3432,7 +3432,7 @@ TypedArray<int> Node::get_orphan_node_ids() {
_print_orphan_nodes_map.clear();
// Collect and return information about orphan nodes.
ObjectDB::debug_objects(_print_orphan_nodes_routine);
ObjectDB::debug_objects(_print_orphan_nodes_routine, nullptr);
for (const KeyValue<ObjectID, List<String>> &E : _print_orphan_nodes_map) {
ret.push_back(E.key);