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