mirror of
https://github.com/godotengine/godot.git
synced 2025-10-19 16:03:29 +00:00
Merge pull request #97210 from AleksLitynski/object-snapshot-debugger
Add an ObjectDB Profiling Tool
This commit is contained in:
commit
f6aa5ba23c
34 changed files with 3885 additions and 24 deletions
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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" />
|
||||
|
|
20
modules/objectdb_profiler/SCsub
Normal file
20
modules/objectdb_profiler/SCsub
Normal 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
|
6
modules/objectdb_profiler/config.py
Normal file
6
modules/objectdb_profiler/config.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
def can_build(env, platform):
|
||||
return env.debug_features
|
||||
|
||||
|
||||
def configure(env):
|
||||
pass
|
282
modules/objectdb_profiler/editor/data_viewers/class_view.cpp
Normal file
282
modules/objectdb_profiler/editor/data_viewers/class_view.cpp
Normal 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();
|
||||
}
|
||||
}
|
74
modules/objectdb_profiler/editor/data_viewers/class_view.h
Normal file
74
modules/objectdb_profiler/editor/data_viewers/class_view.h
Normal 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;
|
||||
};
|
275
modules/objectdb_profiler/editor/data_viewers/node_view.cpp
Normal file
275
modules/objectdb_profiler/editor/data_viewers/node_view.cpp
Normal 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();
|
||||
}
|
86
modules/objectdb_profiler/editor/data_viewers/node_view.h
Normal file
86
modules/objectdb_profiler/editor/data_viewers/node_view.h
Normal 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;
|
||||
};
|
260
modules/objectdb_profiler/editor/data_viewers/object_view.cpp
Normal file
260
modules/objectdb_profiler/editor/data_viewers/object_view.cpp
Normal 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;
|
||||
}
|
63
modules/objectdb_profiler/editor/data_viewers/object_view.h
Normal file
63
modules/objectdb_profiler/editor/data_viewers/object_view.h
Normal 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;
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
}
|
127
modules/objectdb_profiler/editor/data_viewers/shared_controls.h
Normal file
127
modules/objectdb_profiler/editor/data_viewers/shared_controls.h
Normal 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();
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
};
|
284
modules/objectdb_profiler/editor/data_viewers/summary_view.cpp
Normal file
284
modules/objectdb_profiler/editor/data_viewers/summary_view.cpp
Normal 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)));
|
||||
}
|
67
modules/objectdb_profiler/editor/data_viewers/summary_view.h
Normal file
67
modules/objectdb_profiler/editor/data_viewers/summary_view.h
Normal 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;
|
||||
};
|
464
modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp
Normal file
464
modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp
Normal 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);
|
||||
}
|
103
modules/objectdb_profiler/editor/objectdb_profiler_panel.h
Normal file
103
modules/objectdb_profiler/editor/objectdb_profiler_panel.h
Normal 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);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
65
modules/objectdb_profiler/editor/objectdb_profiler_plugin.h
Normal file
65
modules/objectdb_profiler/editor/objectdb_profiler_plugin.h
Normal 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;
|
||||
};
|
374
modules/objectdb_profiler/editor/snapshot_data.cpp
Normal file
374
modules/objectdb_profiler/editor/snapshot_data.cpp
Normal 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;
|
||||
}
|
111
modules/objectdb_profiler/editor/snapshot_data.h
Normal file
111
modules/objectdb_profiler/editor/snapshot_data.h
Normal 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();
|
||||
};
|
54
modules/objectdb_profiler/register_types.cpp
Normal file
54
modules/objectdb_profiler/register_types.cpp
Normal 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();
|
||||
}
|
||||
}
|
36
modules/objectdb_profiler/register_types.h
Normal file
36
modules/objectdb_profiler/register_types.h
Normal 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);
|
175
modules/objectdb_profiler/snapshot_collector.cpp
Normal file
175
modules/objectdb_profiler/snapshot_collector.cpp
Normal 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;
|
||||
}
|
53
modules/objectdb_profiler/snapshot_collector.h
Normal file
53
modules/objectdb_profiler/snapshot_collector.h
Normal 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();
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue