Path: blob/master/modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp
20898 views
/**************************************************************************/1/* objectdb_profiler_panel.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "objectdb_profiler_panel.h"3132#include "../snapshot_collector.h"33#include "data_viewers/class_view.h"34#include "data_viewers/node_view.h"35#include "data_viewers/object_view.h"36#include "data_viewers/refcounted_view.h"37#include "data_viewers/summary_view.h"3839#include "core/config/project_settings.h"40#include "core/os/time.h"41#include "editor/debugger/editor_debugger_node.h"42#include "editor/debugger/script_editor_debugger.h"43#include "editor/docks/inspector_dock.h"44#include "editor/editor_node.h"45#include "editor/inspector/editor_inspector.h"46#include "editor/themes/editor_scale.h"47#include "scene/gui/button.h"48#include "scene/gui/label.h"49#include "scene/gui/option_button.h"50#include "scene/gui/split_container.h"51#include "scene/gui/tab_container.h"5253// ObjectDB snapshots are very large. In remote_debugger_peer.cpp, the max in_buf and out_buf size is 8mb.54// Snapshots are typically larger than that, so we send them 6mb at a time. Leaving 2mb for other data.55const int SNAPSHOT_CHUNK_SIZE = 6 << 20;5657void ObjectDBProfilerPanel::_request_object_snapshot() {58take_snapshot->set_disabled(true);59take_snapshot->set_text(TTRC("Generating Snapshot"));60// Pause the game while the snapshot is taken so the state of the game isn't modified as we capture the snapshot.61if (EditorDebuggerNode::get_singleton()->get_current_debugger()->is_breaked()) {62requested_break_for_snapshot = false;63_begin_object_snapshot();64} else {65awaiting_debug_break = true;66requested_break_for_snapshot = true; // We only need to resume the game if we are the ones who paused it.67EditorDebuggerNode::get_singleton()->debug_break();68}69}7071void ObjectDBProfilerPanel::_on_debug_breaked(bool p_reallydid, bool p_can_debug, const String &p_reason, bool p_has_stackdump) {72if (p_reallydid && awaiting_debug_break) {73awaiting_debug_break = false;74_begin_object_snapshot();75}76}7778void ObjectDBProfilerPanel::_begin_object_snapshot() {79Array args = { next_request_id++, SnapshotCollector::get_godot_version_string() };80EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_prepare_snapshot", args);81}8283bool ObjectDBProfilerPanel::handle_debug_message(const String &p_message, const Array &p_data, int p_index) {84if (p_message == "snapshot:snapshot_prepared") {85int request_id = p_data[0];86int total_size = p_data[1];87partial_snapshots[request_id] = PartialSnapshot();88partial_snapshots[request_id].total_size = total_size;89Array args = { request_id, 0, SNAPSHOT_CHUNK_SIZE };90take_snapshot->set_text(vformat(TTR("Receiving Snapshot (0/%s MiB)"), _to_mb(total_size)));91EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);92return true;93}94if (p_message == "snapshot:snapshot_chunk") {95int request_id = p_data[0];96PartialSnapshot &chunk = partial_snapshots[request_id];97chunk.data.append_array(p_data[1]);98take_snapshot->set_text(vformat(TTR("Receiving Snapshot (%s/%s MiB)"), _to_mb(chunk.data.size()), _to_mb(chunk.total_size)));99if (chunk.data.size() != chunk.total_size) {100Array args = { request_id, chunk.data.size(), chunk.data.size() + SNAPSHOT_CHUNK_SIZE };101EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);102return true;103}104105take_snapshot->set_text(TTRC("Visualizing Snapshot"));106// Wait a frame just so the button has a chance to update its text so the user knows what's going on.107get_tree()->connect("process_frame", callable_mp(this, &ObjectDBProfilerPanel::receive_snapshot).bind(request_id), CONNECT_ONE_SHOT);108return true;109}110return false;111}112113void ObjectDBProfilerPanel::receive_snapshot(int request_id) {114const Vector<uint8_t> &in_data = partial_snapshots[request_id].data;115Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();116if (snapshot_dir.is_valid()) {117Error err;118String base_snapshot_file_name = Time::get_singleton()->get_datetime_string_from_system(false).replace_char('T', '_').replace_char(':', '-');119String snapshot_file_name = base_snapshot_file_name;120String current_dir = snapshot_dir->get_current_dir();121String joined_dir = current_dir.path_join(snapshot_file_name) + ".odb_snapshot";122123for (int i = 2; FileAccess::exists(joined_dir); i++) {124snapshot_file_name = base_snapshot_file_name + '_' + String::chr('0' + i);125joined_dir = current_dir.path_join(snapshot_file_name) + ".odb_snapshot";126}127128Ref<FileAccess> file = FileAccess::open(joined_dir, FileAccess::WRITE, &err);129if (err == OK) {130file->store_buffer(in_data);131file->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.132133_add_snapshot_button(snapshot_file_name, joined_dir);134snapshot_list->deselect_all();135snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());136snapshot_list->ensure_cursor_is_visible();137_show_selected_snapshot();138} else {139ERR_PRINT("Could not persist ObjectDB Snapshot: " + String(error_names[err]));140}141}142partial_snapshots.erase(request_id);143if (requested_break_for_snapshot) {144EditorDebuggerNode::get_singleton()->debug_continue();145}146take_snapshot->set_disabled(false);147take_snapshot->set_text("Take ObjectDB Snapshot");148}149150Ref<DirAccess> ObjectDBProfilerPanel::_get_and_create_snapshot_storage_dir() {151String profiles_dir = "user://";152Ref<DirAccess> da = DirAccess::open(profiles_dir);153ERR_FAIL_COND_V_MSG(da.is_null(), nullptr, vformat("Could not open 'user://' directory: '%s'.", profiles_dir));154Error err = da->change_dir("objectdb_snapshots");155if (err != OK) {156Error err_mk = da->make_dir("objectdb_snapshots");157Error err_ch = da->change_dir("objectdb_snapshots");158ERR_FAIL_COND_V_MSG(err_mk != OK || err_ch != OK, nullptr, "Could not create ObjectDB Snapshots directory: " + da->get_current_dir());159}160return da;161}162163TreeItem *ObjectDBProfilerPanel::_add_snapshot_button(const String &p_snapshot_file_name, const String &p_full_file_path) {164TreeItem *item = snapshot_list->create_item(snapshot_list->get_root());165item->set_text(0, p_snapshot_file_name);166item->set_metadata(0, p_full_file_path);167item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);168item->move_before(snapshot_list->get_root()->get_first_child());169_update_diff_items();170_update_enabled_diff_items();171return item;172}173174void ObjectDBProfilerPanel::_show_selected_snapshot() {175if (snapshot_list->get_selected()->get_text(0) == (String)diff_button->get_selected_metadata()) {176for (int i = 0; i < diff_button->get_item_count(); i++) {177if (diff_button->get_item_text(i) == current_snapshot->name) {178diff_button->select(i);179break;180}181}182}183show_snapshot(snapshot_list->get_selected()->get_text(0), diff_button->get_selected_metadata());184_update_enabled_diff_items();185}186187void ObjectDBProfilerPanel::_on_snapshot_deselected() {188snapshot_list->deselect_all();189diff_button->select(0);190clear_snapshot();191_update_enabled_diff_items();192}193194Ref<GameStateSnapshot> ObjectDBProfilerPanel::get_snapshot(const String &p_snapshot_file_name) {195if (snapshot_cache.has(p_snapshot_file_name)) {196return snapshot_cache.get(p_snapshot_file_name);197}198199Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();200ERR_FAIL_COND_V_MSG(snapshot_dir.is_null(), nullptr, "Could not access ObjectDB Snapshot directory");201202String full_file_path = snapshot_dir->get_current_dir().path_join(p_snapshot_file_name) + ".odb_snapshot";203204Error err;205Ref<FileAccess> snapshot_file = FileAccess::open(full_file_path, FileAccess::READ, &err);206ERR_FAIL_COND_V_MSG(err != OK, nullptr, "Could not open ObjectDB Snapshot file: " + full_file_path);207208Vector<uint8_t> content = snapshot_file->get_buffer(snapshot_file->get_length()); // We want to split on newlines, so normalize them.209ERR_FAIL_COND_V_MSG(content.is_empty(), nullptr, "ObjectDB Snapshot file is empty: " + full_file_path);210211Ref<GameStateSnapshot> snapshot = GameStateSnapshot::create_ref(p_snapshot_file_name, content);212if (snapshot.is_valid()) {213snapshot_cache.insert(p_snapshot_file_name, snapshot);214}215216return snapshot;217}218219void ObjectDBProfilerPanel::show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name) {220clear_snapshot(false);221222current_snapshot = get_snapshot(p_snapshot_file_name);223if (!p_snapshot_diff_file_name.is_empty()) {224diff_snapshot = get_snapshot(p_snapshot_diff_file_name);225}226227_update_view_tabs();228_view_tab_changed(view_tabs->get_current_tab());229}230231void ObjectDBProfilerPanel::_view_tab_changed(int p_tab_idx) {232// Populating tabs only on tab changed because we're handling a lot of data,233// and the editor freezes for a while if we try to populate every tab at once.234SnapshotView *view = cast_to<SnapshotView>(view_tabs->get_current_tab_control());235GameStateSnapshot *snapshot = current_snapshot.ptr();236GameStateSnapshot *diff = diff_snapshot.ptr();237if (snapshot != nullptr && !view->is_showing_snapshot(snapshot, diff)) {238view->show_snapshot(snapshot, diff);239}240}241242void ObjectDBProfilerPanel::clear_snapshot(bool p_update_view_tabs) {243for (SnapshotView *view : views) {244view->clear_snapshot();245}246247const Object *edited_object = InspectorDock::get_inspector_singleton()->get_edited_object();248if (Object::cast_to<SnapshotDataObject>(edited_object)) {249EditorNode::get_singleton()->push_item(nullptr);250}251252current_snapshot.unref();253diff_snapshot.unref();254255if (p_update_view_tabs) {256_update_view_tabs();257}258}259260void ObjectDBProfilerPanel::set_enabled(bool p_enabled) {261take_snapshot->set_text(TTRC("Take ObjectDB Snapshot"));262take_snapshot->set_disabled(!p_enabled);263}264265void ObjectDBProfilerPanel::_snapshot_rmb(const Vector2 &p_pos, MouseButton p_button) {266if (p_button != MouseButton::RIGHT) {267return;268}269rmb_menu->clear(false);270271rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Rename")), TTRC("Rename"), OdbProfilerMenuOptions::ODB_MENU_RENAME);272rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("Show in File Manager"), OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER);273rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTRC("Delete"), OdbProfilerMenuOptions::ODB_MENU_DELETE);274275rmb_menu->set_position(snapshot_list->get_screen_position() + p_pos);276rmb_menu->reset_size();277rmb_menu->popup();278}279280void ObjectDBProfilerPanel::_rmb_menu_pressed(int p_tool, bool p_confirm_override) {281String file_path = snapshot_list->get_selected()->get_metadata(0);282String global_path = ProjectSettings::get_singleton()->globalize_path(file_path);283switch (rmb_menu->get_item_id(p_tool)) {284case OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER: {285OS::get_singleton()->shell_show_in_file_manager(global_path, true);286break;287}288case OdbProfilerMenuOptions::ODB_MENU_DELETE: {289DirAccess::remove_file_or_error(global_path);290snapshot_list->get_root()->remove_child(snapshot_list->get_selected());291if (snapshot_list->get_root()->get_child_count() > 0) {292snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());293} else {294// If we deleted the last snapshot, jump back to the summary tab and clear everything out.295clear_snapshot();296}297_update_diff_items();298break;299}300case OdbProfilerMenuOptions::ODB_MENU_RENAME: {301snapshot_list->edit_selected(true);302break;303}304}305}306307void ObjectDBProfilerPanel::_edit_snapshot_name() {308String new_snapshot_name = snapshot_list->get_selected()->get_text(0);309String full_file_with_path = snapshot_list->get_selected()->get_metadata(0);310Vector<String> full_path_parts = full_file_with_path.rsplit("/", false, 1);311String full_file_path = full_path_parts[0];312String file_name = full_path_parts[1];313String old_snapshot_name = file_name.split(".")[0];314String new_full_file_path = full_file_path.path_join(new_snapshot_name) + ".odb_snapshot";315316bool name_taken = false;317for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {318TreeItem *item = snapshot_list->get_root()->get_child(i);319if (item != snapshot_list->get_selected()) {320if (item->get_text(0) == new_snapshot_name) {321name_taken = true;322break;323}324}325}326327if (name_taken || new_snapshot_name.contains_char(':') || new_snapshot_name.contains_char('\\') || new_snapshot_name.contains_char('/') || new_snapshot_name.begins_with(".") || new_snapshot_name.is_empty()) {328EditorNode::get_singleton()->show_warning(TTRC("Invalid snapshot name."));329snapshot_list->get_selected()->set_text(0, old_snapshot_name);330return;331}332333Error err = DirAccess::rename_absolute(full_file_with_path, new_full_file_path);334if (err != OK) {335EditorNode::get_singleton()->show_warning(TTRC("Snapshot rename failed"));336snapshot_list->get_selected()->set_text(0, old_snapshot_name);337} else {338snapshot_list->get_selected()->set_metadata(0, new_full_file_path);339}340341_update_diff_items();342_show_selected_snapshot();343}344345ObjectDBProfilerPanel::ObjectDBProfilerPanel() {346set_name(TTRC("ObjectDB Profiler"));347348snapshot_cache = LRUCache<String, Ref<GameStateSnapshot>>(SNAPSHOT_CACHE_MAX_SIZE);349350EditorDebuggerNode::get_singleton()->get_current_debugger()->connect("breaked", callable_mp(this, &ObjectDBProfilerPanel::_on_debug_breaked));351352HSplitContainer *root_container = memnew(HSplitContainer);353root_container->set_anchors_preset(Control::LayoutPreset::PRESET_FULL_RECT);354root_container->set_v_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);355root_container->set_h_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);356root_container->set_split_offset(300 * EDSCALE);357add_child(root_container);358359VBoxContainer *snapshot_column = memnew(VBoxContainer);360root_container->add_child(snapshot_column);361362take_snapshot = memnew(Button(TTRC("Take ObjectDB Snapshot")));363snapshot_column->add_child(take_snapshot);364take_snapshot->connect(SceneStringName(pressed), callable_mp(this, &ObjectDBProfilerPanel::_request_object_snapshot));365366snapshot_list = memnew(Tree);367snapshot_list->create_item();368snapshot_list->set_hide_folding(true);369snapshot_column->add_child(snapshot_list);370snapshot_list->set_select_mode(Tree::SelectMode::SELECT_ROW);371snapshot_list->set_hide_root(true);372snapshot_list->set_columns(1);373snapshot_list->set_column_titles_visible(true);374snapshot_list->set_column_title(0, "Snapshots");375snapshot_list->set_column_expand(0, true);376snapshot_list->set_column_clip_content(0, true);377snapshot_list->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot));378snapshot_list->connect("nothing_selected", callable_mp(this, &ObjectDBProfilerPanel::_on_snapshot_deselected));379snapshot_list->connect("item_edited", callable_mp(this, &ObjectDBProfilerPanel::_edit_snapshot_name));380snapshot_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);381snapshot_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);382snapshot_list->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);383snapshot_list->set_theme_type_variation("TreeSecondary");384385snapshot_list->set_allow_rmb_select(true);386snapshot_list->connect("item_mouse_selected", callable_mp(this, &ObjectDBProfilerPanel::_snapshot_rmb));387388rmb_menu = memnew(PopupMenu);389add_child(rmb_menu);390rmb_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ObjectDBProfilerPanel::_rmb_menu_pressed).bind(false));391392HBoxContainer *diff_button_and_label = memnew(HBoxContainer);393diff_button_and_label->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);394snapshot_column->add_child(diff_button_and_label);395Label *diff_against = memnew(Label(TTRC("Diff Against:")));396diff_button_and_label->add_child(diff_against);397398diff_button = memnew(OptionButton);399diff_button->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);400diff_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);401diff_button->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot).unbind(1));402diff_button_and_label->add_child(diff_button);403404// Tabs of various views right for each snapshot.405view_tabs = memnew(TabContainer);406view_tabs->set_theme_type_variation("TabContainerInner");407root_container->add_child(view_tabs);408view_tabs->set_custom_minimum_size(Size2(300 * EDSCALE, 0));409view_tabs->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);410view_tabs->connect("tab_changed", callable_mp(this, &ObjectDBProfilerPanel::_view_tab_changed));411412add_view(memnew(SnapshotSummaryView));413add_view(memnew(SnapshotClassView));414add_view(memnew(SnapshotObjectView));415add_view(memnew(SnapshotNodeView));416add_view(memnew(SnapshotRefCountedView));417418set_enabled(false);419420// Load all the snapshot names from disk.421Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();422if (snapshot_dir.is_valid()) {423for (const String &file_name : snapshot_dir->get_files()) {424Vector<String> name_parts = file_name.split(".");425ERR_CONTINUE_MSG(name_parts.size() != 2 || name_parts[1] != "odb_snapshot", "ObjectDB snapshot file did not have .odb_snapshot extension. Skipping: " + file_name);426_add_snapshot_button(name_parts[0], snapshot_dir->get_current_dir().path_join(file_name));427}428}429}430431void ObjectDBProfilerPanel::add_view(SnapshotView *p_to_add) {432views.push_back(p_to_add);433view_tabs->add_child(p_to_add);434_update_view_tabs();435}436437void ObjectDBProfilerPanel::_update_view_tabs() {438bool has_snapshot = current_snapshot.is_valid();439for (int i = 1; i < view_tabs->get_tab_count(); i++) {440view_tabs->set_tab_disabled(i, !has_snapshot);441}442443if (!has_snapshot) {444view_tabs->set_current_tab(0);445}446}447448void ObjectDBProfilerPanel::_update_diff_items() {449diff_button->clear();450diff_button->add_item(TTRC("None"), 0);451diff_button->set_item_metadata(0, String());452diff_button->set_item_auto_translate_mode(0, Node::AUTO_TRANSLATE_MODE_ALWAYS);453454for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {455String name = snapshot_list->get_root()->get_child(i)->get_text(0);456diff_button->add_item(name);457diff_button->set_item_metadata(i + 1, name);458}459}460461void ObjectDBProfilerPanel::_update_enabled_diff_items() {462TreeItem *selected_snapshot = snapshot_list->get_selected();463if (selected_snapshot == nullptr) {464diff_button->set_disabled(true);465return;466}467468diff_button->set_disabled(false);469470String snapshot_name = selected_snapshot->get_text(0);471for (int i = 0; i < diff_button->get_item_count(); i++) {472diff_button->set_item_disabled(i, diff_button->get_item_text(i) == snapshot_name);473}474}475476String ObjectDBProfilerPanel::_to_mb(int p_x) {477return String::num((double)p_x / (double)(1 << 20), 2);478}479480481