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