From 78f1543e35b37a09b087dcda83607dd21ca9bcea Mon Sep 17 00:00:00 2001 From: Aleksander Litynski Date: Tue, 6 May 2025 10:30:52 +0200 Subject: [PATCH 1/3] Add an ObjectDB Profiling Tool A new tab is added to the debugger that can help profile a game's memory usage. Specifically, this lets you save a snapshot of all the objects in a running game's ObjectDB to disk. It then lets you view the snapshot and diff two snapshots against each other. This is meant to work similarly to Chrome's heap snapshot tool or Unity's memory profiler. --- core/object/object.cpp | 8 +- core/object/object.h | 4 +- doc/classes/Tree.xml | 15 + editor/editor_json_visualizer.cpp | 130 +++++ editor/editor_json_visualizer.h | 51 ++ ...editor_native_shader_source_visualizer.cpp | 87 +--- .../editor_native_shader_source_visualizer.h | 4 +- modules/objectdb_profiler/SCsub | 20 + modules/objectdb_profiler/config.py | 9 + .../editor/data_viewers/class_view.cpp | 274 +++++++++++ .../editor/data_viewers/class_view.h | 73 +++ .../editor/data_viewers/json_view.cpp | 163 +++++++ .../editor/data_viewers/json_view.h | 57 +++ .../editor/data_viewers/node_view.cpp | 262 +++++++++++ .../editor/data_viewers/node_view.h | 85 ++++ .../editor/data_viewers/object_view.cpp | 251 ++++++++++ .../editor/data_viewers/object_view.h | 63 +++ .../editor/data_viewers/refcounted_view.cpp | 310 ++++++++++++ .../editor/data_viewers/refcounted_view.h | 61 +++ .../editor/data_viewers/shared_controls.cpp | 248 ++++++++++ .../editor/data_viewers/shared_controls.h | 127 +++++ .../editor/data_viewers/snapshot_view.cpp | 70 +++ .../editor/data_viewers/snapshot_view.h | 54 +++ .../editor/data_viewers/summary_view.cpp | 282 +++++++++++ .../editor/data_viewers/summary_view.h | 66 +++ .../editor/objectdb_profiler_panel.cpp | 445 ++++++++++++++++++ .../editor/objectdb_profiler_panel.h | 102 ++++ .../editor/objectdb_profiler_plugin.cpp | 66 +++ .../editor/objectdb_profiler_plugin.h | 65 +++ .../editor/snapshot_data.cpp | 388 +++++++++++++++ .../objectdb_profiler/editor/snapshot_data.h | 111 +++++ modules/objectdb_profiler/register_types.cpp | 54 +++ modules/objectdb_profiler/register_types.h | 36 ++ .../objectdb_profiler/snapshot_collector.cpp | 183 +++++++ .../objectdb_profiler/snapshot_collector.h | 52 ++ scene/debugger/scene_debugger.cpp | 39 +- scene/debugger/scene_debugger.h | 2 + scene/gui/tree.cpp | 44 ++ scene/gui/tree.h | 4 + scene/main/node.cpp | 6 +- 40 files changed, 4262 insertions(+), 109 deletions(-) create mode 100644 editor/editor_json_visualizer.cpp create mode 100644 editor/editor_json_visualizer.h create mode 100644 modules/objectdb_profiler/SCsub create mode 100644 modules/objectdb_profiler/config.py create mode 100644 modules/objectdb_profiler/editor/data_viewers/class_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/class_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/json_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/json_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/node_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/node_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/object_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/object_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/refcounted_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/shared_controls.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/snapshot_view.h create mode 100644 modules/objectdb_profiler/editor/data_viewers/summary_view.cpp create mode 100644 modules/objectdb_profiler/editor/data_viewers/summary_view.h create mode 100644 modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp create mode 100644 modules/objectdb_profiler/editor/objectdb_profiler_panel.h create mode 100644 modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp create mode 100644 modules/objectdb_profiler/editor/objectdb_profiler_plugin.h create mode 100644 modules/objectdb_profiler/editor/snapshot_data.cpp create mode 100644 modules/objectdb_profiler/editor/snapshot_data.h create mode 100644 modules/objectdb_profiler/register_types.cpp create mode 100644 modules/objectdb_profiler/register_types.h create mode 100644 modules/objectdb_profiler/snapshot_collector.cpp create mode 100644 modules/objectdb_profiler/snapshot_collector.h diff --git a/core/object/object.cpp b/core/object/object.cpp index 759525aa9df..c82131df93c 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -2334,12 +2334,11 @@ 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--; } } @@ -2507,6 +2506,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 = " - RefCount: " + itos(((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. diff --git a/core/object/object.h b/core/object/object.h index fe1ef660b56..223f9a284cb 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -1046,7 +1046,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; @@ -1078,6 +1078,6 @@ public: template _ALWAYS_INLINE_ static Ref 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(); }; diff --git a/doc/classes/Tree.xml b/doc/classes/Tree.xml index 7f15eb4b980..5f1296bc102 100644 --- a/doc/classes/Tree.xml +++ b/doc/classes/Tree.xml @@ -124,6 +124,13 @@ Returns column title language code. + + + + + Returns the column title's tooltip text. + + @@ -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. + + + + + + Sets the column title's tooltip text. + + diff --git a/editor/editor_json_visualizer.cpp b/editor/editor_json_visualizer.cpp new file mode 100644 index 00000000000..5ded66ebadf --- /dev/null +++ b/editor/editor_json_visualizer.cpp @@ -0,0 +1,130 @@ +/**************************************************************************/ +/* editor_json_visualizer.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 "editor_json_visualizer.h" + +#include "editor/editor_settings.h" +#include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/text_edit.h" +#include "servers/rendering/shader_language.h" + +EditorJsonVisualizerSyntaxHighlighter::EditorJsonVisualizerSyntaxHighlighter(const List &p_keywords) { + set_number_color(EDITOR_GET("text_editor/theme/highlighting/number_color")); + set_symbol_color(EDITOR_GET("text_editor/theme/highlighting/symbol_color")); + set_function_color(EDITOR_GET("text_editor/theme/highlighting/function_color")); + set_member_variable_color(EDITOR_GET("text_editor/theme/highlighting/member_variable_color")); + + clear_keyword_colors(); + const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color"); + const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color"); + + for (const String &keyword : p_keywords) { + if (ShaderLanguage::is_control_flow_keyword(keyword)) { + add_keyword_color(keyword, control_flow_keyword_color); + } else { + add_keyword_color(keyword, keyword_color); + } + } + + // Colorize comments. + const Color comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color"); + clear_color_regions(); + add_color_region("/*", "*/", comment_color, false); + add_color_region("//", "", comment_color, true); + + // Colorize preprocessor statements. + const Color user_type_color = EDITOR_GET("text_editor/theme/highlighting/user_type_color"); + add_color_region("#", "", user_type_color, true); + + set_uint_suffix_enabled(true); +} + +void EditorJsonVisualizer::load_theme(Ref p_syntax_highlighter) { + set_editable(false); + set_syntax_highlighter(p_syntax_highlighter); + add_theme_font_override(SceneStringName(font), get_theme_font("source", EditorStringName(EditorFonts))); + add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size("source_size", EditorStringName(EditorFonts))); + add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing")); + + // Appearance: Caret + set_caret_type((TextEdit::CaretType)EDITOR_GET("text_editor/appearance/caret/type").operator int()); + set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink")); + set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval")); + set_highlight_current_line(EDITOR_GET("text_editor/appearance/caret/highlight_current_line")); + set_highlight_all_occurrences(EDITOR_GET("text_editor/appearance/caret/highlight_all_occurrences")); + + // Appearance: Gutters + set_draw_line_numbers(EDITOR_GET("text_editor/appearance/gutters/show_line_numbers")); + set_line_numbers_zero_padded(EDITOR_GET("text_editor/appearance/gutters/line_numbers_zero_padded")); + + // Appearance: Minimap + set_draw_minimap(EDITOR_GET("text_editor/appearance/minimap/show_minimap")); + set_minimap_width((int)EDITOR_GET("text_editor/appearance/minimap/minimap_width") * EDSCALE); + + // Appearance: Lines + set_line_folding_enabled(EDITOR_GET("text_editor/appearance/lines/code_folding")); + set_draw_fold_gutter(EDITOR_GET("text_editor/appearance/lines/code_folding")); + set_line_wrapping_mode((TextEdit::LineWrappingMode)EDITOR_GET("text_editor/appearance/lines/word_wrap").operator int()); + set_autowrap_mode((TextServer::AutowrapMode)EDITOR_GET("text_editor/appearance/lines/autowrap_mode").operator int()); + + // Appearance: Whitespace + set_draw_tabs(EDITOR_GET("text_editor/appearance/whitespace/draw_tabs")); + set_draw_spaces(EDITOR_GET("text_editor/appearance/whitespace/draw_spaces")); + add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing")); + + // Behavior: Navigation + set_scroll_past_end_of_file_enabled(EDITOR_GET("text_editor/behavior/navigation/scroll_past_end_of_file")); + set_smooth_scroll_enabled(EDITOR_GET("text_editor/behavior/navigation/smooth_scrolling")); + set_v_scroll_speed(EDITOR_GET("text_editor/behavior/navigation/v_scroll_speed")); + set_drag_and_drop_selection_enabled(EDITOR_GET("text_editor/behavior/navigation/drag_and_drop_selection")); + + // Behavior: Indent + set_indent_size(EDITOR_GET("text_editor/behavior/indent/size")); + set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent")); + set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines")); +} + +void EditorJsonVisualizer::_notification(int p_what) { + if (p_what == NOTIFICATION_THEME_CHANGED) { + Ref source_font = get_theme_font("source", EditorStringName(EditorFonts)); + int source_font_size = get_theme_font_size("source_size", EditorStringName(EditorFonts)); + int line_spacing = EDITOR_GET("text_editor/theme/line_spacing"); + if (get_theme_font(SceneStringName(font)) != source_font) { + add_theme_font_override(SceneStringName(font), source_font); + } + if (get_theme_font_size(SceneStringName(font_size)) != source_font_size) { + add_theme_font_size_override(SceneStringName(font_size), source_font_size); + } + if (get_theme_constant("line_spacing") != line_spacing) { + add_theme_constant_override("line_spacing", line_spacing); + } + } +} diff --git a/editor/editor_json_visualizer.h b/editor/editor_json_visualizer.h new file mode 100644 index 00000000000..71320f73e2a --- /dev/null +++ b/editor/editor_json_visualizer.h @@ -0,0 +1,51 @@ +/**************************************************************************/ +/* editor_json_visualizer.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/code_edit.h" +#include "scene/resources/syntax_highlighter.h" + +class EditorJsonVisualizerSyntaxHighlighter : public CodeHighlighter { + GDCLASS(EditorJsonVisualizerSyntaxHighlighter, CodeHighlighter) + +public: + EditorJsonVisualizerSyntaxHighlighter(const List &p_keywords); +}; + +class EditorJsonVisualizer : public CodeEdit { + GDCLASS(EditorJsonVisualizer, CodeEdit) + +protected: + void _notification(int p_what); + +public: + void load_theme(Ref p_syntax_highlighter); +}; diff --git a/editor/shader/editor_native_shader_source_visualizer.cpp b/editor/shader/editor_native_shader_source_visualizer.cpp index 9d9dd5af6eb..445c8025387 100644 --- a/editor/shader/editor_native_shader_source_visualizer.cpp +++ b/editor/shader/editor_native_shader_source_visualizer.cpp @@ -37,40 +37,6 @@ #include "scene/gui/text_edit.h" #include "servers/rendering/shader_language.h" -void EditorNativeShaderSourceVisualizer::_load_theme_settings() { - syntax_highlighter->set_number_color(EDITOR_GET("text_editor/theme/highlighting/number_color")); - syntax_highlighter->set_symbol_color(EDITOR_GET("text_editor/theme/highlighting/symbol_color")); - syntax_highlighter->set_function_color(EDITOR_GET("text_editor/theme/highlighting/function_color")); - syntax_highlighter->set_member_variable_color(EDITOR_GET("text_editor/theme/highlighting/member_variable_color")); - - syntax_highlighter->clear_keyword_colors(); - - List keywords; - ShaderLanguage::get_keyword_list(&keywords); - const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color"); - const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color"); - - for (const String &keyword : keywords) { - if (ShaderLanguage::is_control_flow_keyword(keyword)) { - syntax_highlighter->add_keyword_color(keyword, control_flow_keyword_color); - } else { - syntax_highlighter->add_keyword_color(keyword, keyword_color); - } - } - - // Colorize comments. - const Color comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color"); - syntax_highlighter->clear_color_regions(); - syntax_highlighter->add_color_region("/*", "*/", comment_color, false); - syntax_highlighter->add_color_region("//", "", comment_color, true); - - // Colorize preprocessor statements. - const Color user_type_color = EDITOR_GET("text_editor/theme/highlighting/user_type_color"); - syntax_highlighter->add_color_region("#", "", user_type_color, true); - - syntax_highlighter->set_uint_suffix_enabled(true); -} - void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) { if (versions) { memdelete(versions); @@ -79,7 +45,10 @@ void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) { RS::ShaderNativeSourceCode nsc = RS::get_singleton()->shader_get_native_source_code(p_shader); - _load_theme_settings(); + List keywords; + ShaderLanguage::get_keyword_list(&keywords); + Ref syntax_highlighter; + syntax_highlighter.instantiate(keywords); versions = memnew(TabContainer); versions->set_tab_alignment(TabBar::ALIGNMENT_CENTER); @@ -93,50 +62,8 @@ void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) { vtab->set_h_size_flags(Control::SIZE_EXPAND_FILL); versions->add_child(vtab); for (int j = 0; j < nsc.versions[i].stages.size(); j++) { - CodeEdit *code_edit = memnew(CodeEdit); - code_edit->set_editable(false); - code_edit->set_syntax_highlighter(syntax_highlighter); - code_edit->add_theme_font_override(SceneStringName(font), get_theme_font("source", EditorStringName(EditorFonts))); - code_edit->add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size("source_size", EditorStringName(EditorFonts))); - code_edit->add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing")); - - // Appearance: Caret - code_edit->set_caret_type((TextEdit::CaretType)EDITOR_GET("text_editor/appearance/caret/type").operator int()); - code_edit->set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink")); - code_edit->set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval")); - code_edit->set_highlight_current_line(EDITOR_GET("text_editor/appearance/caret/highlight_current_line")); - code_edit->set_highlight_all_occurrences(EDITOR_GET("text_editor/appearance/caret/highlight_all_occurrences")); - - // Appearance: Gutters - code_edit->set_draw_line_numbers(EDITOR_GET("text_editor/appearance/gutters/show_line_numbers")); - code_edit->set_line_numbers_zero_padded(EDITOR_GET("text_editor/appearance/gutters/line_numbers_zero_padded")); - - // Appearance: Minimap - code_edit->set_draw_minimap(EDITOR_GET("text_editor/appearance/minimap/show_minimap")); - code_edit->set_minimap_width((int)EDITOR_GET("text_editor/appearance/minimap/minimap_width") * EDSCALE); - - // Appearance: Lines - code_edit->set_line_folding_enabled(EDITOR_GET("text_editor/appearance/lines/code_folding")); - code_edit->set_draw_fold_gutter(EDITOR_GET("text_editor/appearance/lines/code_folding")); - code_edit->set_line_wrapping_mode((TextEdit::LineWrappingMode)EDITOR_GET("text_editor/appearance/lines/word_wrap").operator int()); - code_edit->set_autowrap_mode((TextServer::AutowrapMode)EDITOR_GET("text_editor/appearance/lines/autowrap_mode").operator int()); - - // Appearance: Whitespace - code_edit->set_draw_tabs(EDITOR_GET("text_editor/appearance/whitespace/draw_tabs")); - code_edit->set_draw_spaces(EDITOR_GET("text_editor/appearance/whitespace/draw_spaces")); - code_edit->add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing")); - - // Behavior: Navigation - code_edit->set_scroll_past_end_of_file_enabled(EDITOR_GET("text_editor/behavior/navigation/scroll_past_end_of_file")); - code_edit->set_smooth_scroll_enabled(EDITOR_GET("text_editor/behavior/navigation/smooth_scrolling")); - code_edit->set_v_scroll_speed(EDITOR_GET("text_editor/behavior/navigation/v_scroll_speed")); - code_edit->set_drag_and_drop_selection_enabled(EDITOR_GET("text_editor/behavior/navigation/drag_and_drop_selection")); - - // Behavior: Indent - code_edit->set_indent_size(EDITOR_GET("text_editor/behavior/indent/size")); - code_edit->set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent")); - code_edit->set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines")); - + EditorJsonVisualizer *code_edit = memnew(EditorJsonVisualizer); + code_edit->load_theme(syntax_highlighter); code_edit->set_name(nsc.versions[i].stages[j].name); code_edit->set_text(nsc.versions[i].stages[j].code); code_edit->set_v_size_flags(Control::SIZE_EXPAND_FILL); @@ -153,8 +80,6 @@ void EditorNativeShaderSourceVisualizer::_bind_methods() { } EditorNativeShaderSourceVisualizer::EditorNativeShaderSourceVisualizer() { - syntax_highlighter.instantiate(); - add_to_group("_native_shader_source_visualizer"); set_title(TTR("Native Shader Source Inspector")); } diff --git a/editor/shader/editor_native_shader_source_visualizer.h b/editor/shader/editor_native_shader_source_visualizer.h index f75e165c77b..e49bb71b764 100644 --- a/editor/shader/editor_native_shader_source_visualizer.h +++ b/editor/shader/editor_native_shader_source_visualizer.h @@ -30,16 +30,14 @@ #pragma once +#include "editor/editor_json_visualizer.h" #include "scene/gui/dialogs.h" #include "scene/gui/tab_container.h" -#include "scene/resources/syntax_highlighter.h" class EditorNativeShaderSourceVisualizer : public AcceptDialog { GDCLASS(EditorNativeShaderSourceVisualizer, AcceptDialog) TabContainer *versions = nullptr; - Ref syntax_highlighter; - void _load_theme_settings(); void _inspect_shader(RID p_shader); protected: diff --git a/modules/objectdb_profiler/SCsub b/modules/objectdb_profiler/SCsub new file mode 100644 index 00000000000..0b628d0e056 --- /dev/null +++ b/modules/objectdb_profiler/SCsub @@ -0,0 +1,20 @@ +#!/usr/bin/env python +from misc.utility.scons_hints import * + +Import("env") +Import("env_modules") + +env_mp = env_modules.Clone() + +module_obj = [] + +# Only include in editor and debug builds. +if env_mp.debug_features: + env_mp.add_source_files(module_obj, "*.cpp") + + # Only the editor needs these files, don't include them in the game. + if env.editor_build: + env_mp.add_source_files(module_obj, "editor/*.cpp") + env_mp.add_source_files(module_obj, "editor/data_viewers/*.cpp") + +env.modules_sources += module_obj diff --git a/modules/objectdb_profiler/config.py b/modules/objectdb_profiler/config.py new file mode 100644 index 00000000000..17753c1b68b --- /dev/null +++ b/modules/objectdb_profiler/config.py @@ -0,0 +1,9 @@ +# config.py + + +def can_build(env, platform): + return env.debug_features + + +def configure(env): + pass diff --git a/modules/objectdb_profiler/editor/data_viewers/class_view.cpp b/modules/objectdb_profiler/editor/data_viewers/class_view.cpp new file mode 100644 index 00000000000..bff623a91d7 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/class_view.cpp @@ -0,0 +1,274 @@ +/**************************************************************************/ +/* 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 "editor/editor_node.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/panel_container.h" +#include "scene/gui/split_container.h" +#include "shared_controls.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 &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(TTR("Classes")); + + class_tree = nullptr; + object_list = nullptr; + diff_object_list = nullptr; +} + +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, TTR("Filter Classes"))); + filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0); + + TreeSortAndFilterBar::SortOptionIndexes default_sort; + if (!diff_data) { + default_sort = filter_bar->add_sort_option(TTR("Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1); + } else { + filter_bar->add_sort_option(TTR("A Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1); + filter_bar->add_sort_option(TTR("B Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 2); + default_sort = filter_bar->add_sort_option(TTR("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_list_column->add_child(class_tree); + 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, TTR("Object 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 ? TTR("A Count") : TTR("Count")); + class_tree->set_column_expand(1, false); + if (diff_data) { + class_tree->set_column_title(2, TTR("B Count")); + class_tree->set_column_expand(2, false); + class_tree->set_column_title(3, TTR("Delta")); + class_tree->set_column_expand(3, false); + + // Add tooltip with the names of snapshot A and B + class_tree->set_column_title_tooltip_text(1, TTR("A: ") + snapshot_data->name); + class_tree->set_column_title_tooltip_text(2, TTR("B: ") + diff_data->name); + } + 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); + + 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(TTR("Objects"))); + } else { + object_lists->add_child(object_list = _make_object_list_tree(TTR("A Objects"))); + object_lists->add_child(diff_object_list = _make_object_list_tree(TTR("B Objects"))); + } + + HashMap 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 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.get(0); + 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)) + ")"); + 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 &p_class_map, GameStateSnapshot *p_objects) { + for (const KeyValue &pair : p_objects->objects) { + StringName class_name = StringName(pair.value->type_name); + StringName parent_class_name = class_name != StringName() && 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 != StringName()) { + 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 != StringName() ? 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((Object *)snapshot->objects[object_id]); +} + +void SnapshotClassView::_class_selected() { + if (!diff_data) { + _populate_object_list(snapshot_data, object_list, TTR("Objects")); + } else { + _populate_object_list(snapshot_data, object_list, TTR("A Objects")); + _populate_object_list(diff_data, diff_object_list, TTR("B Objects")); + } +} + +void SnapshotClassView::_populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base) { + p_list->clear(); + String class_name = class_tree->get_selected()->get_metadata(0); + TreeItem *root = p_list->create_item(); + int object_count = 0; + for (const KeyValue &pair : p_snapshot->objects) { + if (pair.value->type_name == class_name) { + TreeItem *item = p_list->create_item(root); + 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, p_name_base + " (" + itos(object_count) + ")"); +} + +void SnapshotClassView::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + for (TreeItem *item : _get_children_recursive(class_tree)) { + item->set_icon(0, EditorNode::get_singleton()->get_class_icon(item->get_metadata(0), "")); + } + + } break; + } +} diff --git a/modules/objectdb_profiler/editor/data_viewers/class_view.h b/modules/objectdb_profiler/editor/data_viewers/class_view.h new file mode 100644 index 00000000000..792f2b03e84 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/class_view.h @@ -0,0 +1,73 @@ +/**************************************************************************/ +/* 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 child_classes; + List instances; + TreeItem *tree_node = nullptr; + HashMap recursive_instance_count_cache; + + int instance_count(GameStateSnapshot *p_snapshot = nullptr); + int get_recursive_instance_count(HashMap &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 &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); + +public: + SnapshotClassView(); + virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override; +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/json_view.cpp b/modules/objectdb_profiler/editor/data_viewers/json_view.cpp new file mode 100644 index 00000000000..282d738f381 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/json_view.cpp @@ -0,0 +1,163 @@ +/**************************************************************************/ +/* json_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 "json_view.h" + +#include "core/io/json.h" +#include "scene/gui/center_container.h" +#include "scene/gui/panel_container.h" +#include "scene/gui/split_container.h" +#include "shared_controls.h" + +SnapshotJsonView::SnapshotJsonView() { + set_name(TTR("JSON")); +} + +void SnapshotJsonView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) { + // Lock isn't released until the data processing background thread has finished running + // and the json has been passed back to the main thread and displayed. + SnapshotView::show_snapshot(p_data, p_diff_data); + + HSplitContainer *box = memnew(HSplitContainer); + box->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); + add_child(box); + + loading_panel = memnew(DarkPanelContainer); + CenterContainer *loading_center = memnew(CenterContainer); + Label *loading_label = memnew(Label(TTR("Loading"))); + add_child(loading_panel); + loading_panel->add_child(loading_center); + loading_center->add_child(loading_label); + loading_panel->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); + loading_center->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); + + VBoxContainer *json_box = memnew(VBoxContainer); + json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL); + json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL); + box->add_child(json_box); + String hdr_a_text = diff_data ? TTR("Snapshot A JSON") : TTR("Snapshot JSON"); + SpanningHeader *hdr_a = memnew(SpanningHeader(hdr_a_text)); + if (diff_data) { + hdr_a->set_tooltip_text(TTR("Snapshot A: ") + snapshot_data->name); + } + json_box->add_child(hdr_a); + + Ref syntax_highlighter; + syntax_highlighter.instantiate(List()); + + json_content = memnew(EditorJsonVisualizer); + json_content->load_theme(syntax_highlighter); + json_content->set_name(hdr_a_text); + json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL); + json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL); + json_box->add_child(json_content); + + if (diff_data) { + VBoxContainer *diff_json_box = memnew(VBoxContainer); + diff_json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL); + diff_json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL); + box->add_child(diff_json_box); + String hrd_b_text = TTR("Snapshot B JSON"); + SpanningHeader *hdr_b = memnew(SpanningHeader(hrd_b_text)); + hdr_b->set_tooltip_text(TTR("Snapshot B: ") + diff_data->name); + diff_json_box->add_child(hdr_b); + + diff_json_content = memnew(EditorJsonVisualizer); + diff_json_content->load_theme(syntax_highlighter); + diff_json_content->set_name(hrd_b_text); + diff_json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL); + diff_json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL); + diff_json_box->add_child(diff_json_content); + } + + WorkerThreadPool::get_singleton()->add_native_task(&SnapshotJsonView::_serialization_worker, this); +} + +String SnapshotJsonView::_snapshot_to_json(GameStateSnapshot *p_snapshot) { + if (p_snapshot == nullptr) { + return ""; + } + Dictionary json_data; + json_data["name"] = p_snapshot->name; + Dictionary objects; + for (const KeyValue &obj : p_snapshot->objects) { + Dictionary obj_data; + obj_data["type_name"] = obj.value->type_name; + + Array prop_list; + for (const PropertyInfo &prop : obj.value->prop_list) { + prop_list.push_back((Dictionary)prop); + } + objects["prop_list"] = prop_list; + + Dictionary prop_values; + for (const KeyValue &prop : obj.value->prop_values) { + // should only ever be one entry in this context + prop_values[prop.key] = prop.value; + } + obj_data["prop_values"] = prop_values; + + objects[obj.key] = obj_data; + } + json_data["objects"] = objects; + return JSON::stringify(json_data, " ", true, true); +} + +void SnapshotJsonView::_serialization_worker(void *p_ud) { + // About 0.3s to serialize snapshots in a small game. + SnapshotJsonView *self = static_cast(p_ud); + GameStateSnapshot *snapshot_data = self->snapshot_data; + GameStateSnapshot *diff_data = self->diff_data; + // let the message queue figure out if self is still a valid object or if it's been destroyed. + MessageQueue::get_singleton()->push_call(self, "_update_text", + snapshot_data, diff_data, + _snapshot_to_json(snapshot_data), + _snapshot_to_json(diff_data)); +} + +void SnapshotJsonView::_update_text(GameStateSnapshot *p_data_ptr, GameStateSnapshot *p_diff_ptr, const String &p_data_str, const String &p_diff_data_str) { + if (p_data_ptr != snapshot_data || p_diff_ptr != diff_data) { + // If the GameStateSnapshots we generated strings for no longer match the snapshots we asked for, + // throw these results away. We'll get more from a different worker process. + return; + } + + // About 5s to insert the string into the editor. + json_content->set_text(p_data_str); + if (diff_data) { + diff_json_content->set_text(p_diff_data_str); + } + loading_panel->queue_free(); + // Loading json done, release the lock. +} + +void SnapshotJsonView::_bind_methods() { + ClassDB::bind_method(D_METHOD("_update_text", "p_data_ptr", "p_diff_ptr", "p_data_str", "p_diff_data_str"), &SnapshotJsonView::_update_text); +} diff --git a/modules/objectdb_profiler/editor/data_viewers/json_view.h b/modules/objectdb_profiler/editor/data_viewers/json_view.h new file mode 100644 index 00000000000..d3ef4581706 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/json_view.h @@ -0,0 +1,57 @@ +/**************************************************************************/ +/* json_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 "editor/editor_json_visualizer.h" +#include "snapshot_view.h" + +class SnapshotJsonView : public SnapshotView { + GDCLASS(SnapshotJsonView, SnapshotView); + +protected: + static void _serialization_worker(void *p_ud); + void _update_text(GameStateSnapshot *p_data_ptr, GameStateSnapshot *p_diff_ptr, const String &p_data_str, const String &p_diff_data_str); + + static void _bind_methods(); + + EditorJsonVisualizer *json_content = nullptr; + EditorJsonVisualizer *diff_json_content = nullptr; + + Control *loading_panel = nullptr; + + void _load_theme_settings(); + static String _snapshot_to_json(GameStateSnapshot *p_snapshot); + +public: + SnapshotJsonView(); + virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override; +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/node_view.cpp b/modules/objectdb_profiler/editor/data_viewers/node_view.cpp new file mode 100644 index 00000000000..44d47816b54 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/node_view.cpp @@ -0,0 +1,262 @@ +/**************************************************************************/ +/* 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/split_container.h" + +SnapshotNodeView::SnapshotNodeView() { + set_name("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); + + bool show_diff_label = diff_data && combined_diff_view; + main_tree = _make_node_tree(diff_data && !combined_diff_view ? TTR("A Nodes") : TTR("Nodes"), snapshot_data); + diff_sides->add_child(main_tree.root); + _add_snapshot_to_tree(main_tree.tree, snapshot_data, show_diff_label ? "-" : ""); + + if (diff_data) { + CheckButton *diff_mode_toggle = memnew(CheckButton(TTR("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, "+"); + } else { + // Add a second column with the diff snapshot. + diff_tree = _make_node_tree(TTR("B Nodes"), diff_data); + diff_sides->add_child(diff_tree.root); + _add_snapshot_to_tree(diff_tree.tree, diff_data, ""); + } + } + + _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, GameStateSnapshot *p_snapshot) { + 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, TTR("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(); + } + } + + List &objects = tree_item_owners[p_tree_selected_from->get_selected()]; + if (objects.is_empty()) { + return; + } + if (objects.size() == 1) { + EditorNode::get_singleton()->push_item((Object *)(objects.get(0))); + } + if (objects.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) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + _refresh_icons(); + } break; + } +} + +void SnapshotNodeView::_add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, const String &p_diff_group_name) { + for (const KeyValue &kv : p_snapshot->objects) { + if (kv.value->is_node() && !kv.value->extra_debug_data.has("node_parent")) { + TreeItem *root_item = _add_child_named(p_tree, p_tree->get_root(), kv.value, p_diff_group_name); + _add_object_to_tree(root_item, kv.value, p_diff_group_name); + } + } +} + +void SnapshotNodeView::_add_object_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, const String &p_diff_group_name) { + for (const Variant &v : (Array)p_data->extra_debug_data["node_children"]) { + SnapshotDataObject *child_object = p_data->snapshot->objects[ObjectID((uint64_t)v)]; + TreeItem *child_item = _add_child_named(p_parent_item->get_tree(), p_parent_item, child_object, p_diff_group_name); + _add_object_to_tree(child_item, child_object, p_diff_group_name); + } +} + +TreeItem *SnapshotNodeView::_add_child_named(Tree *p_tree, TreeItem *p_item, SnapshotDataObject *p_item_owner, const String &p_diff_group_name) { + bool has_group = !p_diff_group_name.is_empty(); + const String &item_name = p_item_owner->extra_debug_data["node_name"]; + // Find out if this node already exists. + TreeItem *child_item = nullptr; + if (has_group) { + for (int idx = 0; idx < p_item->get_child_count(); idx++) { + TreeItem *child = p_item->get_child(idx); + if (child->get_text(0) == item_name) { + child_item = child; + break; + } + } + } + + if (child_item) { + // If it exists, clear the background color because we now know it exists in both trees. + child_item->clear_custom_bg_color(0); + } else { + // Add the new node and set it's background color to green or red depending on which snapshot it's a part of. + if (p_item_owner->extra_debug_data["node_is_scene_root"]) { + child_item = p_tree->get_root() ? p_tree->get_root() : p_tree->create_item(); + } else { + child_item = p_tree->create_item(p_item); + } + if (has_group) { + if (p_diff_group_name == "+") { + child_item->set_custom_bg_color(0, Color(0, 1, 0, 0.1)); + } + if (p_diff_group_name == "-") { + child_item->set_custom_bg_color(0, Color(1, 0, 0, 0.1)); + } + } + } + + child_item->set_text(0, item_name); + _add_tree_item_owner(child_item, p_item_owner); + return child_item; +} + +// Each node in the tree may be part of one or two snapshots. This tracks that relationship +// so we can display the correct data in the inspector if a node is clicked. +void SnapshotNodeView::_add_tree_item_owner(TreeItem *p_item, SnapshotDataObject *p_owner) { + if (!tree_item_owners.has(p_item)) { + tree_item_owners.insert(p_item, List()); + } + tree_item_owners[p_item].push_back(p_owner); +} + +void SnapshotNodeView::_refresh_icons() { + for (TreeItem *item : _get_children_recursive(main_tree.tree)) { + item->set_icon(0, EditorNode::get_singleton()->get_class_icon(tree_item_owners[item].get(0)->type_name, "")); + } + if (diff_tree.tree) { + for (TreeItem *item : _get_children_recursive(diff_tree.tree)) { + item->set_icon(0, EditorNode::get_singleton()->get_class_icon(tree_item_owners[item].get(0)->type_name, "")); + } + } +} + +void SnapshotNodeView::clear_snapshot() { + SnapshotView::clear_snapshot(); + + tree_item_owners.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) { + List &objects = tree_item_owners[active_tree->get_selected()]; + EditorNode::get_singleton()->push_item((Object *)objects.get(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(TTR("Snapshot A"), 0); + choose_object_menu->add_item(TTR("Snapshot B"), 1); + choose_object_menu->reset_size(); + choose_object_menu->set_position(get_screen_position() + get_local_mouse_position()); + choose_object_menu->popup(); +} diff --git a/modules/objectdb_profiler/editor/data_viewers/node_view.h b/modules/objectdb_profiler/editor/data_viewers/node_view.h new file mode 100644 index 00000000000..19cfe72ca24 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/node_view.h @@ -0,0 +1,85 @@ +/**************************************************************************/ +/* 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); + +protected: + NodeTreeElements main_tree; + NodeTreeElements diff_tree; + Tree *active_tree = nullptr; + PopupMenu *choose_object_menu = nullptr; + bool combined_diff_view = true; + HashMap> tree_item_owners; + + void _node_selected(Tree *p_tree_selected_from); + void _notification(int p_what); + NodeTreeElements _make_node_tree(const String &p_tree_name, GameStateSnapshot *p_snapshot); + 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(); + + // `_add_snapshot_to_tree`, `_add_object_to_tree`, and `_add_child_named` work together to add items to the node tree. + // They support adding two snapshots to the same tree, and will highlight rows to show additions and removals. + // `_add_snapshot_to_tree` walks the root items in the tree and adds them first, then `_add_object_to_tree` recursively + // adds all the child items. `_add_child_named` is used by both to add each individual items. + void _add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, const String &p_diff_group_name = ""); + void _add_object_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, const String &p_diff_group_name = ""); + TreeItem *_add_child_named(Tree *p_tree, TreeItem *p_item, SnapshotDataObject *p_item_owner, const String &p_diff_group_name = ""); + void _add_tree_item_owner(TreeItem *p_item, SnapshotDataObject *p_owner); + +public: + SnapshotNodeView(); + virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override; + virtual void clear_snapshot() override; +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/object_view.cpp b/modules/objectdb_profiler/editor/data_viewers/object_view.cpp new file mode 100644 index 00000000000..a7ba411f8e4 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/object_view.cpp @@ -0,0 +1,251 @@ +/**************************************************************************/ +/* 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(TTR("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, TTR("Filter Objects"))); + object_column->add_child(filter_bar); + int sort_idx = 0; + if (diff_data) { + filter_bar->add_sort_option(TTR("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++); + } + filter_bar->add_sort_option(TTR("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++); + filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++); + filter_bar->add_sort_option(TTR("Inbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++); + TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option( + TTR("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, TTR("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, TTR("Class")); + object_list->set_column_expand(offset + 0, true); + object_list->set_column_title_tooltip_text(offset + 0, TTR("Object's class")); + object_list->set_column_title(offset + 1, TTR("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, TTR("Object's name")); + object_list->set_column_title(offset + 2, TTR("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, TTR("Number of inbound references")); + object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE); + object_list->set_column_title(offset + 3, TTR("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, TTR("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, TTR("A")); + if (diff_data) { + _insert_data(diff_data, TTR("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 &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); + offset = 1; + } + 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((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, TTR("Inbound References"), TTR("Source"), TTR("Other object referencing this object"), TTR("Property"), TTR("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 &ob : d->inbound_references) { + TreeItem *i = inbound_tree->create_item(ib_root); + 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, TTR("Outbound References"), TTR("Property"), TTR("Property of this object referencing other object"), TTR("Target"), TTR("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 &ob : d->outbound_references) { + TreeItem *i = outbound_tree->create_item(ob_root); + 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; +} diff --git a/modules/objectdb_profiler/editor/data_viewers/object_view.h b/modules/objectdb_profiler/editor/data_viewers/object_view.h new file mode 100644 index 00000000000..0cb1921b60f --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/object_view.h @@ -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 item_data_map; + HashMap data_item_map; + HashMap 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; +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp b/modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp new file mode 100644 index 00000000000..9f063dfab58 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp @@ -0,0 +1,310 @@ +/**************************************************************************/ +/* 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(TTR("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, TTR("Filter RefCounteds"))); + refs_column->add_child(filter_bar); + int offset = diff_data ? 1 : 0; + if (diff_data) { + filter_bar->add_sort_option(TTR("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0); + } + filter_bar->add_sort_option(TTR("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 0); + filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 1); + TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option( + TTR("Native Refs"), + TreeSortAndFilterBar::SortType::NUMERIC_SORT, + offset + 2); + filter_bar->add_sort_option(TTR("ObjectDB Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 3); + filter_bar->add_sort_option(TTR("Total Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 4); + filter_bar->add_sort_option(TTR("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, TTR("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, TTR("Class")); + refs_list->set_column_expand(offset + 0, true); + refs_list->set_column_title_tooltip_text(offset + 0, TTR("Object's class")); + refs_list->set_column_title(offset + 1, TTR("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, TTR("Object's name")); + refs_list->set_column_title(offset + 2, TTR("Native Refs")); + refs_list->set_column_expand(offset + 2, false); + refs_list->set_column_title_tooltip_text(offset + 2, TTR("References not owned by the ObjectDB")); + refs_list->set_column_title(offset + 3, TTR("ObjectDB Refs")); + refs_list->set_column_expand(offset + 3, false); + refs_list->set_column_title_tooltip_text(offset + 3, TTR("References owned by the ObjectDB")); + refs_list->set_column_title(offset + 4, TTR("Total Refs")); + refs_list->set_column_expand(offset + 4, false); + refs_list->set_column_title_tooltip_text(offset + 4, TTR("ObjectDB References + Native References")); + refs_list->set_column_title(offset + 5, TTR("ObjectDB Cycles")); + refs_list->set_column_expand(offset + 5, false); + refs_list->set_column_title_tooltip_text(offset + 5, TTR("Cycles detected in the ObjectDB")); + refs_list->connect("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, TTR("A")); + if (diff_data) { + _insert_data(diff_data, TTR("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 &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); + offset = 1; + } + + 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(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((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 += TTR(" Native References: ") + String::num_uint64(native_refs) + "\n"; + count_str += TTR(" ObjectDB References: ") + String::num_uint64(objectdb_refs) + "\n"; + count_str += TTR(" Total References: ") + String::num_uint64(total_refs) + "\n"; + count_str += TTR(" ObjectDB Cycles: ") + String::num_uint64(ref_cycles.size()) + "\n"; + 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(TTR("[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, TTR("Source")); + inbound_tree->set_column_expand(0, true); + inbound_tree->set_column_clip_content(0, false); + inbound_tree->set_column_title_tooltip_text(0, TTR("Other object referencing this object")); + inbound_tree->set_column_title(1, TTR("Property")); + inbound_tree->set_column_expand(1, true); + inbound_tree->set_column_clip_content(1, true); + inbound_tree->set_column_title_tooltip_text(1, TTR("Property of other object referencing this object")); + inbound_tree->set_column_title(2, TTR("Duplicate?")); + inbound_tree->set_column_expand(2, false); + inbound_tree->set_column_title_tooltip_text(2, TTR("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 property_repeat_count; + for (const KeyValue &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 &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_text(1, ob.key); + i->set_text(2, property_repeat_count[ob.value] > 1 ? TTR("Yes") : TTR("No")); + reference_item_map[i] = data_item_map[target]; + } + } + + if (ref_cycles.size() > 0) { + properties_container->add_child(memnew(SpanningHeader(TTR("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(); + } +} diff --git a/modules/objectdb_profiler/editor/data_viewers/refcounted_view.h b/modules/objectdb_profiler/editor/data_viewers/refcounted_view.h new file mode 100644 index 00000000000..5d110623dc6 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/refcounted_view.h @@ -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 item_data_map; + HashMap data_item_map; + HashMap 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; +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp b/modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp new file mode 100644 index 00000000000..a478ed71d47 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp @@ -0,0 +1,248 @@ +/**************************************************************************/ +/* 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/menu_button.h" +#include "scene/resources/style_box_flat.h" + +SpanningHeader::SpanningHeader(const String &p_text) { + Ref 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 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("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 ourself 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 out children aren't 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 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(); + + List items; + 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(); + } + if (sort.type == ALPHA_SORT && sort.ascending == false) { + items.sort_custom(); + items.reverse(); + } + if (sort.type == NUMERIC_SORT && sort.ascending == true) { + items.sort_custom(); + } + if (sort.type == NUMERIC_SORT && sort.ascending == false) { + items.sort_custom(); + 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_ENTER_TREE: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_TRANSLATION_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(), TTR("Sort By ") + p_new_option + TTR(" (Ascending)"), 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(), TTR("Sort By ") + p_new_option + TTR(" (Descending)"), 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; + } + + OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort"); + _apply_sort(); + OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort"); + OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter"); + _apply_filter(); + OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter"); +} diff --git a/modules/objectdb_profiler/editor/data_viewers/shared_controls.h b/modules/objectdb_profiler/editor/data_viewers/shared_controls.h new file mode 100644 index 00000000000..473724f25eb --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/shared_controls.h @@ -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/line_edit.h" +#include "scene/gui/panel_container.h" +#include "scene/gui/tree.h" + +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 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(); +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp b/modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp new file mode 100644 index 00000000000..5b1453f7423 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp @@ -0,0 +1,70 @@ +/**************************************************************************/ +/* 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/label.h" +#include "scene/gui/rich_text_label.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; +} + +List SnapshotView::_get_children_recursive(Tree *p_tree) { + List found_items; + List 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; +} diff --git a/modules/objectdb_profiler/editor/data_viewers/snapshot_view.h b/modules/objectdb_profiler/editor/data_viewers/snapshot_view.h new file mode 100644 index 00000000000..251a090bb54 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/snapshot_view.h @@ -0,0 +1,54 @@ +/**************************************************************************/ +/* 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; + + List _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); +}; diff --git a/modules/objectdb_profiler/editor/data_viewers/summary_view.cpp b/modules/objectdb_profiler/editor/data_viewers/summary_view.cpp new file mode 100644 index 00000000000..2d48a33fba5 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/summary_view.cpp @@ -0,0 +1,282 @@ +/**************************************************************************/ +/* 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("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 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 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(TTR("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(TTR("Press 'Take ObjectDB Snapshot' to snapshot the ObjectDB."))); + Label *l2 = memnew(Label(TTR("Memory in Godot is either owned natively by the engine or owned by the ObjectDB."))); + Label *l3 = memnew(Label(TTR("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 ? TTR("Snapshot") : TTR("Snapshot A"); + String snapshot_b_name = TTR("Snapshot B"); + + _push_overview_blurb(snapshot_a_name + TTR(" Overview"), snapshot_data); + if (diff_data) { + _push_overview_blurb(snapshot_b_name + TTR(" Overview"), diff_data); + } + + _push_node_blurb(snapshot_a_name + TTR(" Nodes"), snapshot_data); + if (diff_data) { + _push_node_blurb(snapshot_b_name + TTR(" Nodes"), diff_data); + } + + _push_refcounted_blurb(snapshot_a_name + TTR(" RefCounteds"), snapshot_data); + if (diff_data) { + _push_refcounted_blurb(snapshot_b_name + TTR(" RefCounteds"), diff_data); + } + + _push_object_blurb(snapshot_a_name + TTR(" Objects"), snapshot_data); + if (diff_data) { + _push_object_blurb(snapshot_b_name + TTR(" 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_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 += TTR(" [i]Name:[/i] ") + p_snapshot->name + "\n"; + if (p_snapshot->snapshot_context.has("timestamp")) { + c += TTR(" [i]Timestamp:[/i] ") + Time::get_singleton()->get_datetime_string_from_unix_time((double)p_snapshot->snapshot_context["timestamp"]) + "\n"; + } + if (p_snapshot->snapshot_context.has("game_version")) { + c += TTR(" [i]Game Version:[/i] ") + (String)p_snapshot->snapshot_context["game_version"] + "\n"; + } + if (p_snapshot->snapshot_context.has("editor_version")) { + c += TTR(" [i]Editor Version:[/i] ") + (String)p_snapshot->snapshot_context["editor_version"] + "\n"; + } + + double bytes_to_mb = 0.000001; + if (p_snapshot->snapshot_context.has("mem_usage")) { + c += TTR(" [i]Memory Used:[/i] ") + String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_usage"]) * bytes_to_mb, 3) + " MB\n"; + } + if (p_snapshot->snapshot_context.has("mem_max_usage")) { + c += TTR(" [i]Max Memory Used:[/i] ") + String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_max_usage"]) * bytes_to_mb, 3) + " MB\n"; + } + if (p_snapshot->snapshot_context.has("mem_available")) { + // I'm guessing pretty hard about what this is supposed to be. It's hard coded to be -1 cast to a uint64_t in Memory.h, + // so it _could_ be checking if we're on a 64 bit system, I think... + c += TTR(" [i]Max uint64 value:[/i] ") + String::num_uint64((uint64_t)p_snapshot->snapshot_context["mem_available"]) + "\n"; + } + c += TTR(" [i]Total Objects:[/i] ") + itos(p_snapshot->objects.size()) + "\n"; + + int node_count = 0; + for (const KeyValue &pair : p_snapshot->objects) { + if (pair.value->is_node()) { + node_count++; + } + } + c += TTR(" [i]Total Nodes:[/i] ") + itos(node_count) + "\n"; + c += "[/ul]\n"; + + blurb_list->add_child(memnew(SummaryBlurb(p_title, c))); +} + +void SnapshotSummaryView::_push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot) { + List nodes; + for (const KeyValue &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"]) { + const 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 = TTR("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) { + List rcs; + for (const KeyValue &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 = TTR("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) { + List objects; + for (const KeyValue &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 = TTR("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))); +} diff --git a/modules/objectdb_profiler/editor/data_viewers/summary_view.h b/modules/objectdb_profiler/editor/data_viewers/summary_view.h new file mode 100644 index 00000000000..2e6431cda77 --- /dev/null +++ b/modules/objectdb_profiler/editor/data_viewers/summary_view.h @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* 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 "scene/gui/margin_container.h" +#include "snapshot_view.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; +}; diff --git a/modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp b/modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp new file mode 100644 index 00000000000..a44e6c58f49 --- /dev/null +++ b/modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp @@ -0,0 +1,445 @@ +/**************************************************************************/ +/* 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 "core/config/project_settings.h" +#include "core/os/memory.h" +#include "core/os/time.h" +#include "data_viewers/class_view.h" +#include "data_viewers/json_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 "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(TTR("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; + args.push_back(next_request_id++); + args.push_back(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.get(0); + int total_size = p_data.get(1); + partial_snapshots[request_id] = PartialSnapshot(); + partial_snapshots[request_id].total_size = total_size; + Array args; + args.push_back(request_id); + args.push_back(0); + args.push_back(SNAPSHOT_CHUNK_SIZE); + take_snapshot->set_text(TTR("Receiving Snapshot") + " (0/" + _to_mb(total_size) + " mb)"); + 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.get(0); + PartialSnapshot &chunk = partial_snapshots[request_id]; + chunk.data.append_array(p_data.get(1)); + take_snapshot->set_text(TTR("Receiving Snapshot") + " (" + _to_mb(chunk.data.size()) + "/" + _to_mb(chunk.total_size) + " mb)"); + if (chunk.data.size() != chunk.total_size) { + Array args; + args.push_back(request_id); + args.push_back(chunk.data.size()); + args.push_back(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(TTR("Visualizing Snapshot")); + // Wait a frame just so the button has a chance to update it's 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 &in_data = partial_snapshots[request_id].data; + String snapshot_file_name = Time::get_singleton()->get_datetime_string_from_system(false).replace("T", "_").replace(":", "-"); + Ref 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 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 ObjectDBProfilerPanel::_get_and_create_snapshot_storage_dir() { + String profiles_dir = "user://"; + Ref 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->move_before(snapshot_list->get_root()->get_first_child()); + _update_diff_items(); + return item; +} + +void ObjectDBProfilerPanel::_show_selected_snapshot() { + if (snapshot_list->get_selected()->get_text(0) == diff_options[diff_button->get_selected_id()]) { + 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_options[diff_button->get_selected_id()]); + _update_enabled_diff_items(); +} + +Ref 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); + } else { + Ref 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 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 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 snapshot = GameStateSnapshot::create_ref(p_snapshot_file_name, content); + if (snapshot.is_valid()) { + // Don't cache a null snapshot. + 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(); + + current_snapshot = get_snapshot(p_snapshot_file_name); + if (p_snapshot_diff_file_name != "none") { + diff_snapshot = get_snapshot(p_snapshot_diff_file_name); + } else { + diff_snapshot.unref(); + } + + _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 while if we try to populate every tab at once. + SnapshotView *view = cast_to(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() { + for (SnapshotView *view : views) { + view->clear_snapshot(); + } + current_snapshot.unref(); +} + +void ObjectDBProfilerPanel::set_enabled(bool p_enabled) { + take_snapshot->set_text(TTR("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")), TTR("Rename Snapshot"), OdbProfilerMenuOptions::ODB_MENU_RENAME); + rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR("Show in Folder"), OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER); + rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Snapshot"), OdbProfilerMenuOptions::ODB_MENU_DELETE); + + rmb_menu->reset_size(); + rmb_menu->set_position(get_screen_position() + p_pos); + 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. + view_tabs->set_current_tab(0); + clear_snapshot(); + } + _update_diff_items(); + break; + } + case OdbProfilerMenuOptions::ODB_MENU_RENAME: { + snapshot_list->edit_selected(true); + break; + } + } +} + +void ObjectDBProfilerPanel::_edit_snapshot_name() { + const String &new_snapshot_name = snapshot_list->get_selected()->get_text(0); + const String &full_file_with_path = snapshot_list->get_selected()->get_metadata(0); + Vector full_path_parts = full_file_with_path.rsplit("/", false, 1); + const String &full_file_path = full_path_parts.get(0); + const String &file_name = full_path_parts.get(1); + const String &old_snapshot_name = file_name.split(".").get(0); + const 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(":") || new_snapshot_name.contains("\\") || new_snapshot_name.contains("/") || new_snapshot_name.begins_with(".") || new_snapshot_name.is_empty()) { + EditorNode::get_singleton()->show_warning(TTR("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(TTR("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(TTR("ObjectDB Profiler")); + + snapshot_cache = LRUCache>(SNAPSHOT_CACHE_MAX_SIZE); + + EditorDebuggerNode::get_singleton()->get_current_debugger()->connect(SNAME("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); + + // snapshot button + take_snapshot = memnew(Button(TTR("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("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(SNAME("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(TTR("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->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_apply_diff)); + 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)); + add_view(memnew(SnapshotJsonView)); + + set_enabled(false); + + // Load all the snapshot names from disk. + Ref snapshot_dir = _get_and_create_snapshot_storage_dir(); + if (snapshot_dir.is_valid()) { + for (const String &file_name : snapshot_dir->get_files()) { + Vector name_parts = file_name.split("."); + if (name_parts.size() != 2 || name_parts[1] != "odb_snapshot") { + ERR_PRINT("ObjectDB Snapshot file did not have .odb_snapshot extension. Skipping: " + file_name); + continue; + } + } + } +} + +void ObjectDBProfilerPanel::add_view(SnapshotView *p_to_add) { + views.push_back(p_to_add); + view_tabs->add_child(p_to_add); +} + +void ObjectDBProfilerPanel::_update_diff_items() { + diff_button->clear(); + diff_button->add_item("none", 0); + diff_options[0] = "none"; + + for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) { + const String &name = snapshot_list->get_root()->get_child(i)->get_text(0); + diff_button->add_item(name); + diff_options[i + 1] = name; // 0 = none, so i + 1. + } +} + +void ObjectDBProfilerPanel::_update_enabled_diff_items() { + const String &sn_name = snapshot_list->get_selected()->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) == sn_name); + } +} + +void ObjectDBProfilerPanel::_apply_diff(int p_item_idx) { + _show_selected_snapshot(); +} + +String ObjectDBProfilerPanel::_to_mb(int p_x) { + return String::num((double)p_x / (double)(1 << 20), 2); +} diff --git a/modules/objectdb_profiler/editor/objectdb_profiler_panel.h b/modules/objectdb_profiler/editor/objectdb_profiler_panel.h new file mode 100644 index 00000000000..b0656921ffe --- /dev/null +++ b/modules/objectdb_profiler/editor/objectdb_profiler_panel.h @@ -0,0 +1,102 @@ +/**************************************************************************/ +/* 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 "core/io/dir_access.h" +#include "core/templates/lru.h" +#include "data_viewers/snapshot_view.h" +#include "snapshot_data.h" + +class TabContainer; +class Tree; + +const int SNAPSHOT_CACHE_MAX_SIZE = 10; + +// UI loaded by the debugger. +class ObjectDBProfilerPanel : public Control { + GDCLASS(ObjectDBProfilerPanel, Control); + +protected: + enum OdbProfilerMenuOptions { + ODB_MENU_RENAME, + ODB_MENU_SHOW_IN_FOLDER, + ODB_MENU_DELETE, + }; + + struct PartialSnapshot { + int total_size; + Vector 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 diff_options; + HashMap partial_snapshots; + + List views; + Ref current_snapshot; + Ref diff_snapshot; + LRUCache> 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(); + Ref _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 _apply_diff(int p_item_idx); + 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(); + Ref 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); +}; diff --git a/modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp b/modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp new file mode 100644 index 00000000000..efc00f04610 --- /dev/null +++ b/modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp @@ -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 session = get_session(p_session_id); + ERR_FAIL_COND(session.is_null()); + debugger_panel = memnew(ObjectDBProfilerPanel); + session->connect(SNAME("started"), callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(true)); + session->connect(SNAME("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); + } + } +} diff --git a/modules/objectdb_profiler/editor/objectdb_profiler_plugin.h b/modules/objectdb_profiler/editor/objectdb_profiler_plugin.h new file mode 100644 index 00000000000..b6b09043494 --- /dev/null +++ b/modules/objectdb_profiler/editor/objectdb_profiler_plugin.h @@ -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/plugins/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 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; +}; diff --git a/modules/objectdb_profiler/editor/snapshot_data.cpp b/modules/objectdb_profiler/editor/snapshot_data.cpp new file mode 100644 index 00000000000..2c5347fee27 --- /dev/null +++ b/modules/objectdb_profiler/editor/snapshot_data.cpp @@ -0,0 +1,388 @@ +/**************************************************************************/ +/* 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/version.h" +#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED) +#include "modules/gdscript/gdscript.h" +#else +#include "core/object/script_language.h" +#endif +#include "scene/debugger/scene_debugger.h" +#include "zlib.h" + +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; + // pinfo.name = pinfo.name.trim_prefix("Node/").trim_prefix("Members/"); + + 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.contains("::")) { + // 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()); + Ref