Browse Source

Merge pull request #97210 from AleksLitynski/object-snapshot-debugger

Add an ObjectDB Profiling Tool
Thaddeus Crews 1 week ago
parent
commit
f6aa5ba23c
34 changed files with 3885 additions and 24 deletions
  1. 5 2
      core/object/object.cpp
  2. 2 2
      core/object/object.h
  3. 15 0
      doc/classes/Tree.xml
  4. 20 0
      modules/objectdb_profiler/SCsub
  5. 6 0
      modules/objectdb_profiler/config.py
  6. 282 0
      modules/objectdb_profiler/editor/data_viewers/class_view.cpp
  7. 74 0
      modules/objectdb_profiler/editor/data_viewers/class_view.h
  8. 275 0
      modules/objectdb_profiler/editor/data_viewers/node_view.cpp
  9. 86 0
      modules/objectdb_profiler/editor/data_viewers/node_view.h
  10. 260 0
      modules/objectdb_profiler/editor/data_viewers/object_view.cpp
  11. 63 0
      modules/objectdb_profiler/editor/data_viewers/object_view.h
  12. 323 0
      modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp
  13. 61 0
      modules/objectdb_profiler/editor/data_viewers/refcounted_view.h
  14. 241 0
      modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp
  15. 127 0
      modules/objectdb_profiler/editor/data_viewers/shared_controls.h
  16. 68 0
      modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp
  17. 55 0
      modules/objectdb_profiler/editor/data_viewers/snapshot_view.h
  18. 284 0
      modules/objectdb_profiler/editor/data_viewers/summary_view.cpp
  19. 67 0
      modules/objectdb_profiler/editor/data_viewers/summary_view.h
  20. 464 0
      modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp
  21. 103 0
      modules/objectdb_profiler/editor/objectdb_profiler_panel.h
  22. 66 0
      modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp
  23. 65 0
      modules/objectdb_profiler/editor/objectdb_profiler_plugin.h
  24. 374 0
      modules/objectdb_profiler/editor/snapshot_data.cpp
  25. 111 0
      modules/objectdb_profiler/editor/snapshot_data.h
  26. 54 0
      modules/objectdb_profiler/register_types.cpp
  27. 36 0
      modules/objectdb_profiler/register_types.h
  28. 175 0
      modules/objectdb_profiler/snapshot_collector.cpp
  29. 53 0
      modules/objectdb_profiler/snapshot_collector.h
  30. 22 17
      scene/debugger/scene_debugger.cpp
  31. 2 0
      scene/debugger/scene_debugger.h
  32. 39 0
      scene/gui/tree.cpp
  33. 4 0
      scene/gui/tree.h
  34. 3 3
      scene/main/node.cpp

+ 5 - 2
core/object/object.cpp

@@ -2374,12 +2374,12 @@ void postinitialize_handler(Object *p_object) {
 	p_object->_postinitialize();
 }
 
-void ObjectDB::debug_objects(DebugFunc p_func) {
+void ObjectDB::debug_objects(DebugFunc p_func, void *p_user_data) {
 	spin_lock.lock();
 
 	for (uint32_t i = 0, count = slot_count; i < slot_max && count != 0; i++) {
 		if (object_slots[i].validator) {
-			p_func(object_slots[i].object);
+			p_func(object_slots[i].object, p_user_data);
 			count--;
 		}
 	}
@@ -2547,6 +2547,9 @@ void ObjectDB::cleanup() {
 					if (obj->is_class("Resource")) {
 						extra_info = " - Resource path: " + String(resource_get_path->call(obj, nullptr, 0, call_error));
 					}
+					if (obj->is_class("RefCounted")) {
+						extra_info = " - Reference count: " + itos((static_cast<RefCounted *>(obj))->get_reference_count());
+					}
 
 					uint64_t id = uint64_t(i) | (uint64_t(object_slots[i].validator) << OBJECTDB_SLOT_MAX_COUNT_BITS) | (object_slots[i].is_ref_counted ? OBJECTDB_REFERENCE_BIT : 0);
 					DEV_ASSERT(id == (uint64_t)obj->get_instance_id()); // We could just use the id from the object, but this check may help catching memory corruption catastrophes.

+ 2 - 2
core/object/object.h

@@ -1094,7 +1094,7 @@ class ObjectDB {
 	static void setup();
 
 public:
-	typedef void (*DebugFunc)(Object *p_obj);
+	typedef void (*DebugFunc)(Object *p_obj, void *p_user_data);
 
 	_ALWAYS_INLINE_ static Object *get_instance(ObjectID p_instance_id) {
 		uint64_t id = p_instance_id;
@@ -1126,6 +1126,6 @@ public:
 	template <typename T>
 	_ALWAYS_INLINE_ static Ref<T> get_ref(ObjectID p_instance_id); // Defined in ref_counted.h
 
-	static void debug_objects(DebugFunc p_func);
+	static void debug_objects(DebugFunc p_func, void *p_user_data);
 	static int get_object_count();
 };

+ 15 - 0
doc/classes/Tree.xml

@@ -124,6 +124,13 @@
 				Returns column title language code.
 			</description>
 		</method>
+		<method name="get_column_title_tooltip_text" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="column" type="int" />
+			<description>
+				Returns the column title's tooltip text.
+			</description>
+		</method>
 		<method name="get_column_width" qualifiers="const">
 			<return type="int" />
 			<param index="0" name="column" type="int" />
@@ -322,6 +329,14 @@
 				Sets language code of column title used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
 			</description>
 		</method>
+		<method name="set_column_title_tooltip_text">
+			<return type="void" />
+			<param index="0" name="column" type="int" />
+			<param index="1" name="tooltip_text" type="String" />
+			<description>
+				Sets the column title's tooltip text.
+			</description>
+		</method>
 		<method name="set_selected">
 			<return type="void" />
 			<param index="0" name="item" type="TreeItem" />

+ 20 - 0
modules/objectdb_profiler/SCsub

@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+from misc.utility.scons_hints import *
+
+Import("env")
+Import("env_modules")
+
+env_objdb = env_modules.Clone()
+
+module_obj = []
+
+# Only include in editor and debug builds.
+if env_objdb.debug_features:
+    env_objdb.add_source_files(module_obj, "*.cpp")
+
+    # Only the editor needs these files, don't include them in the game.
+    if env.editor_build:
+        env_objdb.add_source_files(module_obj, "editor/*.cpp")
+        env_objdb.add_source_files(module_obj, "editor/data_viewers/*.cpp")
+
+env.modules_sources += module_obj

+ 6 - 0
modules/objectdb_profiler/config.py

@@ -0,0 +1,6 @@
+def can_build(env, platform):
+    return env.debug_features
+
+
+def configure(env):
+    pass

+ 282 - 0
modules/objectdb_profiler/editor/data_viewers/class_view.cpp

@@ -0,0 +1,282 @@
+/**************************************************************************/
+/*  class_view.cpp                                                        */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "class_view.h"
+
+#include "shared_controls.h"
+
+#include "editor/editor_node.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/split_container.h"
+
+int ClassData::instance_count(GameStateSnapshot *p_snapshot) {
+	int count = 0;
+	for (const SnapshotDataObject *instance : instances) {
+		if (!p_snapshot || instance->snapshot == p_snapshot) {
+			count += 1;
+		}
+	}
+	return count;
+}
+
+int ClassData::get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot) {
+	if (!recursive_instance_count_cache.has(p_snapshot)) {
+		recursive_instance_count_cache[p_snapshot] = instance_count(p_snapshot);
+		for (const String &child : child_classes) {
+			recursive_instance_count_cache[p_snapshot] += p_all_classes[child].get_recursive_instance_count(p_all_classes, p_snapshot);
+		}
+	}
+	return recursive_instance_count_cache[p_snapshot];
+}
+
+SnapshotClassView::SnapshotClassView() {
+	set_name(TTRC("Classes"));
+}
+
+void SnapshotClassView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	HSplitContainer *classes_view = memnew(HSplitContainer);
+	add_child(classes_view);
+	classes_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	classes_view->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	classes_view->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	classes_view->set_split_offset(0);
+
+	VBoxContainer *class_list_column = memnew(VBoxContainer);
+	class_list_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	class_list_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	classes_view->add_child(class_list_column);
+
+	class_tree = memnew(Tree);
+
+	TreeSortAndFilterBar *filter_bar = memnew(TreeSortAndFilterBar(class_tree, TTRC("Filter Classes")));
+	filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
+
+	TreeSortAndFilterBar::SortOptionIndexes default_sort;
+	if (!diff_data) {
+		default_sort = filter_bar->add_sort_option(TTRC("Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
+	} else {
+		filter_bar->add_sort_option(TTRC("A Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
+		filter_bar->add_sort_option(TTRC("B Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 2);
+		default_sort = filter_bar->add_sort_option(TTRC("Delta"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 3);
+	}
+	class_list_column->add_child(filter_bar);
+
+	class_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	class_tree->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
+	class_tree->set_hide_folding(false);
+	class_tree->set_hide_root(true);
+	class_tree->set_columns(diff_data ? 4 : 2);
+	class_tree->set_column_titles_visible(true);
+	class_tree->set_column_title(0, TTRC("Class"));
+	class_tree->set_column_expand(0, true);
+	class_tree->set_column_custom_minimum_width(0, 200 * EDSCALE);
+	class_tree->set_column_title(1, diff_data ? TTRC("A Count") : TTRC("Count"));
+	class_tree->set_column_expand(1, false);
+	if (diff_data) {
+		class_tree->set_column_title_tooltip_text(1, vformat(TTR("A: %s"), snapshot_data->name));
+		class_tree->set_column_title_tooltip_text(2, vformat(TTR("B: %s"), diff_data->name));
+		class_tree->set_column_title(2, TTRC("B Count"));
+		class_tree->set_column_expand(2, false);
+		class_tree->set_column_title(3, TTRC("Delta"));
+		class_tree->set_column_expand(3, false);
+	}
+	class_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_class_selected));
+	class_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	class_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	class_tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	class_list_column->add_child(class_tree);
+
+	VSplitContainer *object_lists = memnew(VSplitContainer);
+	classes_view->add_child(object_lists);
+	object_lists->set_custom_minimum_size(Size2(150 * EDSCALE, 0));
+	object_lists->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_lists->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	if (!diff_data) {
+		object_lists->add_child(object_list = _make_object_list_tree(TTRC("Objects")));
+	} else {
+		object_lists->add_child(object_list = _make_object_list_tree(TTRC("A Objects")));
+		object_lists->add_child(diff_object_list = _make_object_list_tree(TTRC("B Objects")));
+	}
+
+	HashMap<String, ClassData> grouped_by_class;
+	grouped_by_class["Object"] = ClassData("Object", "");
+	_add_objects_to_class_map(grouped_by_class, snapshot_data);
+	if (diff_data != nullptr) {
+		_add_objects_to_class_map(grouped_by_class, diff_data);
+	}
+
+	grouped_by_class[""].tree_node = class_tree->create_item();
+	List<String> classes_todo;
+	for (const String &c : grouped_by_class[""].child_classes) {
+		classes_todo.push_front(c);
+	}
+	while (classes_todo.size() > 0) {
+		String next_class_name = classes_todo.front()->get();
+		classes_todo.pop_front();
+		ClassData &next = grouped_by_class[next_class_name];
+		ClassData &nexts_parent = grouped_by_class[next.parent_class_name];
+		next.tree_node = class_tree->create_item(nexts_parent.tree_node);
+		next.tree_node->set_text(0, next_class_name + " (" + String::num_int64(next.instance_count(snapshot_data)) + ")");
+		next.tree_node->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+		int a_count = next.get_recursive_instance_count(grouped_by_class, snapshot_data);
+		next.tree_node->set_text(1, String::num_int64(a_count));
+		if (diff_data) {
+			int b_count = next.get_recursive_instance_count(grouped_by_class, diff_data);
+			next.tree_node->set_text(2, String::num_int64(b_count));
+			next.tree_node->set_text(3, String::num_int64(a_count - b_count));
+		}
+		next.tree_node->set_metadata(0, next_class_name);
+		for (const String &c : next.child_classes) {
+			classes_todo.push_front(c);
+		}
+	}
+
+	// Icons won't load until the frame after show_snapshot is called. Not sure why, but just defer the load.
+	callable_mp(this, &SnapshotClassView::_notification).call_deferred(NOTIFICATION_THEME_CHANGED);
+
+	// Default to sort by descending count. Putting the biggest groups at the top is generally pretty interesting.
+	filter_bar->select_sort(default_sort.descending);
+	filter_bar->apply();
+}
+
+Tree *SnapshotClassView::_make_object_list_tree(const String &p_column_name) {
+	Tree *list = memnew(Tree);
+	list->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	list->set_hide_folding(true);
+	list->set_hide_root(true);
+	list->set_columns(1);
+	list->set_column_titles_visible(true);
+	list->set_column_title(0, p_column_name);
+	list->set_column_expand(0, true);
+	list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_object_selected).bind(list));
+	list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	return list;
+}
+
+void SnapshotClassView::_add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects) {
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_objects->objects) {
+		StringName class_name = pair.value->type_name;
+		StringName parent_class_name = !class_name.is_empty() && ClassDB::class_exists(class_name) ? ClassDB::get_parent_class(class_name) : "";
+
+		p_class_map[class_name].instances.push_back(pair.value);
+
+		// Go up the tree and insert all parents/grandparents.
+		while (!class_name.is_empty()) {
+			if (!p_class_map.has(class_name)) {
+				p_class_map[class_name] = ClassData(class_name, parent_class_name);
+			}
+
+			if (!p_class_map.has(parent_class_name)) {
+				// Leave our grandparent blank for now. Next iteration of the while loop will fill it in.
+				p_class_map[parent_class_name] = ClassData(parent_class_name, "");
+			}
+			p_class_map[class_name].parent_class_name = parent_class_name;
+			p_class_map[parent_class_name].child_classes.insert(class_name);
+
+			class_name = parent_class_name;
+			parent_class_name = !class_name.is_empty() ? ClassDB::get_parent_class(class_name) : "";
+		}
+	}
+}
+
+void SnapshotClassView::_object_selected(Tree *p_tree) {
+	GameStateSnapshot *snapshot = snapshot_data;
+	if (diff_data) {
+		Tree *other = p_tree == diff_object_list ? object_list : diff_object_list;
+		TreeItem *selected = other->get_selected();
+		if (selected) {
+			selected->deselect(0);
+		}
+		if (p_tree == diff_object_list) {
+			snapshot = diff_data;
+		}
+	}
+	ObjectID object_id = p_tree->get_selected()->get_metadata(0);
+	EditorNode::get_singleton()->push_item(static_cast<Object *>(snapshot->objects[object_id]));
+}
+
+void SnapshotClassView::_class_selected() {
+	_update_lists();
+}
+
+void SnapshotClassView::_populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base) {
+	p_list->clear();
+
+	TreeItem *selected_item = class_tree->get_selected();
+	if (selected_item == nullptr) {
+		p_list->set_column_title(0, vformat("%s (0)", TTR(p_name_base)));
+		return;
+	}
+
+	String class_name = selected_item->get_metadata(0);
+	TreeItem *root = p_list->create_item();
+	int object_count = 0;
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		if (pair.value->type_name == class_name) {
+			TreeItem *item = p_list->create_item(root);
+			item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+			item->set_text(0, pair.value->get_name());
+			item->set_metadata(0, pair.value->remote_object_id);
+			item->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
+			object_count++;
+		}
+	}
+
+	p_list->set_column_title(0, vformat("%s (%d)", TTR(p_name_base), object_count));
+}
+
+void SnapshotClassView::_update_lists() {
+	if (snapshot_data == nullptr) {
+		return;
+	}
+
+	if (!diff_data) {
+		_populate_object_list(snapshot_data, object_list, TTRC("Objects"));
+	} else {
+		_populate_object_list(snapshot_data, object_list, TTRC("A Objects"));
+		_populate_object_list(diff_data, diff_object_list, TTRC("B Objects"));
+	}
+}
+
+void SnapshotClassView::_notification(int p_what) {
+	if (p_what == NOTIFICATION_THEME_CHANGED) {
+		for (TreeItem *item : _get_children_recursive(class_tree)) {
+			item->set_icon(0, EditorNode::get_singleton()->get_class_icon(item->get_metadata(0), ""));
+		}
+	} else if (p_what == NOTIFICATION_TRANSLATION_CHANGED) {
+		_update_lists();
+	}
+}

+ 74 - 0
modules/objectdb_profiler/editor/data_viewers/class_view.h

@@ -0,0 +1,74 @@
+/**************************************************************************/
+/*  class_view.h                                                          */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../snapshot_data.h"
+#include "snapshot_view.h"
+
+class Tree;
+class TreeItem;
+
+struct ClassData {
+	ClassData() {}
+	ClassData(const String &p_name, const String &p_parent) :
+			class_name(p_name), parent_class_name(p_parent) {}
+	String class_name;
+	String parent_class_name;
+	HashSet<String> child_classes;
+	LocalVector<SnapshotDataObject *> instances;
+	TreeItem *tree_node = nullptr;
+	HashMap<GameStateSnapshot *, int> recursive_instance_count_cache;
+
+	int instance_count(GameStateSnapshot *p_snapshot = nullptr);
+	int get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot = nullptr);
+};
+
+class SnapshotClassView : public SnapshotView {
+	GDCLASS(SnapshotClassView, SnapshotView);
+
+protected:
+	Tree *class_tree = nullptr;
+	Tree *object_list = nullptr;
+	Tree *diff_object_list = nullptr;
+
+	void _object_selected(Tree *p_tree);
+	void _class_selected();
+	void _add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects);
+	void _notification(int p_what);
+
+	Tree *_make_object_list_tree(const String &p_column_name);
+	void _populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base);
+	void _update_lists();
+
+public:
+	SnapshotClassView();
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
+};

+ 275 - 0
modules/objectdb_profiler/editor/data_viewers/node_view.cpp

@@ -0,0 +1,275 @@
+/**************************************************************************/
+/*  node_view.cpp                                                         */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "node_view.h"
+
+#include "editor/editor_node.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/check_button.h"
+#include "scene/gui/popup_menu.h"
+#include "scene/gui/split_container.h"
+
+SnapshotNodeView::SnapshotNodeView() {
+	set_name(TTRC("Nodes"));
+}
+
+void SnapshotNodeView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	HSplitContainer *diff_sides = memnew(HSplitContainer);
+	diff_sides->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	add_child(diff_sides);
+
+	main_tree = _make_node_tree(diff_data && !combined_diff_view ? TTRC("A Nodes") : TTRC("Nodes"));
+	diff_sides->add_child(main_tree.root);
+	_add_snapshot_to_tree(main_tree.tree, snapshot_data, diff_data && combined_diff_view ? DIFF_GROUP_REMOVED : DIFF_GROUP_NONE);
+
+	if (diff_data) {
+		CheckButton *diff_mode_toggle = memnew(CheckButton(TTRC("Combine Diff")));
+		diff_mode_toggle->set_pressed(combined_diff_view);
+		diff_mode_toggle->connect(SceneStringName(toggled), callable_mp(this, &SnapshotNodeView::_toggle_diff_mode));
+		main_tree.filter_bar->add_child(diff_mode_toggle);
+		main_tree.filter_bar->move_child(diff_mode_toggle, 0);
+
+		if (combined_diff_view) {
+			// Merge the snapshots together and add a diff.
+			_add_snapshot_to_tree(main_tree.tree, diff_data, DIFF_GROUP_ADDED);
+		} else {
+			// Add a second column with the diff snapshot.
+			diff_tree = _make_node_tree(TTRC("B Nodes"));
+			diff_sides->add_child(diff_tree.root);
+			_add_snapshot_to_tree(diff_tree.tree, diff_data, DIFF_GROUP_NONE);
+		}
+	}
+
+	_refresh_icons();
+	main_tree.filter_bar->apply();
+	if (diff_tree.filter_bar) {
+		diff_tree.filter_bar->apply();
+		diff_sides->set_split_offset(diff_sides->get_size().x * 0.5);
+	}
+
+	choose_object_menu = memnew(PopupMenu);
+	add_child(choose_object_menu);
+	choose_object_menu->connect(SceneStringName(id_pressed), callable_mp(this, &SnapshotNodeView::_choose_object_pressed).bind(false));
+}
+
+NodeTreeElements SnapshotNodeView::_make_node_tree(const String &p_tree_name) {
+	NodeTreeElements elements;
+	elements.root = memnew(VBoxContainer);
+	elements.root->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	elements.tree = memnew(Tree);
+	elements.filter_bar = memnew(TreeSortAndFilterBar(elements.tree, TTRC("Filter Nodes")));
+	elements.root->add_child(elements.filter_bar);
+	elements.tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	elements.tree->set_custom_minimum_size(Size2(150, 0) * EDSCALE);
+	elements.tree->set_hide_folding(false);
+	elements.root->add_child(elements.tree);
+	elements.tree->set_hide_root(true);
+	elements.tree->set_allow_reselect(true);
+	elements.tree->set_columns(1);
+	elements.tree->set_column_titles_visible(true);
+	elements.tree->set_column_title(0, p_tree_name);
+	elements.tree->set_column_expand(0, true);
+	elements.tree->set_column_clip_content(0, false);
+	elements.tree->set_column_custom_minimum_width(0, 150 * EDSCALE);
+	elements.tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotNodeView::_node_selected).bind(elements.tree));
+	elements.tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	elements.tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	elements.tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	elements.tree->create_item();
+
+	return elements;
+}
+
+void SnapshotNodeView::_node_selected(Tree *p_tree_selected_from) {
+	active_tree = p_tree_selected_from;
+	if (diff_tree.tree) {
+		// Deselect nodes in non-active tree, if needed.
+		if (active_tree == main_tree.tree) {
+			diff_tree.tree->deselect_all();
+		}
+		if (active_tree == diff_tree.tree) {
+			main_tree.tree->deselect_all();
+		}
+	}
+
+	const LocalVector<SnapshotDataObject *> &item_data = tree_item_data[p_tree_selected_from->get_selected()];
+	if (item_data.is_empty()) {
+		return;
+	} else if (item_data.size() == 1) {
+		EditorNode::get_singleton()->push_item(static_cast<Object *>(item_data[0]));
+	} else if (item_data.size() == 2) {
+		// This happens if we're in the combined diff view and the node exists in both trees
+		// The user has to specify which version of the node they want to see in the inspector.
+		_show_choose_object_menu();
+	}
+}
+
+void SnapshotNodeView::_toggle_diff_mode(bool p_state) {
+	combined_diff_view = p_state;
+	show_snapshot(snapshot_data, diff_data); // Redraw everything when we toggle views.
+}
+
+void SnapshotNodeView::_notification(int p_what) {
+	if (p_what == NOTIFICATION_THEME_CHANGED) {
+		_refresh_icons();
+	}
+}
+
+void SnapshotNodeView::_add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, DiffGroup p_diff_group) {
+	SnapshotDataObject *scene_root = nullptr;
+	LocalVector<SnapshotDataObject *> orphan_nodes;
+
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &kv : p_snapshot->objects) {
+		if (kv.value->is_node() && !kv.value->extra_debug_data.has("node_parent")) {
+			if (kv.value->extra_debug_data["node_is_scene_root"]) {
+				scene_root = kv.value;
+			} else {
+				orphan_nodes.push_back(kv.value);
+			}
+		}
+	}
+
+	if (scene_root != nullptr) {
+		TreeItem *root_item = _add_item_to_tree(p_tree, p_tree->get_root(), scene_root, p_diff_group);
+		_add_children_to_tree(root_item, scene_root, p_diff_group);
+	}
+
+	if (!orphan_nodes.is_empty()) {
+		TreeItem *orphans_item = _add_item_to_tree(p_tree, p_tree->get_root(), TTRC("Orphan Nodes"), p_diff_group);
+		for (SnapshotDataObject *orphan_node : orphan_nodes) {
+			TreeItem *orphan_item = _add_item_to_tree(p_tree, orphans_item, orphan_node, p_diff_group);
+			_add_children_to_tree(orphan_item, orphan_node, p_diff_group);
+		}
+	}
+}
+
+void SnapshotNodeView::_add_children_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, DiffGroup p_diff_group) {
+	for (const Variant &child_id : (Array)p_data->extra_debug_data["node_children"]) {
+		SnapshotDataObject *child_object = p_data->snapshot->objects[ObjectID((uint64_t)child_id)];
+		TreeItem *child_item = _add_item_to_tree(p_parent_item->get_tree(), p_parent_item, child_object, p_diff_group);
+		_add_children_to_tree(child_item, child_object, p_diff_group);
+	}
+}
+
+TreeItem *SnapshotNodeView::_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, const String &p_item_name, DiffGroup p_diff_group) {
+	// Find out if this node already exists.
+	TreeItem *item = nullptr;
+	if (p_diff_group != DIFF_GROUP_NONE) {
+		for (int idx = 0; idx < p_parent->get_child_count(); idx++) {
+			TreeItem *child = p_parent->get_child(idx);
+			if (child->get_text(0) == p_item_name) {
+				item = child;
+				break;
+			}
+		}
+	}
+
+	if (item) {
+		// If it exists, clear the background color because we now know it exists in both trees.
+		item->clear_custom_bg_color(0);
+	} else {
+		// Add the new node and set its background color to green or red depending on which snapshot it's a part of.
+		item = p_tree->create_item(p_parent);
+
+		if (p_diff_group == DIFF_GROUP_ADDED) {
+			item->set_custom_bg_color(0, Color(0, 1, 0, 0.1));
+		} else if (p_diff_group == DIFF_GROUP_REMOVED) {
+			item->set_custom_bg_color(0, Color(1, 0, 0, 0.1));
+		}
+	}
+
+	item->set_text(0, p_item_name);
+	item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+
+	return item;
+}
+
+TreeItem *SnapshotNodeView::_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, SnapshotDataObject *p_data, DiffGroup p_diff_group) {
+	String node_name = p_data->extra_debug_data["node_name"];
+	TreeItem *child_item = _add_item_to_tree(p_tree, p_parent, node_name, p_diff_group);
+	tree_item_data[child_item].push_back(p_data);
+	return child_item;
+}
+
+void SnapshotNodeView::_refresh_icons() {
+	for (TreeItem *item : _get_children_recursive(main_tree.tree)) {
+		HashMap<TreeItem *, LocalVector<SnapshotDataObject *>>::Iterator E = tree_item_data.find(item);
+		if (E && !E->value.is_empty()) {
+			item->set_icon(0, EditorNode::get_singleton()->get_class_icon(E->value[0]->type_name));
+		} else {
+			item->set_icon(0, EditorNode::get_singleton()->get_class_icon("MissingNode"));
+		}
+	}
+
+	if (diff_tree.tree) {
+		for (TreeItem *item : _get_children_recursive(diff_tree.tree)) {
+			HashMap<TreeItem *, LocalVector<SnapshotDataObject *>>::Iterator E = tree_item_data.find(item);
+			if (E && !E->value.is_empty()) {
+				item->set_icon(0, EditorNode::get_singleton()->get_class_icon(E->value[0]->type_name));
+			} else {
+				item->set_icon(0, EditorNode::get_singleton()->get_class_icon("MissingNode"));
+			}
+		}
+	}
+}
+
+void SnapshotNodeView::clear_snapshot() {
+	SnapshotView::clear_snapshot();
+
+	tree_item_data.clear();
+	main_tree.tree = nullptr;
+	main_tree.filter_bar = nullptr;
+	main_tree.root = nullptr;
+	diff_tree.tree = nullptr;
+	diff_tree.filter_bar = nullptr;
+	diff_tree.root = nullptr;
+	active_tree = nullptr;
+}
+
+void SnapshotNodeView::_choose_object_pressed(int p_object_idx, bool p_confirm_override) {
+	EditorNode::get_singleton()->push_item(static_cast<Object *>(tree_item_data[active_tree->get_selected()][p_object_idx]));
+}
+
+void SnapshotNodeView::_show_choose_object_menu() {
+	remove_child(choose_object_menu);
+	add_child(choose_object_menu);
+	choose_object_menu->clear(false);
+	choose_object_menu->add_item(TTRC("Snapshot A"), 0);
+	choose_object_menu->add_item(TTRC("Snapshot B"), 1);
+	choose_object_menu->reset_size();
+	choose_object_menu->set_position(get_screen_position() + get_local_mouse_position());
+	choose_object_menu->popup();
+}

+ 86 - 0
modules/objectdb_profiler/editor/data_viewers/node_view.h

@@ -0,0 +1,86 @@
+/**************************************************************************/
+/*  node_view.h                                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../snapshot_data.h"
+#include "shared_controls.h"
+#include "snapshot_view.h"
+
+class Tree;
+
+// When diffing in split view, we have two trees/filters
+// so this struct is used to group their properties together.
+struct NodeTreeElements {
+	NodeTreeElements() {
+		tree = nullptr;
+		filter_bar = nullptr;
+		root = nullptr;
+	}
+	Tree *tree = nullptr;
+	TreeSortAndFilterBar *filter_bar = nullptr;
+	VBoxContainer *root = nullptr;
+};
+
+class SnapshotNodeView : public SnapshotView {
+	GDCLASS(SnapshotNodeView, SnapshotView);
+
+	enum DiffGroup {
+		DIFF_GROUP_NONE,
+		DIFF_GROUP_ADDED,
+		DIFF_GROUP_REMOVED
+	};
+
+	NodeTreeElements main_tree;
+	NodeTreeElements diff_tree;
+	Tree *active_tree = nullptr;
+	PopupMenu *choose_object_menu = nullptr;
+	bool combined_diff_view = true;
+	HashMap<TreeItem *, LocalVector<SnapshotDataObject *>> tree_item_data;
+
+	void _node_selected(Tree *p_tree_selected_from);
+	void _notification(int p_what);
+	NodeTreeElements _make_node_tree(const String &p_tree_name);
+	void _apply_filters();
+	void _refresh_icons();
+	void _toggle_diff_mode(bool p_state);
+	void _choose_object_pressed(int p_object_idx, bool p_confirm_override);
+	void _show_choose_object_menu();
+
+	void _add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, DiffGroup p_diff_group = DIFF_GROUP_NONE);
+	void _add_children_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, DiffGroup p_diff_group = DIFF_GROUP_NONE);
+	TreeItem *_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, const String &p_item_name, DiffGroup p_diff_group = DIFF_GROUP_NONE);
+	TreeItem *_add_item_to_tree(Tree *p_tree, TreeItem *p_parent, SnapshotDataObject *p_data, DiffGroup p_diff_group = DIFF_GROUP_NONE);
+
+public:
+	SnapshotNodeView();
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
+	virtual void clear_snapshot() override;
+};

+ 260 - 0
modules/objectdb_profiler/editor/data_viewers/object_view.cpp

@@ -0,0 +1,260 @@
+/**************************************************************************/
+/*  object_view.cpp                                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "object_view.h"
+
+#include "editor/editor_node.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/rich_text_label.h"
+#include "scene/gui/split_container.h"
+
+SnapshotObjectView::SnapshotObjectView() {
+	set_name(TTRC("Objects"));
+}
+
+void SnapshotObjectView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+
+	item_data_map.clear();
+	data_item_map.clear();
+
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	objects_view = memnew(HSplitContainer);
+	add_child(objects_view);
+	objects_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	VBoxContainer *object_column = memnew(VBoxContainer);
+	object_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	objects_view->add_child(object_column);
+	object_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	object_list = memnew(Tree);
+
+	filter_bar = memnew(TreeSortAndFilterBar(object_list, TTRC("Filter Objects")));
+	object_column->add_child(filter_bar);
+	int sort_idx = 0;
+	if (diff_data) {
+		filter_bar->add_sort_option(TTRC("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
+	}
+	filter_bar->add_sort_option(TTRC("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
+	filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
+	filter_bar->add_sort_option(TTRC("Inbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
+	TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
+			TTRC("Outbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
+
+	// Tree of objects.
+	object_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	object_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+	object_list->set_hide_folding(false);
+	object_column->add_child(object_list);
+	object_list->set_hide_root(true);
+	object_list->set_columns(diff_data ? 5 : 4);
+	object_list->set_column_titles_visible(true);
+	int offset = 0;
+	if (diff_data) {
+		object_list->set_column_title(0, TTRC("Snapshot"));
+		object_list->set_column_expand(0, false);
+		object_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
+		offset++;
+	}
+	object_list->set_column_title(offset + 0, TTRC("Class"));
+	object_list->set_column_expand(offset + 0, true);
+	object_list->set_column_title_tooltip_text(offset + 0, TTRC("Object's class"));
+	object_list->set_column_title(offset + 1, TTRC("Object"));
+	object_list->set_column_expand(offset + 1, true);
+	object_list->set_column_expand_ratio(offset + 1, 2);
+	object_list->set_column_title_tooltip_text(offset + 1, TTRC("Object's name"));
+	object_list->set_column_title(offset + 2, TTRC("In"));
+	object_list->set_column_expand(offset + 2, false);
+	object_list->set_column_clip_content(offset + 2, false);
+	object_list->set_column_title_tooltip_text(offset + 2, TTRC("Number of inbound references"));
+	object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
+	object_list->set_column_title(offset + 3, TTRC("Out"));
+	object_list->set_column_expand(offset + 3, false);
+	object_list->set_column_clip_content(offset + 3, false);
+	object_list->set_column_title_tooltip_text(offset + 3, TTRC("Number of outbound references"));
+	object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
+	object_list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_object_selected));
+	object_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	object_details = memnew(VBoxContainer);
+	object_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+	objects_view->add_child(object_details);
+	object_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	object_list->create_item();
+	_insert_data(snapshot_data, TTRC("A"));
+	if (diff_data) {
+		_insert_data(diff_data, TTRC("B"));
+	}
+
+	filter_bar->select_sort(default_sort.descending);
+	filter_bar->apply();
+	object_list->set_selected(object_list->get_root()->get_first_child());
+	// Expand the left panel as wide as we can. Passing `INT_MAX` or any very large int will have the opposite effect
+	// and shrink the left panel as small as it can go. So, pass an int we know is larger than the current panel, but not
+	// 'very' large (whatever that exact number is).
+	objects_view->set_split_offset(get_viewport_rect().size.x);
+}
+
+void SnapshotObjectView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		TreeItem *item = object_list->create_item(object_list->get_root());
+		int offset = 0;
+		if (diff_data) {
+			item->set_text(0, p_name);
+			item->set_tooltip_text(0, p_snapshot->name);
+			item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+			offset = 1;
+		}
+		item->set_auto_translate_mode(offset + 0, AUTO_TRANSLATE_MODE_DISABLED);
+		item->set_auto_translate_mode(offset + 1, AUTO_TRANSLATE_MODE_DISABLED);
+		item->set_text(offset + 0, pair.value->type_name);
+		item->set_text(offset + 1, pair.value->get_name());
+		item->set_text(offset + 2, String::num_uint64(pair.value->inbound_references.size()));
+		item->set_text(offset + 3, String::num_uint64(pair.value->outbound_references.size()));
+		item_data_map[item] = pair.value;
+		data_item_map[pair.value] = item;
+	}
+}
+
+void SnapshotObjectView::_object_selected() {
+	reference_item_map.clear();
+
+	for (int i = 0; i < object_details->get_child_count(); i++) {
+		object_details->get_child(i)->queue_free();
+	}
+
+	SnapshotDataObject *d = item_data_map[object_list->get_selected()];
+	EditorNode::get_singleton()->push_item(static_cast<Object *>(d));
+
+	DarkPanelContainer *object_panel = memnew(DarkPanelContainer);
+	VBoxContainer *object_panel_content = memnew(VBoxContainer);
+	object_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_details->add_child(object_panel);
+	object_panel->add_child(object_panel_content);
+	object_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
+
+	ScrollContainer *properties_scroll = memnew(ScrollContainer);
+	properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
+	properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
+	properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	object_panel_content->add_child(properties_scroll);
+
+	VBoxContainer *properties_container = memnew(VBoxContainer);
+	properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	properties_scroll->add_child(properties_container);
+	properties_container->add_theme_constant_override("separation", 8);
+
+	inbound_tree = _make_references_list(properties_container, TTRC("Inbound References"), TTRC("Source"), TTRC("Other object referencing this object"), TTRC("Property"), TTRC("Property of other object referencing this object"));
+	inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(inbound_tree));
+	TreeItem *ib_root = inbound_tree->create_item();
+	for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
+		TreeItem *i = inbound_tree->create_item(ib_root);
+		i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+		i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
+
+		SnapshotDataObject *target = d->snapshot->objects[ob.value];
+		i->set_text(0, target->get_name());
+		i->set_text(1, ob.key);
+		reference_item_map[i] = data_item_map[target];
+	}
+
+	outbound_tree = _make_references_list(properties_container, TTRC("Outbound References"), TTRC("Property"), TTRC("Property of this object referencing other object"), TTRC("Target"), TTRC("Other object being referenced"));
+	outbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(outbound_tree));
+	TreeItem *ob_root = outbound_tree->create_item();
+	for (const KeyValue<String, ObjectID> &ob : d->outbound_references) {
+		TreeItem *i = outbound_tree->create_item(ob_root);
+		i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+		i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
+
+		SnapshotDataObject *target = d->snapshot->objects[ob.value];
+		i->set_text(0, ob.key);
+		i->set_text(1, target->get_name());
+		reference_item_map[i] = data_item_map[target];
+	}
+}
+
+void SnapshotObjectView::_reference_selected(Tree *p_source_tree) {
+	TreeItem *ref_item = p_source_tree->get_selected();
+	Tree *other_tree = p_source_tree == inbound_tree ? outbound_tree : inbound_tree;
+	other_tree->deselect_all();
+	TreeItem *other = reference_item_map[ref_item];
+	if (other) {
+		if (!other->is_visible()) {
+			// Clear the filter if we can't see the node we just chose.
+			filter_bar->clear_filter();
+		}
+		other->get_tree()->deselect_all();
+		other->get_tree()->set_selected(other);
+		other->get_tree()->ensure_cursor_is_visible();
+	}
+}
+
+Tree *SnapshotObjectView::_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip) {
+	VBoxContainer *vbox = memnew(VBoxContainer);
+	vbox->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	vbox->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	vbox->add_theme_constant_override("separation", 4);
+	p_container->add_child(vbox);
+
+	vbox->set_custom_minimum_size(Vector2(300, 0) * EDSCALE);
+
+	RichTextLabel *lbl = memnew(RichTextLabel("[center]" + p_name + "[center]"));
+	lbl->set_fit_content(true);
+	lbl->set_use_bbcode(true);
+	vbox->add_child(lbl);
+	Tree *tree = memnew(Tree);
+	tree->set_hide_folding(true);
+	vbox->add_child(tree);
+	tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	tree->set_hide_root(true);
+	tree->set_columns(2);
+	tree->set_column_titles_visible(true);
+	tree->set_column_title(0, p_col_1);
+	tree->set_column_expand(0, true);
+	tree->set_column_title_tooltip_text(0, p_col_1_tooltip);
+	tree->set_column_clip_content(0, false);
+	tree->set_column_title(1, p_col_2);
+	tree->set_column_expand(1, true);
+	tree->set_column_clip_content(1, false);
+	tree->set_column_title_tooltip_text(1, p_col_2_tooltip);
+	tree->set_v_scroll_enabled(false);
+	tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	return tree;
+}

+ 63 - 0
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<TreeItem *, SnapshotDataObject *> item_data_map;
+	HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
+	HashMap<TreeItem *, TreeItem *> reference_item_map;
+
+	void _object_selected();
+	void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
+	Tree *_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip);
+	void _reference_selected(Tree *p_source_tree);
+
+public:
+	SnapshotObjectView();
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
+};

+ 323 - 0
modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp

@@ -0,0 +1,323 @@
+/**************************************************************************/
+/*  refcounted_view.cpp                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "refcounted_view.h"
+
+#include "editor/editor_node.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/rich_text_label.h"
+#include "scene/gui/split_container.h"
+
+SnapshotRefCountedView::SnapshotRefCountedView() {
+	set_name(TTRC("RefCounted"));
+}
+
+void SnapshotRefCountedView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+
+	item_data_map.clear();
+	data_item_map.clear();
+
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	refs_view = memnew(HSplitContainer);
+	add_child(refs_view);
+	refs_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	VBoxContainer *refs_column = memnew(VBoxContainer);
+	refs_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	refs_view->add_child(refs_column);
+
+	// Tree of Refs.
+	refs_list = memnew(Tree);
+
+	filter_bar = memnew(TreeSortAndFilterBar(refs_list, TTRC("Filter RefCounteds")));
+	refs_column->add_child(filter_bar);
+	int offset = diff_data ? 1 : 0;
+	if (diff_data) {
+		filter_bar->add_sort_option(TTRC("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
+	}
+	filter_bar->add_sort_option(TTRC("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 0);
+	filter_bar->add_sort_option(TTRC("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 1);
+	TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
+			TTRC("Native Refs"),
+			TreeSortAndFilterBar::SortType::NUMERIC_SORT,
+			offset + 2);
+	filter_bar->add_sort_option(TTRC("ObjectDB Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 3);
+	filter_bar->add_sort_option(TTRC("Total Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 4);
+	filter_bar->add_sort_option(TTRC("ObjectDB Cycles"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 5);
+
+	refs_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	refs_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+	refs_list->set_hide_folding(false);
+	refs_column->add_child(refs_list);
+	refs_list->set_hide_root(true);
+	refs_list->set_columns(diff_data ? 7 : 6);
+	refs_list->set_column_titles_visible(true);
+
+	if (diff_data) {
+		refs_list->set_column_title(0, TTRC("Snapshot"));
+		refs_list->set_column_expand(0, false);
+		refs_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
+	}
+
+	refs_list->set_column_title(offset + 0, TTRC("Class"));
+	refs_list->set_column_expand(offset + 0, true);
+	refs_list->set_column_title_tooltip_text(offset + 0, TTRC("Object's class"));
+
+	refs_list->set_column_title(offset + 1, TTRC("Name"));
+	refs_list->set_column_expand(offset + 1, true);
+	refs_list->set_column_expand_ratio(offset + 1, 2);
+	refs_list->set_column_title_tooltip_text(offset + 1, TTRC("Object's name"));
+
+	refs_list->set_column_title(offset + 2, TTRC("Native Refs"));
+	refs_list->set_column_expand(offset + 2, false);
+	refs_list->set_column_title_tooltip_text(offset + 2, TTRC("References not owned by the ObjectDB"));
+
+	refs_list->set_column_title(offset + 3, TTRC("ObjectDB Refs"));
+	refs_list->set_column_expand(offset + 3, false);
+	refs_list->set_column_title_tooltip_text(offset + 3, TTRC("References owned by the ObjectDB"));
+
+	refs_list->set_column_title(offset + 4, TTRC("Total Refs"));
+	refs_list->set_column_expand(offset + 4, false);
+	refs_list->set_column_title_tooltip_text(offset + 4, TTRC("ObjectDB References + Native References"));
+
+	refs_list->set_column_title(offset + 5, TTRC("ObjectDB Cycles"));
+	refs_list->set_column_expand(offset + 5, false);
+	refs_list->set_column_title_tooltip_text(offset + 5, TTRC("Cycles detected in the ObjectDB"));
+
+	refs_list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotRefCountedView::_refcounted_selected));
+	refs_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	refs_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	// View of the selected refcounted.
+	ref_details = memnew(VBoxContainer);
+	ref_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+	refs_view->add_child(ref_details);
+	ref_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	ref_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	refs_list->create_item();
+	_insert_data(snapshot_data, TTRC("A"));
+	if (diff_data) {
+		_insert_data(diff_data, TTRC("B"));
+	}
+
+	// Push the split as far right as possible.
+	filter_bar->select_sort(default_sort.descending);
+	filter_bar->apply();
+	refs_list->set_selected(refs_list->get_root()->get_first_child());
+
+	callable_mp(this, &SnapshotRefCountedView::_set_split_to_center).call_deferred();
+}
+
+void SnapshotRefCountedView::_set_split_to_center() {
+	refs_view->set_split_offset(refs_view->get_size().x * 0.5);
+}
+
+void SnapshotRefCountedView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		if (!pair.value->is_refcounted()) {
+			continue;
+		}
+
+		TreeItem *item = refs_list->create_item(refs_list->get_root());
+		item_data_map[item] = pair.value;
+		data_item_map[pair.value] = item;
+		int total_refs = pair.value->extra_debug_data.has("ref_count") ? (uint64_t)pair.value->extra_debug_data["ref_count"] : 0;
+		int objectdb_refs = pair.value->get_unique_inbound_references().size();
+		int native_refs = total_refs - objectdb_refs;
+
+		Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
+
+		int offset = 0;
+		if (diff_data) {
+			item->set_text(0, p_name);
+			item->set_tooltip_text(0, p_snapshot->name);
+			item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+			offset = 1;
+		}
+
+		item->set_text(offset + 0, pair.value->type_name);
+		item->set_auto_translate_mode(offset + 0, AUTO_TRANSLATE_MODE_DISABLED);
+		item->set_text(offset + 1, pair.value->get_name());
+		item->set_auto_translate_mode(offset + 1, AUTO_TRANSLATE_MODE_DISABLED);
+		item->set_text(offset + 2, String::num_uint64(native_refs));
+		item->set_text(offset + 3, String::num_uint64(objectdb_refs));
+		item->set_text(offset + 4, String::num_uint64(total_refs));
+		item->set_text(offset + 5, String::num_uint64(ref_cycles.size())); // Compute cycles and attach it to refcounted object.
+
+		if (total_refs == ref_cycles.size()) {
+			// Often, references are held by the engine so we can't know if we're stuck in a cycle or not
+			// But if the full cycle is visible in the ObjectDB,
+			// tell the user by highlighting the cells in red.
+			item->set_custom_bg_color(offset + 5, Color(1, 0, 0, 0.1));
+		}
+	}
+}
+
+void SnapshotRefCountedView::_refcounted_selected() {
+	for (int i = 0; i < ref_details->get_child_count(); i++) {
+		ref_details->get_child(i)->queue_free();
+	}
+
+	SnapshotDataObject *d = item_data_map[refs_list->get_selected()];
+	EditorNode::get_singleton()->push_item(static_cast<Object *>(d));
+
+	DarkPanelContainer *refcounted_panel = memnew(DarkPanelContainer);
+	VBoxContainer *refcounted_panel_content = memnew(VBoxContainer);
+	refcounted_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	refcounted_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	ref_details->add_child(refcounted_panel);
+	refcounted_panel->add_child(refcounted_panel_content);
+	refcounted_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
+
+	ScrollContainer *properties_scroll = memnew(ScrollContainer);
+	properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
+	properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
+	properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	refcounted_panel_content->add_child(properties_scroll);
+
+	VBoxContainer *properties_container = memnew(VBoxContainer);
+	properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	properties_scroll->add_child(properties_container);
+	properties_container->add_theme_constant_override("separation", 5);
+	properties_container->add_theme_constant_override("margin_left", 2);
+	properties_container->add_theme_constant_override("margin_right", 2);
+	properties_container->add_theme_constant_override("margin_top", 2);
+	properties_container->add_theme_constant_override("margin_bottom", 2);
+
+	int total_refs = d->extra_debug_data.has("ref_count") ? (uint64_t)d->extra_debug_data["ref_count"] : 0;
+	int objectdb_refs = d->get_unique_inbound_references().size();
+	int native_refs = total_refs - objectdb_refs;
+	Array ref_cycles = (Array)d->extra_debug_data["ref_cycles"];
+
+	String count_str = "[ul]\n";
+	count_str += vformat(TTRC("Native References: %d\n"), native_refs);
+	count_str += vformat(TTRC("ObjectDB References: %d\n"), objectdb_refs);
+	count_str += vformat(TTRC("Total References: %d\n"), total_refs);
+	count_str += vformat(TTRC("ObjectDB Cycles: %d\n"), ref_cycles.size());
+	count_str += "[/ul]\n";
+	RichTextLabel *counts = memnew(RichTextLabel(count_str));
+	counts->set_use_bbcode(true);
+	counts->set_fit_content(true);
+	counts->add_theme_constant_override("line_separation", 6);
+	properties_container->add_child(counts);
+
+	if (d->inbound_references.size() > 0) {
+		RichTextLabel *inbound_lbl = memnew(RichTextLabel(TTRC("[center]ObjectDB References[center]")));
+		inbound_lbl->set_fit_content(true);
+		inbound_lbl->set_use_bbcode(true);
+		properties_container->add_child(inbound_lbl);
+		Tree *inbound_tree = memnew(Tree);
+		inbound_tree->set_hide_folding(true);
+		properties_container->add_child(inbound_tree);
+		inbound_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
+		inbound_tree->set_hide_root(true);
+		inbound_tree->set_columns(3);
+		inbound_tree->set_column_titles_visible(true);
+		inbound_tree->set_column_title(0, TTRC("Source"));
+		inbound_tree->set_column_expand(0, true);
+		inbound_tree->set_column_clip_content(0, false);
+		inbound_tree->set_column_title_tooltip_text(0, TTRC("Other object referencing this object"));
+		inbound_tree->set_column_title(1, TTRC("Property"));
+		inbound_tree->set_column_expand(1, true);
+		inbound_tree->set_column_clip_content(1, true);
+		inbound_tree->set_column_title_tooltip_text(1, TTRC("Property of other object referencing this object"));
+		inbound_tree->set_column_title(2, TTRC("Duplicate?"));
+		inbound_tree->set_column_expand(2, false);
+		inbound_tree->set_column_title_tooltip_text(2, TTRC("Was the same reference returned by multiple getters on the source object?"));
+		inbound_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		inbound_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		inbound_tree->set_v_scroll_enabled(false);
+		inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotRefCountedView::_ref_selected).bind(inbound_tree));
+
+		// The same reference can exist as multiple properties of an object (for example, gdscript `@export` properties exist twice).
+		// We flag for the user if a property is exposed multiple times so it's clearer why there are more references in the list
+		// than the ObjectDB References count would suggest.
+		HashMap<ObjectID, int> property_repeat_count;
+		for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
+			if (!property_repeat_count.has(ob.value)) {
+				property_repeat_count.insert(ob.value, 0);
+			}
+			property_repeat_count[ob.value]++;
+		}
+
+		TreeItem *root = inbound_tree->create_item();
+		for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
+			TreeItem *i = inbound_tree->create_item(root);
+			SnapshotDataObject *target = d->snapshot->objects[ob.value];
+			i->set_text(0, target->get_name());
+			i->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+			i->set_text(1, ob.key);
+			i->set_auto_translate_mode(1, AUTO_TRANSLATE_MODE_DISABLED);
+			i->set_text(2, property_repeat_count[ob.value] > 1 ? TTRC("Yes") : TTRC("No"));
+			reference_item_map[i] = data_item_map[target];
+		}
+	}
+
+	if (ref_cycles.size() > 0) {
+		properties_container->add_child(memnew(SpanningHeader(TTRC("ObjectDB Cycles"))));
+		Tree *cycles_tree = memnew(Tree);
+		cycles_tree->set_hide_folding(true);
+		properties_container->add_child(cycles_tree);
+		cycles_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
+		cycles_tree->set_hide_root(true);
+		cycles_tree->set_columns(1);
+		cycles_tree->set_column_titles_visible(false);
+		cycles_tree->set_column_expand(0, true);
+		cycles_tree->set_column_clip_content(0, false);
+		cycles_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		cycles_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		cycles_tree->set_v_scroll_enabled(false);
+
+		TreeItem *root = cycles_tree->create_item();
+		for (const Variant &cycle : ref_cycles) {
+			TreeItem *i = cycles_tree->create_item(root);
+			i->set_text(0, cycle);
+			i->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
+		}
+	}
+}
+
+void SnapshotRefCountedView::_ref_selected(Tree *p_source_tree) {
+	TreeItem *target = reference_item_map[p_source_tree->get_selected()];
+	if (target) {
+		if (!target->is_visible()) {
+			// Clear the filter if we can't see the node we just chose.
+			filter_bar->clear_filter();
+		}
+		target->get_tree()->deselect_all();
+		target->get_tree()->set_selected(target);
+		target->get_tree()->ensure_cursor_is_visible();
+	}
+}

+ 61 - 0
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<TreeItem *, SnapshotDataObject *> item_data_map;
+	HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
+	HashMap<TreeItem *, TreeItem *> reference_item_map;
+
+	void _refcounted_selected();
+	void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
+	void _ref_selected(Tree *p_source_tree);
+	void _set_split_to_center();
+
+public:
+	SnapshotRefCountedView();
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
+};

+ 241 - 0
modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp

@@ -0,0 +1,241 @@
+/**************************************************************************/
+/*  shared_controls.cpp                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "shared_controls.h"
+
+#include "editor/editor_node.h"
+#include "editor/editor_string_names.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/label.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/menu_button.h"
+#include "scene/resources/style_box_flat.h"
+
+SpanningHeader::SpanningHeader(const String &p_text) {
+	Ref<StyleBoxFlat> title_sbf;
+	title_sbf.instantiate();
+	title_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
+	add_theme_style_override(SceneStringName(panel), title_sbf);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	Label *title = memnew(Label(p_text));
+	add_child(title);
+	title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+	title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
+}
+
+DarkPanelContainer::DarkPanelContainer() {
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	Ref<StyleBoxFlat> content_wrapper_sbf;
+	content_wrapper_sbf.instantiate();
+	content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
+	add_theme_style_override(SceneStringName(panel), content_wrapper_sbf);
+}
+
+void TreeSortAndFilterBar::_apply_filter(TreeItem *p_current_node) {
+	if (!p_current_node) {
+		p_current_node = managed_tree->get_root();
+	}
+
+	if (!p_current_node) {
+		return;
+	}
+
+	// Reset ourselves to default state.
+	p_current_node->set_visible(true);
+	p_current_node->clear_custom_color(0);
+
+	// Go through each child and filter them.
+	bool any_child_visible = false;
+	for (TreeItem *child = p_current_node->get_first_child(); child; child = child->get_next()) {
+		_apply_filter(child);
+		if (child->is_visible()) {
+			any_child_visible = true;
+		}
+	}
+
+	// Check if we match the filter.
+	String filter_str = filter_edit->get_text().strip_edges(true, true).to_lower();
+
+	// We are visible.
+	bool matches_filter = false;
+	for (int i = 0; i < managed_tree->get_columns(); i++) {
+		if (p_current_node->get_text(i).to_lower().contains(filter_str)) {
+			matches_filter = true;
+			break;
+		}
+	}
+	if (matches_filter || filter_str.is_empty()) {
+		p_current_node->set_visible(true);
+	} else if (any_child_visible) {
+		// We have a visible child.
+		p_current_node->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+	} else {
+		// We and our children are not visible.
+		p_current_node->set_visible(false);
+	}
+}
+
+void TreeSortAndFilterBar::_apply_sort() {
+	if (!sort_button->is_visible()) {
+		return;
+	}
+	for (int i = 0; i != sort_button->get_popup()->get_item_count(); i++) {
+		// Update the popup buttons to be checked/unchecked.
+		sort_button->get_popup()->set_item_checked(i, (i == (int)current_sort));
+	}
+
+	SortItem sort = sort_items[current_sort];
+
+	List<TreeItem *> items_to_sort;
+	items_to_sort.push_back(managed_tree->get_root());
+
+	while (items_to_sort.size() > 0) {
+		TreeItem *to_sort = items_to_sort.front()->get();
+		items_to_sort.pop_front();
+
+		LocalVector<TreeItemColumn> items;
+		items.reserve(to_sort->get_child_count());
+		for (int i = 0; i < to_sort->get_child_count(); i++) {
+			items.push_back(TreeItemColumn(to_sort->get_child(i), sort.column));
+		}
+
+		if (sort.type == ALPHA_SORT && sort.ascending == true) {
+			items.sort_custom<TreeItemAlphaComparator>();
+		}
+		if (sort.type == ALPHA_SORT && sort.ascending == false) {
+			items.sort_custom<TreeItemAlphaComparator>();
+			items.reverse();
+		}
+		if (sort.type == NUMERIC_SORT && sort.ascending == true) {
+			items.sort_custom<TreeItemNumericComparator>();
+		}
+		if (sort.type == NUMERIC_SORT && sort.ascending == false) {
+			items.sort_custom<TreeItemNumericComparator>();
+			items.reverse();
+		}
+
+		TreeItem *previous = nullptr;
+		for (const TreeItemColumn &item : items) {
+			if (previous != nullptr) {
+				item.item->move_after(previous);
+			} else {
+				item.item->move_before(to_sort->get_first_child());
+			}
+			previous = item.item;
+			items_to_sort.push_back(item.item);
+		}
+	}
+}
+
+void TreeSortAndFilterBar::_sort_changed(int p_id) {
+	current_sort = p_id;
+	_apply_sort();
+}
+
+void TreeSortAndFilterBar::_filter_changed(const String &p_filter) {
+	_apply_filter();
+}
+
+TreeSortAndFilterBar::TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text) :
+		managed_tree(p_managed_tree) {
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	add_theme_constant_override("h_separation", 10 * EDSCALE);
+	filter_edit = memnew(LineEdit);
+	filter_edit->set_clear_button_enabled(true);
+	filter_edit->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	filter_edit->set_placeholder(p_filter_placeholder_text);
+	add_child(filter_edit);
+	filter_edit->connect(SceneStringName(text_changed), callable_mp(this, &TreeSortAndFilterBar::_filter_changed));
+
+	sort_button = memnew(MenuButton);
+	sort_button->set_visible(false);
+	sort_button->set_flat(false);
+	sort_button->set_theme_type_variation("FlatMenuButton");
+	PopupMenu *p = sort_button->get_popup();
+	p->connect(SceneStringName(id_pressed), callable_mp(this, &TreeSortAndFilterBar::_sort_changed));
+
+	add_child(sort_button);
+}
+
+void TreeSortAndFilterBar::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_POSTINITIALIZE:
+		case NOTIFICATION_THEME_CHANGED: {
+			filter_edit->set_right_icon(get_editor_theme_icon(SNAME("Search")));
+			sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
+			apply();
+		} break;
+	}
+}
+
+TreeSortAndFilterBar::SortOptionIndexes TreeSortAndFilterBar::add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default) {
+	sort_button->set_visible(true);
+	bool is_first_item = sort_items.is_empty();
+	SortItem item_ascending(sort_items.size(), vformat(TTRC("Sort By %s (Ascending)"), p_new_option), p_sort_type, true, p_sort_column);
+	sort_items[item_ascending.id] = item_ascending;
+	sort_button->get_popup()->add_radio_check_item(item_ascending.label, item_ascending.id);
+
+	SortItem item_descending(sort_items.size(), vformat(TTRC("Sort By %s (Descending)"), p_new_option), p_sort_type, false, p_sort_column);
+	sort_items[item_descending.id] = item_descending;
+	sort_button->get_popup()->add_radio_check_item(item_descending.label, item_descending.id);
+
+	if (is_first_item) {
+		sort_button->get_popup()->set_item_checked(0, true);
+	}
+
+	SortOptionIndexes indexes;
+	indexes.ascending = item_ascending.id;
+	indexes.descending = item_descending.id;
+	return indexes;
+}
+
+void TreeSortAndFilterBar::clear_filter() {
+	filter_edit->clear();
+}
+
+void TreeSortAndFilterBar::clear() {
+	sort_button->set_visible(false);
+	sort_button->get_popup()->clear();
+	filter_edit->clear();
+}
+
+void TreeSortAndFilterBar::select_sort(int p_item_id) {
+	_sort_changed(p_item_id);
+}
+
+void TreeSortAndFilterBar::apply() {
+	if (!managed_tree || !managed_tree->get_root()) {
+		return;
+	}
+
+	_apply_sort();
+	_apply_filter();
+}

+ 127 - 0
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/panel_container.h"
+#include "scene/gui/tree.h"
+
+class LineEdit;
+class MenuButton;
+
+class SpanningHeader : public PanelContainer {
+	GDCLASS(SpanningHeader, PanelContainer);
+
+public:
+	SpanningHeader(const String &p_text);
+};
+
+class DarkPanelContainer : public PanelContainer {
+	GDCLASS(DarkPanelContainer, PanelContainer);
+
+public:
+	DarkPanelContainer();
+};
+
+// Utility class that creates a filter text box and a sort menu.
+// Takes a reference to a tree and applies the sort and filter to the tree.
+class TreeSortAndFilterBar : public HBoxContainer {
+	GDCLASS(TreeSortAndFilterBar, HBoxContainer);
+
+public:
+	// The ways a column can be sorted, either alphabetically or numerically.
+	enum SortType {
+		NUMERIC_SORT = 0,
+		ALPHA_SORT,
+		SORT_TYPE_MAX
+	};
+
+	// Returned when a new sort is added. Each new sort can be either ascending or descending,
+	// so we return the index of each sort option.
+	struct SortOptionIndexes {
+		int ascending;
+		int descending;
+	};
+
+protected:
+	// Context needed to sort the tree in a certain way.
+	// Combines a sort type, the column to apply it, and if it's ascending or descending.
+	struct SortItem {
+		SortItem() {}
+		SortItem(int p_id, const String &p_label, SortType p_type, bool p_ascending, int p_column) :
+				id(p_id), label(p_label), type(p_type), ascending(p_ascending), column(p_column) {}
+		int id = 0;
+		String label;
+		SortType type = SortType::NUMERIC_SORT;
+		bool ascending = false;
+		int column = 0;
+	};
+
+	struct TreeItemColumn {
+		TreeItemColumn() {}
+		TreeItemColumn(TreeItem *p_item, int p_column) :
+				item(p_item), column(p_column) {}
+		TreeItem *item = nullptr;
+		int column;
+	};
+
+	struct TreeItemAlphaComparator {
+		bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
+			return NoCaseComparator()(p_a.item->get_text(p_a.column), p_b.item->get_text(p_b.column));
+		}
+	};
+
+	struct TreeItemNumericComparator {
+		bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
+			return p_a.item->get_text(p_a.column).to_int() < p_b.item->get_text(p_b.column).to_int();
+		}
+	};
+
+	LineEdit *filter_edit = nullptr;
+	MenuButton *sort_button = nullptr;
+	Tree *managed_tree = nullptr;
+	HashMap<int, SortItem> sort_items;
+	int current_sort = 0;
+
+	void _apply_filter(TreeItem *p_current_node = nullptr);
+	void _apply_sort();
+	void _sort_changed(int p_id);
+	void _filter_changed(const String &p_filter);
+
+public:
+	TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text);
+	void _notification(int p_what);
+	SortOptionIndexes add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default = false);
+	void clear_filter();
+	void clear();
+	void select_sort(int p_item_id);
+	void apply();
+};

+ 68 - 0
modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp

@@ -0,0 +1,68 @@
+/**************************************************************************/
+/*  snapshot_view.cpp                                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "snapshot_view.h"
+
+#include "scene/gui/tree.h"
+
+void SnapshotView::clear_snapshot() {
+	snapshot_data = nullptr;
+	diff_data = nullptr;
+	for (int i = 0; i < get_child_count(); i++) {
+		get_child(i)->queue_free();
+	}
+}
+
+void SnapshotView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	clear_snapshot();
+	snapshot_data = p_data;
+	diff_data = p_diff_data;
+}
+
+bool SnapshotView::is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	return p_data == snapshot_data && p_diff_data == diff_data;
+}
+
+Vector<TreeItem *> SnapshotView::_get_children_recursive(Tree *p_tree) {
+	Vector<TreeItem *> found_items;
+	List<TreeItem *> items_to_check;
+	if (p_tree && p_tree->get_root()) {
+		items_to_check.push_back(p_tree->get_root());
+	}
+	while (items_to_check.size() > 0) {
+		TreeItem *to_check = items_to_check.front()->get();
+		items_to_check.pop_front();
+		found_items.push_back(to_check);
+		for (int i = 0; i < to_check->get_child_count(); i++) {
+			items_to_check.push_back(to_check->get_child(i));
+		}
+	}
+	return found_items;
+}

+ 55 - 0
modules/objectdb_profiler/editor/data_viewers/snapshot_view.h

@@ -0,0 +1,55 @@
+/**************************************************************************/
+/*  snapshot_view.h                                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../snapshot_data.h"
+
+#include "scene/gui/control.h"
+
+class Tree;
+class TreeItem;
+
+class SnapshotView : public Control {
+	GDCLASS(SnapshotView, Control);
+
+protected:
+	GameStateSnapshot *snapshot_data = nullptr;
+	GameStateSnapshot *diff_data = nullptr;
+
+	Vector<TreeItem *> _get_children_recursive(Tree *p_tree);
+
+public:
+	String view_name;
+
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data = nullptr);
+	virtual void clear_snapshot();
+	bool is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data);
+};

+ 284 - 0
modules/objectdb_profiler/editor/data_viewers/summary_view.cpp

@@ -0,0 +1,284 @@
+/**************************************************************************/
+/*  summary_view.cpp                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "summary_view.h"
+
+#include "core/os/time.h"
+#include "editor/editor_node.h"
+#include "scene/gui/center_container.h"
+#include "scene/gui/label.h"
+#include "scene/gui/panel_container.h"
+#include "scene/gui/rich_text_label.h"
+#include "scene/resources/style_box_flat.h"
+
+SnapshotSummaryView::SnapshotSummaryView() {
+	set_name(TTRC("Summary"));
+
+	set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+
+	MarginContainer *mc = memnew(MarginContainer);
+	mc->add_theme_constant_override("margin_left", 5);
+	mc->add_theme_constant_override("margin_right", 5);
+	mc->add_theme_constant_override("margin_top", 5);
+	mc->add_theme_constant_override("margin_bottom", 5);
+	mc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	PanelContainer *content_wrapper = memnew(PanelContainer);
+	content_wrapper->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	Ref<StyleBoxFlat> content_wrapper_sbf;
+	content_wrapper_sbf.instantiate();
+	content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
+	content_wrapper->add_theme_style_override(SceneStringName(panel), content_wrapper_sbf);
+	content_wrapper->add_child(mc);
+	add_child(content_wrapper);
+
+	VBoxContainer *content = memnew(VBoxContainer);
+	mc->add_child(content);
+	content->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	PanelContainer *pc = memnew(PanelContainer);
+	Ref<StyleBoxFlat> sbf;
+	sbf.instantiate();
+	sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
+	pc->add_theme_style_override("panel", sbf);
+	content->add_child(pc);
+	pc->set_anchors_preset(LayoutPreset::PRESET_TOP_WIDE);
+	Label *title = memnew(Label(TTRC("ObjectDB Snapshot Summary")));
+	pc->add_child(title);
+	title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+	title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
+
+	explainer_text = memnew(CenterContainer);
+	explainer_text->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	explainer_text->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	content->add_child(explainer_text);
+	VBoxContainer *explainer_lines = memnew(VBoxContainer);
+	explainer_text->add_child(explainer_lines);
+	Label *l1 = memnew(Label(TTRC("Press 'Take ObjectDB Snapshot' to snapshot the ObjectDB.")));
+	Label *l2 = memnew(Label(TTRC("Memory in Godot is either owned natively by the engine or owned by the ObjectDB.")));
+	Label *l3 = memnew(Label(TTRC("ObjectDB Snapshots capture only memory owned by the ObjectDB.")));
+	l1->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+	l2->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+	l3->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+	explainer_lines->add_child(l1);
+	explainer_lines->add_child(l2);
+	explainer_lines->add_child(l3);
+
+	ScrollContainer *sc = memnew(ScrollContainer);
+	sc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	sc->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	sc->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	content->add_child(sc);
+
+	blurb_list = memnew(VBoxContainer);
+	sc->add_child(blurb_list);
+	blurb_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	blurb_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+}
+
+void SnapshotSummaryView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+	explainer_text->set_visible(false);
+
+	String snapshot_a_name = diff_data == nullptr ? TTRC("Snapshot") : TTRC("Snapshot A");
+	String snapshot_b_name = TTRC("Snapshot B");
+
+	_push_overview_blurb(snapshot_a_name + " " + TTRC("Overview"), snapshot_data);
+	if (diff_data) {
+		_push_overview_blurb(snapshot_b_name + " " + TTRC("Overview"), diff_data);
+	}
+
+	_push_node_blurb(snapshot_a_name + " " + TTRC("Nodes"), snapshot_data);
+	if (diff_data) {
+		_push_node_blurb(snapshot_b_name + " " + TTRC("Nodes"), diff_data);
+	}
+
+	_push_refcounted_blurb(snapshot_a_name + " " + TTRC("RefCounteds"), snapshot_data);
+	if (diff_data) {
+		_push_refcounted_blurb(snapshot_b_name + " " + TTRC("RefCounteds"), diff_data);
+	}
+
+	_push_object_blurb(snapshot_a_name + " " + TTRC("Objects"), snapshot_data);
+	if (diff_data) {
+		_push_object_blurb(snapshot_b_name + " " + TTRC("Objects"), diff_data);
+	}
+}
+
+void SnapshotSummaryView::clear_snapshot() {
+	// Just clear out the blurbs and leave the explainer.
+	for (int i = 0; i < blurb_list->get_child_count(); i++) {
+		blurb_list->get_child(i)->queue_free();
+	}
+	snapshot_data = nullptr;
+	diff_data = nullptr;
+	explainer_text->set_visible(true);
+}
+
+SummaryBlurb::SummaryBlurb(const String &p_title, const String &p_rtl_content) {
+	add_theme_constant_override("margin_left", 2);
+	add_theme_constant_override("margin_right", 2);
+	add_theme_constant_override("margin_top", 2);
+	add_theme_constant_override("margin_bottom", 2);
+
+	label = memnew(RichTextLabel);
+	label->add_theme_constant_override(SceneStringName(line_separation), 6);
+	label->set_text_direction(Control::TEXT_DIRECTION_INHERITED);
+	label->set_fit_content(true);
+	label->set_use_bbcode(true);
+	label->add_newline();
+	label->push_bold();
+	label->add_text(p_title);
+	label->pop();
+	label->add_newline();
+	label->add_newline();
+	label->append_text(p_rtl_content);
+	add_child(label);
+}
+
+void SnapshotSummaryView::_push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
+	String c = "";
+
+	c += "[ul]\n";
+	c += vformat(" [i]%s[/i] %s\n", TTR("Name:"), p_snapshot->name);
+	if (p_snapshot->snapshot_context.has("timestamp")) {
+		c += vformat(" [i]%s[/i] %s\n", TTR("Timestamp:"), Time::get_singleton()->get_datetime_string_from_unix_time((double)p_snapshot->snapshot_context["timestamp"]));
+	}
+	if (p_snapshot->snapshot_context.has("game_version")) {
+		c += vformat(" [i]%s[/i] %s\n", TTR("Game Version:"), (String)p_snapshot->snapshot_context["game_version"]);
+	}
+	if (p_snapshot->snapshot_context.has("editor_version")) {
+		c += vformat(" [i]%s[/i] %s\n", TTR("Editor Version:"), (String)p_snapshot->snapshot_context["editor_version"]);
+	}
+
+	double bytes_to_mb = 0.000001;
+	if (p_snapshot->snapshot_context.has("mem_usage")) {
+		c += vformat(" [i]%s[/i] %s\n", TTR("Memory Used:"), String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_usage"]) * bytes_to_mb, 3) + " MB");
+	}
+	if (p_snapshot->snapshot_context.has("mem_max_usage")) {
+		c += vformat(" [i]%s[/i] %s\n", TTR("Max Memory Used:"), String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_max_usage"]) * bytes_to_mb, 3) + " MB");
+	}
+	c += vformat(" [i]%s[/i] %s\n", TTR("Total Objects:"), itos(p_snapshot->objects.size()));
+
+	int node_count = 0;
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		if (pair.value->is_node()) {
+			node_count++;
+		}
+	}
+	c += vformat(" [i]%s[/i] %s\n", TTR("Total Nodes:"), itos(node_count));
+	c += "[/ul]\n";
+
+	blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
+}
+
+void SnapshotSummaryView::_push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
+	LocalVector<String> nodes;
+	nodes.reserve(p_snapshot->objects.size());
+
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		// if it's a node AND it doesn't have a parent node
+		if (pair.value->is_node() && !pair.value->extra_debug_data.has("node_parent") && pair.value->extra_debug_data.has("node_is_scene_root") && !pair.value->extra_debug_data["node_is_scene_root"]) {
+			String node_name = pair.value->extra_debug_data["node_name"];
+			nodes.push_back(node_name != "" ? node_name : pair.value->get_name());
+		}
+	}
+
+	if (nodes.size() <= 1) {
+		return;
+	}
+
+	String c = TTRC("Multiple root nodes [i](possible call to 'remove_child' without 'queue_free')[/i]\n");
+	c += "[ul]\n";
+	for (const String &node : nodes) {
+		c += " " + node + "\n";
+	}
+	c += "[/ul]\n";
+
+	blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
+}
+
+void SnapshotSummaryView::_push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
+	LocalVector<String> rcs;
+	rcs.reserve(p_snapshot->objects.size());
+
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		if (pair.value->is_refcounted()) {
+			int ref_count = (uint64_t)pair.value->extra_debug_data["ref_count"];
+			Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
+
+			if (ref_count == ref_cycles.size()) {
+				rcs.push_back(pair.value->get_name());
+			}
+		}
+	}
+
+	if (rcs.is_empty()) {
+		return;
+	}
+
+	String c = TTRC("RefCounted objects only referenced in cycles [i](cycles often indicate a memory leaks)[/i]\n");
+	c += "[ul]\n";
+	for (const String &rc : rcs) {
+		c += " " + rc + "\n";
+	}
+	c += "[/ul]\n";
+
+	blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
+}
+
+void SnapshotSummaryView::_push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
+	LocalVector<String> objects;
+	objects.reserve(p_snapshot->objects.size());
+
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
+		if (pair.value->inbound_references.is_empty() && pair.value->outbound_references.is_empty()) {
+			if (!pair.value->get_script().is_null()) {
+				// This blurb will have a lot of false positives, but we can at least suppress false positives
+				// from unreferenced nodes that are part of the scene tree.
+				if (pair.value->is_node() && (bool)pair.value->extra_debug_data["node_is_scene_root"]) {
+					objects.push_back(pair.value->get_name());
+				}
+			}
+		}
+	}
+
+	if (objects.is_empty()) {
+		return;
+	}
+
+	String c = TTRC("Scripted objects not referenced by any other objects [i](unreferenced objects may indicate a memory leak)[/i]\n");
+	c += "[ul]\n";
+	for (const String &object : objects) {
+		c += " " + object + "\n";
+	}
+	c += "[/ul]\n";
+
+	blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
+}

+ 67 - 0
modules/objectdb_profiler/editor/data_viewers/summary_view.h

@@ -0,0 +1,67 @@
+/**************************************************************************/
+/*  summary_view.h                                                        */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../snapshot_data.h"
+#include "snapshot_view.h"
+
+#include "scene/gui/margin_container.h"
+
+class CenterContainer;
+class RichTextLabel;
+
+class SummaryBlurb : public MarginContainer {
+	GDCLASS(SummaryBlurb, MarginContainer);
+
+public:
+	RichTextLabel *label = nullptr;
+
+	SummaryBlurb(const String &p_title, const String &p_rtl_content);
+};
+
+class SnapshotSummaryView : public SnapshotView {
+	GDCLASS(SnapshotSummaryView, SnapshotView);
+
+protected:
+	VBoxContainer *blurb_list = nullptr;
+	CenterContainer *explainer_text = nullptr;
+
+	void _push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
+	void _push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
+	void _push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
+	void _push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
+
+public:
+	SnapshotSummaryView();
+
+	virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
+	virtual void clear_snapshot() override;
+};

+ 464 - 0
modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp

@@ -0,0 +1,464 @@
+/**************************************************************************/
+/*  objectdb_profiler_panel.cpp                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "objectdb_profiler_panel.h"
+
+#include "../snapshot_collector.h"
+#include "data_viewers/class_view.h"
+#include "data_viewers/node_view.h"
+#include "data_viewers/object_view.h"
+#include "data_viewers/refcounted_view.h"
+#include "data_viewers/summary_view.h"
+
+#include "core/config/project_settings.h"
+#include "core/os/time.h"
+#include "editor/debugger/editor_debugger_node.h"
+#include "editor/debugger/script_editor_debugger.h"
+#include "editor/editor_node.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/split_container.h"
+#include "scene/gui/tab_container.h"
+
+// ObjectDB snapshots are very large. In remote_debugger_peer.cpp, the max in_buf and out_buf size is 8mb.
+// Snapshots are typically larger than that, so we send them 6mb at a time. Leaving 2mb for other data.
+const int SNAPSHOT_CHUNK_SIZE = 6 << 20;
+
+void ObjectDBProfilerPanel::_request_object_snapshot() {
+	take_snapshot->set_disabled(true);
+	take_snapshot->set_text(TTRC("Generating Snapshot"));
+	// Pause the game while the snapshot is taken so the state of the game isn't modified as we capture the snapshot.
+	if (EditorDebuggerNode::get_singleton()->get_current_debugger()->is_breaked()) {
+		requested_break_for_snapshot = false;
+		_begin_object_snapshot();
+	} else {
+		awaiting_debug_break = true;
+		requested_break_for_snapshot = true; // We only need to resume the game if we are the ones who paused it.
+		EditorDebuggerNode::get_singleton()->debug_break();
+	}
+}
+
+void ObjectDBProfilerPanel::_on_debug_breaked(bool p_reallydid, bool p_can_debug, const String &p_reason, bool p_has_stackdump) {
+	if (p_reallydid && awaiting_debug_break) {
+		awaiting_debug_break = false;
+		_begin_object_snapshot();
+	}
+}
+
+void ObjectDBProfilerPanel::_begin_object_snapshot() {
+	Array args = { next_request_id++, SnapshotCollector::get_godot_version_string() };
+	EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_prepare_snapshot", args);
+}
+
+bool ObjectDBProfilerPanel::handle_debug_message(const String &p_message, const Array &p_data, int p_index) {
+	if (p_message == "snapshot:snapshot_prepared") {
+		int request_id = p_data[0];
+		int total_size = p_data[1];
+		partial_snapshots[request_id] = PartialSnapshot();
+		partial_snapshots[request_id].total_size = total_size;
+		Array args = { request_id, 0, SNAPSHOT_CHUNK_SIZE };
+		take_snapshot->set_text(vformat(TTRC("Receiving Snapshot (0/%s MiB)"), _to_mb(total_size)));
+		EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);
+		return true;
+	}
+	if (p_message == "snapshot:snapshot_chunk") {
+		int request_id = p_data[0];
+		PartialSnapshot &chunk = partial_snapshots[request_id];
+		chunk.data.append_array(p_data[1]);
+		take_snapshot->set_text(vformat(TTRC("Receiving Snapshot (%s/%s MiB)"), _to_mb(chunk.data.size()), _to_mb(chunk.total_size)));
+		if (chunk.data.size() != chunk.total_size) {
+			Array args = { request_id, chunk.data.size(), chunk.data.size() + SNAPSHOT_CHUNK_SIZE };
+			EditorDebuggerNode::get_singleton()->get_current_debugger()->send_message("snapshot:request_snapshot_chunk", args);
+			return true;
+		}
+
+		take_snapshot->set_text(TTRC("Visualizing Snapshot"));
+		// Wait a frame just so the button has a chance to update its text so the user knows what's going on.
+		get_tree()->connect("process_frame", callable_mp(this, &ObjectDBProfilerPanel::receive_snapshot).bind(request_id), CONNECT_ONE_SHOT);
+		return true;
+	}
+	return false;
+}
+
+void ObjectDBProfilerPanel::receive_snapshot(int request_id) {
+	const Vector<uint8_t> &in_data = partial_snapshots[request_id].data;
+	String snapshot_file_name = Time::get_singleton()->get_datetime_string_from_system(false).replace_char('T', '_').replace_char(':', '-');
+	Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
+	if (snapshot_dir.is_valid()) {
+		Error err;
+		String current_dir = snapshot_dir->get_current_dir();
+		String joined_dir = current_dir.path_join(snapshot_file_name) + ".odb_snapshot";
+
+		Ref<FileAccess> file = FileAccess::open(joined_dir, FileAccess::WRITE, &err);
+		if (err == OK) {
+			file->store_buffer(in_data);
+			file->close(); // RAII could do this typically, but we want to read the file in _show_selected_snapshot, so we have to finalize the write before that.
+
+			_add_snapshot_button(snapshot_file_name, joined_dir);
+			snapshot_list->deselect_all();
+			snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());
+			snapshot_list->ensure_cursor_is_visible();
+			_show_selected_snapshot();
+		} else {
+			ERR_PRINT("Could not persist ObjectDB Snapshot: " + String(error_names[err]));
+		}
+	}
+	partial_snapshots.erase(request_id);
+	if (requested_break_for_snapshot) {
+		EditorDebuggerNode::get_singleton()->debug_continue();
+	}
+	take_snapshot->set_disabled(false);
+	take_snapshot->set_text("Take ObjectDB Snapshot");
+}
+
+Ref<DirAccess> ObjectDBProfilerPanel::_get_and_create_snapshot_storage_dir() {
+	String profiles_dir = "user://";
+	Ref<DirAccess> da = DirAccess::open(profiles_dir);
+	ERR_FAIL_COND_V_MSG(da.is_null(), nullptr, vformat("Could not open 'user://' directory: '%s'.", profiles_dir));
+	Error err = da->change_dir("objectdb_snapshots");
+	if (err != OK) {
+		Error err_mk = da->make_dir("objectdb_snapshots");
+		Error err_ch = da->change_dir("objectdb_snapshots");
+		ERR_FAIL_COND_V_MSG(err_mk != OK || err_ch != OK, nullptr, "Could not create ObjectDB Snapshots directory: " + da->get_current_dir());
+	}
+	return da;
+}
+
+TreeItem *ObjectDBProfilerPanel::_add_snapshot_button(const String &p_snapshot_file_name, const String &p_full_file_path) {
+	TreeItem *item = snapshot_list->create_item(snapshot_list->get_root());
+	item->set_text(0, p_snapshot_file_name);
+	item->set_metadata(0, p_full_file_path);
+	item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
+	item->move_before(snapshot_list->get_root()->get_first_child());
+	_update_diff_items();
+	_update_enabled_diff_items();
+	return item;
+}
+
+void ObjectDBProfilerPanel::_show_selected_snapshot() {
+	if (snapshot_list->get_selected()->get_text(0) == (String)diff_button->get_selected_metadata()) {
+		for (int i = 0; i < diff_button->get_item_count(); i++) {
+			if (diff_button->get_item_text(i) == current_snapshot->get_snapshot()->name) {
+				diff_button->select(i);
+				break;
+			}
+		}
+	}
+	show_snapshot(snapshot_list->get_selected()->get_text(0), diff_button->get_selected_metadata());
+	_update_enabled_diff_items();
+}
+
+void ObjectDBProfilerPanel::_on_snapshot_deselected() {
+	snapshot_list->deselect_all();
+	diff_button->select(0);
+	clear_snapshot();
+	_update_enabled_diff_items();
+}
+
+Ref<GameStateSnapshotRef> ObjectDBProfilerPanel::get_snapshot(const String &p_snapshot_file_name) {
+	if (snapshot_cache.has(p_snapshot_file_name)) {
+		return snapshot_cache.get(p_snapshot_file_name);
+	}
+
+	Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
+	ERR_FAIL_COND_V_MSG(snapshot_dir.is_null(), nullptr, "Could not access ObjectDB Snapshot directory");
+
+	String full_file_path = snapshot_dir->get_current_dir().path_join(p_snapshot_file_name) + ".odb_snapshot";
+
+	Error err;
+	Ref<FileAccess> snapshot_file = FileAccess::open(full_file_path, FileAccess::READ, &err);
+	ERR_FAIL_COND_V_MSG(err != OK, nullptr, "Could not open ObjectDB Snapshot file: " + full_file_path);
+
+	Vector<uint8_t> content = snapshot_file->get_buffer(snapshot_file->get_length()); // We want to split on newlines, so normalize them.
+	ERR_FAIL_COND_V_MSG(content.is_empty(), nullptr, "ObjectDB Snapshot file is empty: " + full_file_path);
+
+	Ref<GameStateSnapshotRef> snapshot = GameStateSnapshot::create_ref(p_snapshot_file_name, content);
+	if (snapshot.is_valid()) {
+		snapshot_cache.insert(p_snapshot_file_name, snapshot);
+	}
+
+	return snapshot;
+}
+
+void ObjectDBProfilerPanel::show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name) {
+	clear_snapshot(false);
+
+	current_snapshot = get_snapshot(p_snapshot_file_name);
+	if (!p_snapshot_diff_file_name.is_empty()) {
+		diff_snapshot = get_snapshot(p_snapshot_diff_file_name);
+	}
+
+	_update_view_tabs();
+	_view_tab_changed(view_tabs->get_current_tab());
+}
+
+void ObjectDBProfilerPanel::_view_tab_changed(int p_tab_idx) {
+	// Populating tabs only on tab changed because we're handling a lot of data,
+	// and the editor freezes for a while if we try to populate every tab at once.
+	SnapshotView *view = cast_to<SnapshotView>(view_tabs->get_current_tab_control());
+	GameStateSnapshot *snapshot = current_snapshot.is_null() ? nullptr : current_snapshot->get_snapshot();
+	GameStateSnapshot *diff = diff_snapshot.is_null() ? nullptr : diff_snapshot->get_snapshot();
+	if (snapshot != nullptr && !view->is_showing_snapshot(snapshot, diff)) {
+		view->show_snapshot(snapshot, diff);
+	}
+}
+
+void ObjectDBProfilerPanel::clear_snapshot(bool p_update_view_tabs) {
+	for (SnapshotView *view : views) {
+		view->clear_snapshot();
+	}
+
+	current_snapshot.unref();
+	diff_snapshot.unref();
+
+	if (p_update_view_tabs) {
+		_update_view_tabs();
+	}
+}
+
+void ObjectDBProfilerPanel::set_enabled(bool p_enabled) {
+	take_snapshot->set_text(TTRC("Take ObjectDB Snapshot"));
+	take_snapshot->set_disabled(!p_enabled);
+}
+
+void ObjectDBProfilerPanel::_snapshot_rmb(const Vector2 &p_pos, MouseButton p_button) {
+	if (p_button != MouseButton::RIGHT) {
+		return;
+	}
+	rmb_menu->clear(false);
+
+	rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Rename")), TTRC("Rename"), OdbProfilerMenuOptions::ODB_MENU_RENAME);
+	rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("Show in File Manager"), OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER);
+	rmb_menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTRC("Delete"), OdbProfilerMenuOptions::ODB_MENU_DELETE);
+
+	rmb_menu->set_position(snapshot_list->get_screen_position() + p_pos);
+	rmb_menu->reset_size();
+	rmb_menu->popup();
+}
+
+void ObjectDBProfilerPanel::_rmb_menu_pressed(int p_tool, bool p_confirm_override) {
+	String file_path = snapshot_list->get_selected()->get_metadata(0);
+	String global_path = ProjectSettings::get_singleton()->globalize_path(file_path);
+	switch (rmb_menu->get_item_id(p_tool)) {
+		case OdbProfilerMenuOptions::ODB_MENU_SHOW_IN_FOLDER: {
+			OS::get_singleton()->shell_show_in_file_manager(global_path, true);
+			break;
+		}
+		case OdbProfilerMenuOptions::ODB_MENU_DELETE: {
+			DirAccess::remove_file_or_error(global_path);
+			snapshot_list->get_root()->remove_child(snapshot_list->get_selected());
+			if (snapshot_list->get_root()->get_child_count() > 0) {
+				snapshot_list->set_selected(snapshot_list->get_root()->get_first_child());
+			} else {
+				// If we deleted the last snapshot, jump back to the summary tab and clear everything out.
+				clear_snapshot();
+			}
+			_update_diff_items();
+			break;
+		}
+		case OdbProfilerMenuOptions::ODB_MENU_RENAME: {
+			snapshot_list->edit_selected(true);
+			break;
+		}
+	}
+}
+
+void ObjectDBProfilerPanel::_edit_snapshot_name() {
+	String new_snapshot_name = snapshot_list->get_selected()->get_text(0);
+	String full_file_with_path = snapshot_list->get_selected()->get_metadata(0);
+	Vector<String> full_path_parts = full_file_with_path.rsplit("/", false, 1);
+	String full_file_path = full_path_parts[0];
+	String file_name = full_path_parts[1];
+	String old_snapshot_name = file_name.split(".")[0];
+	String new_full_file_path = full_file_path.path_join(new_snapshot_name) + ".odb_snapshot";
+
+	bool name_taken = false;
+	for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {
+		TreeItem *item = snapshot_list->get_root()->get_child(i);
+		if (item != snapshot_list->get_selected()) {
+			if (item->get_text(0) == new_snapshot_name) {
+				name_taken = true;
+				break;
+			}
+		}
+	}
+
+	if (name_taken || new_snapshot_name.contains_char(':') || new_snapshot_name.contains_char('\\') || new_snapshot_name.contains_char('/') || new_snapshot_name.begins_with(".") || new_snapshot_name.is_empty()) {
+		EditorNode::get_singleton()->show_warning(TTRC("Invalid snapshot name."));
+		snapshot_list->get_selected()->set_text(0, old_snapshot_name);
+		return;
+	}
+
+	Error err = DirAccess::rename_absolute(full_file_with_path, new_full_file_path);
+	if (err != OK) {
+		EditorNode::get_singleton()->show_warning(TTRC("Snapshot rename failed"));
+		snapshot_list->get_selected()->set_text(0, old_snapshot_name);
+	} else {
+		snapshot_list->get_selected()->set_metadata(0, new_full_file_path);
+	}
+
+	_update_diff_items();
+	_show_selected_snapshot();
+}
+
+ObjectDBProfilerPanel::ObjectDBProfilerPanel() {
+	set_name(TTRC("ObjectDB Profiler"));
+
+	snapshot_cache = LRUCache<String, Ref<GameStateSnapshotRef>>(SNAPSHOT_CACHE_MAX_SIZE);
+
+	EditorDebuggerNode::get_singleton()->get_current_debugger()->connect("breaked", callable_mp(this, &ObjectDBProfilerPanel::_on_debug_breaked));
+
+	HSplitContainer *root_container = memnew(HSplitContainer);
+	root_container->set_anchors_preset(Control::LayoutPreset::PRESET_FULL_RECT);
+	root_container->set_v_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);
+	root_container->set_h_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL);
+	root_container->set_split_offset(300 * EDSCALE);
+	add_child(root_container);
+
+	VBoxContainer *snapshot_column = memnew(VBoxContainer);
+	root_container->add_child(snapshot_column);
+
+	take_snapshot = memnew(Button(TTRC("Take ObjectDB Snapshot")));
+	snapshot_column->add_child(take_snapshot);
+	take_snapshot->connect(SceneStringName(pressed), callable_mp(this, &ObjectDBProfilerPanel::_request_object_snapshot));
+
+	snapshot_list = memnew(Tree);
+	snapshot_list->create_item();
+	snapshot_list->set_hide_folding(true);
+	snapshot_column->add_child(snapshot_list);
+	snapshot_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
+	snapshot_list->set_hide_root(true);
+	snapshot_list->set_columns(1);
+	snapshot_list->set_column_titles_visible(true);
+	snapshot_list->set_column_title(0, "Snapshots");
+	snapshot_list->set_column_expand(0, true);
+	snapshot_list->set_column_clip_content(0, true);
+	snapshot_list->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot));
+	snapshot_list->connect("nothing_selected", callable_mp(this, &ObjectDBProfilerPanel::_on_snapshot_deselected));
+	snapshot_list->connect("item_edited", callable_mp(this, &ObjectDBProfilerPanel::_edit_snapshot_name));
+	snapshot_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	snapshot_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	snapshot_list->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	snapshot_list->set_allow_rmb_select(true);
+	snapshot_list->connect("item_mouse_selected", callable_mp(this, &ObjectDBProfilerPanel::_snapshot_rmb));
+
+	rmb_menu = memnew(PopupMenu);
+	add_child(rmb_menu);
+	rmb_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ObjectDBProfilerPanel::_rmb_menu_pressed).bind(false));
+
+	HBoxContainer *diff_button_and_label = memnew(HBoxContainer);
+	diff_button_and_label->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	snapshot_column->add_child(diff_button_and_label);
+	Label *diff_against = memnew(Label(TTRC("Diff Against:")));
+	diff_button_and_label->add_child(diff_against);
+
+	diff_button = memnew(OptionButton);
+	diff_button->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	diff_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+	diff_button->connect(SceneStringName(item_selected), callable_mp(this, &ObjectDBProfilerPanel::_show_selected_snapshot).unbind(1));
+	diff_button_and_label->add_child(diff_button);
+
+	// Tabs of various views right for each snapshot.
+	view_tabs = memnew(TabContainer);
+	root_container->add_child(view_tabs);
+	view_tabs->set_custom_minimum_size(Size2(300 * EDSCALE, 0));
+	view_tabs->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	view_tabs->connect("tab_changed", callable_mp(this, &ObjectDBProfilerPanel::_view_tab_changed));
+
+	add_view(memnew(SnapshotSummaryView));
+	add_view(memnew(SnapshotClassView));
+	add_view(memnew(SnapshotObjectView));
+	add_view(memnew(SnapshotNodeView));
+	add_view(memnew(SnapshotRefCountedView));
+
+	set_enabled(false);
+
+	// Load all the snapshot names from disk.
+	Ref<DirAccess> snapshot_dir = _get_and_create_snapshot_storage_dir();
+	if (snapshot_dir.is_valid()) {
+		for (const String &file_name : snapshot_dir->get_files()) {
+			Vector<String> name_parts = file_name.split(".");
+			ERR_CONTINUE_MSG(name_parts.size() != 2 || name_parts[1] != "odb_snapshot", "ObjectDB snapshot file did not have .odb_snapshot extension. Skipping: " + file_name);
+			_add_snapshot_button(name_parts[0], snapshot_dir->get_current_dir().path_join(file_name));
+		}
+	}
+}
+
+void ObjectDBProfilerPanel::add_view(SnapshotView *p_to_add) {
+	views.push_back(p_to_add);
+	view_tabs->add_child(p_to_add);
+	_update_view_tabs();
+}
+
+void ObjectDBProfilerPanel::_update_view_tabs() {
+	bool has_snapshot = current_snapshot.is_valid();
+	for (int i = 1; i < view_tabs->get_tab_count(); i++) {
+		view_tabs->set_tab_disabled(i, !has_snapshot);
+	}
+
+	if (!has_snapshot) {
+		view_tabs->set_current_tab(0);
+	}
+}
+
+void ObjectDBProfilerPanel::_update_diff_items() {
+	diff_button->clear();
+	diff_button->add_item(TTRC("None"), 0);
+	diff_button->set_item_metadata(0, String());
+	diff_button->set_item_auto_translate_mode(0, Node::AUTO_TRANSLATE_MODE_ALWAYS);
+
+	for (int i = 0; i < snapshot_list->get_root()->get_child_count(); i++) {
+		String name = snapshot_list->get_root()->get_child(i)->get_text(0);
+		diff_button->add_item(name);
+		diff_button->set_item_metadata(i + 1, name);
+	}
+}
+
+void ObjectDBProfilerPanel::_update_enabled_diff_items() {
+	TreeItem *selected_snapshot = snapshot_list->get_selected();
+	if (selected_snapshot == nullptr) {
+		diff_button->set_disabled(true);
+		return;
+	}
+
+	diff_button->set_disabled(false);
+
+	String snapshot_name = selected_snapshot->get_text(0);
+	for (int i = 0; i < diff_button->get_item_count(); i++) {
+		diff_button->set_item_disabled(i, diff_button->get_item_text(i) == snapshot_name);
+	}
+}
+
+String ObjectDBProfilerPanel::_to_mb(int p_x) {
+	return String::num((double)p_x / (double)(1 << 20), 2);
+}

+ 103 - 0
modules/objectdb_profiler/editor/objectdb_profiler_panel.h

@@ -0,0 +1,103 @@
+/**************************************************************************/
+/*  objectdb_profiler_panel.h                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "data_viewers/snapshot_view.h"
+#include "snapshot_data.h"
+
+#include "core/io/dir_access.h"
+#include "core/templates/lru.h"
+
+class TabContainer;
+class Tree;
+
+// UI loaded by the debugger.
+class ObjectDBProfilerPanel : public Control {
+	GDCLASS(ObjectDBProfilerPanel, Control);
+
+protected:
+	static constexpr int SNAPSHOT_CACHE_MAX_SIZE = 10;
+
+	enum OdbProfilerMenuOptions {
+		ODB_MENU_RENAME,
+		ODB_MENU_SHOW_IN_FOLDER,
+		ODB_MENU_DELETE,
+	};
+
+	struct PartialSnapshot {
+		int total_size;
+		Vector<uint8_t> data;
+	};
+
+	int next_request_id = 0;
+	bool awaiting_debug_break = false;
+	bool requested_break_for_snapshot = false;
+
+	Tree *snapshot_list = nullptr;
+	Button *take_snapshot = nullptr;
+	TabContainer *view_tabs = nullptr;
+	PopupMenu *rmb_menu = nullptr;
+	OptionButton *diff_button = nullptr;
+	HashMap<int, PartialSnapshot> partial_snapshots;
+
+	LocalVector<SnapshotView *> views;
+	Ref<GameStateSnapshotRef> current_snapshot;
+	Ref<GameStateSnapshotRef> diff_snapshot;
+	LRUCache<String, Ref<GameStateSnapshotRef>> snapshot_cache;
+
+	void _request_object_snapshot();
+	void _begin_object_snapshot();
+	void _on_debug_breaked(bool p_reallydid, bool p_can_debug, const String &p_reason, bool p_has_stackdump);
+	void _show_selected_snapshot();
+	void _on_snapshot_deselected();
+	Ref<DirAccess> _get_and_create_snapshot_storage_dir();
+	TreeItem *_add_snapshot_button(const String &p_snapshot_file_name, const String &p_full_file_path);
+	void _snapshot_rmb(const Vector2 &p_pos, MouseButton p_button);
+	void _rmb_menu_pressed(int p_tool, bool p_confirm_override);
+	void _update_view_tabs();
+	void _update_diff_items();
+	void _update_enabled_diff_items();
+	void _edit_snapshot_name();
+	void _view_tab_changed(int p_tab_idx);
+	String _to_mb(int p_x);
+
+public:
+	ObjectDBProfilerPanel();
+
+	void receive_snapshot(int p_request_id);
+	void show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name);
+	void clear_snapshot(bool p_update_view_tabs = true);
+	Ref<GameStateSnapshotRef> get_snapshot(const String &p_snapshot_file_name);
+	void set_enabled(bool p_enabled);
+	void add_view(SnapshotView *p_to_add);
+
+	bool handle_debug_message(const String &p_message, const Array &p_data, int p_index);
+};

+ 66 - 0
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<EditorDebuggerSession> session = get_session(p_session_id);
+	ERR_FAIL_COND(session.is_null());
+	debugger_panel = memnew(ObjectDBProfilerPanel);
+	session->connect("started", callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(true));
+	session->connect("stopped", callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(false));
+	session->add_session_tab(debugger_panel);
+}
+
+ObjectDBProfilerPlugin::ObjectDBProfilerPlugin() {
+	debugger.instantiate();
+}
+
+void ObjectDBProfilerPlugin::_notification(int p_what) {
+	switch (p_what) {
+		case Node::NOTIFICATION_ENTER_TREE: {
+			add_debugger_plugin(debugger);
+		} break;
+		case Node::NOTIFICATION_EXIT_TREE: {
+			remove_debugger_plugin(debugger);
+		}
+	}
+}

+ 65 - 0
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/debugger/editor_debugger_plugin.h"
+#include "editor/plugins/editor_plugin.h"
+
+class ObjectDBProfilerPanel;
+class ObjectDBProfilerDebuggerPlugin;
+
+// First, ObjectDBProfilerPlugin is loaded. Then it loads ObjectDBProfilerDebuggerPlugin.
+class ObjectDBProfilerPlugin : public EditorPlugin {
+	GDCLASS(ObjectDBProfilerPlugin, EditorPlugin);
+
+protected:
+	Ref<ObjectDBProfilerDebuggerPlugin> debugger;
+	void _notification(int p_what);
+
+public:
+	ObjectDBProfilerPlugin();
+};
+
+class ObjectDBProfilerDebuggerPlugin : public EditorDebuggerPlugin {
+	GDCLASS(ObjectDBProfilerDebuggerPlugin, EditorDebuggerPlugin);
+
+protected:
+	ObjectDBProfilerPanel *debugger_panel = nullptr;
+
+	void _request_object_snapshot(int p_request_id);
+
+public:
+	ObjectDBProfilerDebuggerPlugin() {}
+
+	virtual bool has_capture(const String &p_capture) const override;
+	virtual bool capture(const String &p_message, const Array &p_data, int p_index) override;
+	virtual void setup_session(int p_session_id) override;
+};

+ 374 - 0
modules/objectdb_profiler/editor/snapshot_data.cpp

@@ -0,0 +1,374 @@
+/**************************************************************************/
+/*  snapshot_data.cpp                                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "snapshot_data.h"
+
+#include "core/core_bind.h"
+#include "core/object/script_language.h"
+#include "scene/debugger/scene_debugger.h"
+
+#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED)
+#include "modules/gdscript/gdscript.h"
+#endif
+
+SnapshotDataObject::SnapshotDataObject(SceneDebuggerObject &p_obj, GameStateSnapshot *p_snapshot, ResourceCache &resource_cache) :
+		snapshot(p_snapshot) {
+	remote_object_id = p_obj.id;
+	type_name = p_obj.class_name;
+
+	for (const SceneDebuggerObject::SceneDebuggerProperty &prop : p_obj.properties) {
+		PropertyInfo pinfo = prop.first;
+		Variant pvalue = prop.second;
+
+		if (pinfo.type == Variant::OBJECT && pvalue.is_string()) {
+			String path = pvalue;
+			// If a resource is followed by a ::, it is a nested resource (like a sub_resource in a .tscn file).
+			// To get a reference to it, first we load the parent resource (the .tscn, for example), then,
+			// we load the child resource. The parent resource (dependency) should not be destroyed before the child
+			// resource (pvalue) is loaded.
+			if (path.is_resource_file()) {
+				// Built-in resource.
+				String base_path = path.get_slice("::", 0);
+				if (!resource_cache.cache.has(base_path)) {
+					resource_cache.cache[base_path] = ResourceLoader::load(base_path);
+					resource_cache.misses++;
+				} else {
+					resource_cache.hits++;
+				}
+			}
+			if (!resource_cache.cache.has(path)) {
+				resource_cache.cache[path] = ResourceLoader::load(path);
+				resource_cache.misses++;
+			} else {
+				resource_cache.hits++;
+			}
+			pvalue = resource_cache.cache[path];
+
+			if (pinfo.hint_string == "Script") {
+				if (get_script() != pvalue) {
+					set_script(Ref<RefCounted>());
+					Ref<Script> scr(pvalue);
+					if (scr.is_valid()) {
+						ScriptInstance *scr_instance = scr->placeholder_instance_create(this);
+						if (scr_instance) {
+							set_script_and_instance(pvalue, scr_instance);
+						}
+					}
+				}
+			}
+		}
+		prop_list.push_back(pinfo);
+		prop_values[pinfo.name] = pvalue;
+	}
+}
+
+bool SnapshotDataObject::_get(const StringName &p_name, Variant &r_ret) const {
+	String name = p_name;
+
+	if (name.begins_with("Metadata/")) {
+		name = name.replace_first("Metadata/", "metadata/");
+	}
+	if (!prop_values.has(name)) {
+		return false;
+	}
+
+	r_ret = prop_values[p_name];
+	return true;
+}
+
+void SnapshotDataObject::_get_property_list(List<PropertyInfo> *p_list) const {
+	p_list->clear(); // Sorry, don't want any categories.
+	for (const PropertyInfo &prop : prop_list) {
+		if (prop.name == "script") {
+			// Skip the script property, it's always added by the non-virtual method.
+			continue;
+		}
+
+		p_list->push_back(prop);
+	}
+}
+
+void SnapshotDataObject::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("_is_read_only"), &SnapshotDataObject::_is_read_only);
+}
+
+String SnapshotDataObject::get_node_path() {
+	if (!is_node()) {
+		return "";
+	}
+	SnapshotDataObject *current = this;
+	String path;
+
+	while (true) {
+		String current_node_name = current->extra_debug_data["node_name"];
+		if (current_node_name != "") {
+			if (path != "") {
+				path = current_node_name + "/" + path;
+			} else {
+				path = current_node_name;
+			}
+		}
+		if (!current->extra_debug_data.has("node_parent")) {
+			break;
+		}
+		current = snapshot->objects[current->extra_debug_data["node_parent"]];
+	}
+	return path;
+}
+
+String SnapshotDataObject::_get_script_name(Ref<Script> p_script) {
+#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED)
+	// GDScripts have more specific names than base scripts, so use those names if possible.
+	return GDScript::debug_get_script_name(p_script);
+#else
+	// Otherwise fallback to the base script's name.
+	return p_script->get_global_name();
+#endif
+}
+
+String SnapshotDataObject::get_name() {
+	String found_type_name = type_name;
+
+	// Ideally, we will name it after the script attached to it.
+	Ref<Script> maybe_script = get_script();
+	if (maybe_script.is_valid()) {
+		String full_name;
+		while (maybe_script.is_valid()) {
+			String global_name = _get_script_name(maybe_script);
+			if (global_name != "") {
+				if (full_name != "") {
+					full_name = global_name + "/" + full_name;
+				} else {
+					full_name = global_name;
+				}
+			}
+			maybe_script = maybe_script->get_base_script().ptr();
+		}
+
+		found_type_name = type_name + "/" + full_name;
+	}
+
+	return found_type_name + "_" + uitos(remote_object_id);
+}
+
+bool SnapshotDataObject::is_refcounted() {
+	return is_class(RefCounted::get_class_static());
+}
+
+bool SnapshotDataObject::is_node() {
+	return is_class(Node::get_class_static());
+}
+
+bool SnapshotDataObject::is_class(const String &p_base_class) {
+	return ClassDB::is_parent_class(type_name, p_base_class);
+}
+
+HashSet<ObjectID> SnapshotDataObject::_unique_references(const HashMap<String, ObjectID> &p_refs) {
+	HashSet<ObjectID> obj_set;
+
+	for (const KeyValue<String, ObjectID> &pair : p_refs) {
+		obj_set.insert(pair.value);
+	}
+
+	return obj_set;
+}
+
+HashSet<ObjectID> SnapshotDataObject::get_unique_outbound_refernces() {
+	return _unique_references(outbound_references);
+}
+
+HashSet<ObjectID> SnapshotDataObject::get_unique_inbound_references() {
+	return _unique_references(inbound_references);
+}
+
+void GameStateSnapshot::_get_outbound_references(Variant &p_var, HashMap<String, ObjectID> &r_ret_val, const String &p_current_path) {
+	String path_divider = p_current_path.size() > 0 ? "/" : ""; // Make sure we don't start with a /.
+	switch (p_var.get_type()) {
+		case Variant::Type::INT:
+		case Variant::Type::OBJECT: { // Means ObjectID.
+			ObjectID as_id = ObjectID((uint64_t)p_var);
+			if (!objects.has(as_id)) {
+				return;
+			}
+			r_ret_val[p_current_path] = as_id;
+			break;
+		}
+		case Variant::Type::DICTIONARY: {
+			Dictionary dict = (Dictionary)p_var;
+			LocalVector<Variant> keys = dict.get_key_list();
+			for (Variant &k : keys) {
+				// The dictionary key _could be_ an object. If it is, we name the key property with the same name as the value, but with _key appended to it.
+				_get_outbound_references(k, r_ret_val, p_current_path + path_divider + (String)k + "_key");
+				Variant v = dict.get(k, Variant());
+				_get_outbound_references(v, r_ret_val, p_current_path + path_divider + (String)k);
+			}
+			break;
+		}
+		case Variant::Type::ARRAY: {
+			Array arr = (Array)p_var;
+			int i = 0;
+			for (Variant &v : arr) {
+				_get_outbound_references(v, r_ret_val, p_current_path + path_divider + itos(i));
+				i++;
+			}
+			break;
+		}
+		default: {
+			break;
+		}
+	}
+}
+
+void GameStateSnapshot::_get_rc_cycles(
+		SnapshotDataObject *p_obj,
+		SnapshotDataObject *p_source_obj,
+		HashSet<SnapshotDataObject *> p_traversed_objs,
+		LocalVector<String> &r_ret_val,
+		const String &p_current_path) {
+	// We're at the end of this branch and it was a cycle.
+	if (p_obj == p_source_obj && p_current_path != "") {
+		r_ret_val.push_back(p_current_path);
+		return;
+	}
+
+	// Go through each of our children and try traversing them.
+	for (const KeyValue<String, ObjectID> &next_child : p_obj->outbound_references) {
+		SnapshotDataObject *next_obj = p_obj->snapshot->objects[next_child.value];
+		String next_name = next_obj == p_source_obj ? "self" : next_obj->get_name();
+		String current_name = p_obj == p_source_obj ? "self" : p_obj->get_name();
+		String child_path = current_name + "[\"" + next_child.key + "\"] -> " + next_name;
+		if (p_current_path != "") {
+			child_path = p_current_path + "\n" + child_path;
+		}
+
+		SnapshotDataObject *next = objects[next_child.value];
+		if (next != nullptr && next->is_class(RefCounted::get_class_static()) && !next->is_class(WeakRef::get_class_static()) && !p_traversed_objs.has(next)) {
+			HashSet<SnapshotDataObject *> traversed_copy = p_traversed_objs;
+			if (p_obj != p_source_obj) {
+				traversed_copy.insert(p_obj);
+			}
+			_get_rc_cycles(next, p_source_obj, traversed_copy, r_ret_val, child_path);
+		}
+	}
+}
+
+void GameStateSnapshot::recompute_references() {
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : objects) {
+		Dictionary values;
+		for (const KeyValue<StringName, Variant> &kv : obj.value->prop_values) {
+			// Should only ever be one entry in this context.
+			values[kv.key] = kv.value;
+		}
+
+		Variant values_variant(values);
+		HashMap<String, ObjectID> refs;
+		_get_outbound_references(values_variant, refs);
+
+		obj.value->outbound_references = refs;
+
+		for (const KeyValue<String, ObjectID> &kv : refs) {
+			// Get the guy we are pointing to, and indicate the name of _our_ property that is pointing to them.
+			if (objects.has(kv.value)) {
+				objects[kv.value]->inbound_references[kv.key] = obj.key;
+			}
+		}
+	}
+
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : objects) {
+		if (!obj.value->is_class(RefCounted::get_class_static()) || obj.value->is_class(WeakRef::get_class_static())) {
+			continue;
+		}
+		HashSet<SnapshotDataObject *> traversed_objs;
+		LocalVector<String> cycles;
+
+		_get_rc_cycles(obj.value, obj.value, traversed_objs, cycles, "");
+		Array cycles_array;
+		for (const String &cycle : cycles) {
+			cycles_array.push_back(cycle);
+		}
+		obj.value->extra_debug_data["ref_cycles"] = cycles_array;
+	}
+}
+
+Ref<GameStateSnapshotRef> GameStateSnapshot::create_ref(const String &p_snapshot_name, const Vector<uint8_t> &p_snapshot_buffer) {
+	// A ref to a refcounted object which is a wrapper of a non-refcounted object.
+	Ref<GameStateSnapshotRef> sn;
+	sn.instantiate(memnew(GameStateSnapshot));
+	GameStateSnapshot *snapshot = sn->get_snapshot();
+	snapshot->name = p_snapshot_name;
+
+	// Snapshots may have been created by an older version of the editor. Handle parsing old snapshot versions here based on the version number.
+
+	Vector<uint8_t> snapshot_buffer_decompressed;
+	int success = Compression::decompress_dynamic(&snapshot_buffer_decompressed, -1, p_snapshot_buffer.ptr(), p_snapshot_buffer.size(), Compression::MODE_DEFLATE);
+	ERR_FAIL_COND_V_MSG(success != Z_OK, nullptr, "ObjectDB Snapshot could not be parsed. Failed to decompress snapshot.");
+	CoreBind::Marshalls *m = CoreBind::Marshalls::get_singleton();
+	Array snapshot_data = m->base64_to_variant(m->raw_to_base64(snapshot_buffer_decompressed));
+	ERR_FAIL_COND_V_MSG(snapshot_data.is_empty(), nullptr, "ObjectDB Snapshot could not be parsed. Variant array is empty.");
+	const Variant &first_item = snapshot_data[0];
+	ERR_FAIL_COND_V_MSG(first_item.get_type() != Variant::DICTIONARY, nullptr, "ObjectDB Snapshot could not be parsed. First item is not a Dictionary.");
+	snapshot->snapshot_context = first_item;
+
+	SnapshotDataObject::ResourceCache resource_cache;
+	for (int i = 1; i < snapshot_data.size(); i += 4) {
+		SceneDebuggerObject obj;
+		obj.deserialize(uint64_t(snapshot_data[i + 0]), snapshot_data[i + 1], snapshot_data[i + 2]);
+		ERR_FAIL_COND_V_MSG(snapshot_data[i + 3].get_type() != Variant::DICTIONARY, nullptr, "ObjectDB Snapshot could not be parsed. Extra debug data is not a Dictionary.");
+
+		if (obj.id.is_null()) {
+			continue;
+		}
+
+		snapshot->objects[obj.id] = memnew(SnapshotDataObject(obj, snapshot, resource_cache));
+		snapshot->objects[obj.id]->extra_debug_data = (Dictionary)snapshot_data[i + 3];
+	}
+
+	snapshot->recompute_references();
+	print_verbose("Resource cache hits: " + String::num(resource_cache.hits) + ". Resource cache misses: " + String::num(resource_cache.misses));
+	return sn;
+}
+
+GameStateSnapshot::~GameStateSnapshot() {
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &item : objects) {
+		memdelete(item.value);
+	}
+}
+
+bool GameStateSnapshotRef::unreference() {
+	bool die = RefCounted::unreference();
+	if (die) {
+		memdelete(gamestate_snapshot);
+	}
+	return die;
+}
+
+GameStateSnapshot *GameStateSnapshotRef::get_snapshot() {
+	return gamestate_snapshot;
+}

+ 111 - 0
modules/objectdb_profiler/editor/snapshot_data.h

@@ -0,0 +1,111 @@
+/**************************************************************************/
+/*  snapshot_data.h                                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "editor/debugger/editor_debugger_inspector.h"
+
+class GameStateSnapshot;
+class GameStateSnapshotRef;
+
+class SnapshotDataObject : public Object {
+	GDCLASS(SnapshotDataObject, Object);
+
+	HashSet<ObjectID> _unique_references(const HashMap<String, ObjectID> &p_refs);
+	String _get_script_name(Ref<Script> p_script);
+
+public:
+	GameStateSnapshot *snapshot = nullptr;
+	Dictionary extra_debug_data;
+	HashMap<String, ObjectID> outbound_references;
+	HashMap<String, ObjectID> inbound_references;
+
+	HashSet<ObjectID> get_unique_outbound_refernces();
+	HashSet<ObjectID> get_unique_inbound_references();
+
+	uint64_t remote_object_id = 0;
+	String type_name;
+	LocalVector<PropertyInfo> prop_list;
+	HashMap<StringName, Variant> prop_values;
+	bool _get(const StringName &p_name, Variant &r_ret) const;
+	void _get_property_list(List<PropertyInfo> *p_list) const;
+
+	struct ResourceCache {
+		HashMap<String, Ref<Resource>> cache;
+		int misses = 0;
+		int hits = 0;
+	};
+
+	SnapshotDataObject(SceneDebuggerObject &p_obj, GameStateSnapshot *p_snapshot, ResourceCache &resource_cache);
+
+	String get_name();
+	String get_node_path();
+	bool is_refcounted();
+	bool is_node();
+	bool is_class(const String &p_base_class);
+
+protected:
+	// Snapshots are inherently read-only. Can't edit the past.
+	bool _is_read_only() { return true; }
+	static void _bind_methods();
+};
+
+class GameStateSnapshot : public Object {
+	GDCLASS(GameStateSnapshot, Object);
+
+	void _get_outbound_references(Variant &p_var, HashMap<String, ObjectID> &r_ret_val, const String &p_current_path = "");
+	void _get_rc_cycles(SnapshotDataObject *p_obj, SnapshotDataObject *p_source_obj, HashSet<SnapshotDataObject *> p_traversed_objs, LocalVector<String> &r_ret_val, const String &p_current_path = "");
+
+public:
+	String name;
+	HashMap<ObjectID, SnapshotDataObject *> objects;
+	Dictionary snapshot_context;
+
+	// Ideally, this would extend EditorDebuggerRemoteObject and be refcounted, but we can't have it both ways.
+	// So, instead we have this static 'constructor' that returns a RefCounted wrapper around a GameStateSnapshot.
+	static Ref<GameStateSnapshotRef> create_ref(const String &p_snapshot_name, const Vector<uint8_t> &p_snapshot_buffer);
+	~GameStateSnapshot();
+
+	void recompute_references();
+};
+
+// Thin RefCounted wrapper around a GameStateSnapshot.
+class GameStateSnapshotRef : public RefCounted {
+	GDCLASS(GameStateSnapshotRef, RefCounted);
+
+	GameStateSnapshot *gamestate_snapshot = nullptr;
+
+public:
+	GameStateSnapshotRef(GameStateSnapshot *p_gss) :
+			gamestate_snapshot(p_gss) {}
+
+	bool unreference();
+	GameStateSnapshot *get_snapshot();
+};

+ 54 - 0
modules/objectdb_profiler/register_types.cpp

@@ -0,0 +1,54 @@
+/**************************************************************************/
+/*  register_types.cpp                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "register_types.h"
+
+#include "snapshot_collector.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/objectdb_profiler_plugin.h"
+#endif // TOOLS_ENABLED
+
+void initialize_objectdb_profiler_module(ModuleInitializationLevel p_level) {
+	if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
+		SnapshotCollector::initialize();
+	}
+#ifdef TOOLS_ENABLED
+	else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
+		EditorPlugins::add_by_type<ObjectDBProfilerPlugin>();
+	}
+#endif // TOOLS_ENABLED
+}
+
+void uninitialize_objectdb_profiler_module(ModuleInitializationLevel p_level) {
+	if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
+		SnapshotCollector::deinitialize();
+	}
+}

+ 36 - 0
modules/objectdb_profiler/register_types.h

@@ -0,0 +1,36 @@
+/**************************************************************************/
+/*  register_types.h                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "modules/register_module_types.h"
+
+void initialize_objectdb_profiler_module(ModuleInitializationLevel p_level);
+void uninitialize_objectdb_profiler_module(ModuleInitializationLevel p_level);

+ 175 - 0
modules/objectdb_profiler/snapshot_collector.cpp

@@ -0,0 +1,175 @@
+/**************************************************************************/
+/*  snapshot_collector.cpp                                                */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "snapshot_collector.h"
+
+#include "core/core_bind.h"
+#include "core/debugger/engine_debugger.h"
+#include "core/os/time.h"
+#include "core/version.h"
+#include "scene/main/node.h"
+#include "scene/main/window.h"
+
+void SnapshotCollector::initialize() {
+	pending_snapshots.clear();
+	EngineDebugger::register_message_capture("snapshot", EngineDebugger::Capture(nullptr, SnapshotCollector::parse_message));
+}
+
+void SnapshotCollector::deinitialize() {
+	EngineDebugger::unregister_message_capture("snapshot");
+	pending_snapshots.clear();
+}
+
+void SnapshotCollector::snapshot_objects(Array *p_arr, Dictionary &p_snapshot_context) {
+	print_verbose("Starting to snapshot");
+	p_arr->clear();
+
+	// Gather all ObjectIDs first. The ObjectDB will be locked in debug_objects, so we can't serialize until it exits.
+
+	// In rare cases, the object may be deleted as the snapshot is taken. So, we store the object's class name to give users a clue about what went wrong.
+	LocalVector<Pair<ObjectID, StringName>> debugger_object_ids;
+	debugger_object_ids.reserve(ObjectDB::get_object_count());
+
+	ObjectDB::debug_objects(
+			[](Object *p_obj, void *p_user_data) {
+				LocalVector<Pair<ObjectID, StringName>> *debugger_object_ids_ptr = (LocalVector<Pair<ObjectID, StringName>> *)p_user_data;
+				debugger_object_ids_ptr->push_back(Pair<ObjectID, StringName>(p_obj->get_instance_id(), p_obj->get_class_name()));
+			},
+			(void *)&debugger_object_ids);
+
+	// Get SnapshotDataTransportObject from ObjectID list now that DB is unlocked.
+	LocalVector<SnapshotDataTransportObject> debugger_objects;
+	debugger_objects.reserve(debugger_object_ids.size());
+	for (Pair<ObjectID, StringName> ids : debugger_object_ids) {
+		ObjectID oid = ids.first;
+		Object *obj = ObjectDB::get_instance(oid);
+		if (unlikely(obj == nullptr)) {
+			print_verbose(vformat("Object of class '%s' with ID %ud was found to be deleted after ObjectDB was snapshotted.", ids.second, (uint64_t)oid));
+			continue;
+		}
+
+		if (ids.second == SNAME("EditorInterface")) {
+			// The EditorInterface + EditorNode is _kind of_ constructed in a debug game, but many properties are null
+			// We can prevent it from being constructed, but that would break other projects so better to just skip it.
+			continue;
+		}
+
+		// This is the same way objects in the remote scene tree are serialized,
+		// but here we add a few extra properties via the extra_debug_data dictionary.
+		SnapshotDataTransportObject debug_data(obj);
+
+		// If we're RefCounted, send over our RefCount too. Could add code here to add a few other interesting properties.
+		RefCounted *ref = Object::cast_to<RefCounted>(obj);
+		if (ref) {
+			debug_data.extra_debug_data["ref_count"] = ref->get_reference_count();
+		}
+
+		Node *node = Object::cast_to<Node>(obj);
+		if (node) {
+			debug_data.extra_debug_data["node_name"] = node->get_name();
+			if (node->get_parent() != nullptr) {
+				debug_data.extra_debug_data["node_parent"] = node->get_parent()->get_instance_id();
+			}
+
+			debug_data.extra_debug_data["node_is_scene_root"] = SceneTree::get_singleton()->get_root() == node;
+
+			Array children;
+			for (int i = 0; i < node->get_child_count(); i++) {
+				children.push_back(node->get_child(i)->get_instance_id());
+			}
+			debug_data.extra_debug_data["node_children"] = children;
+		}
+
+		debugger_objects.push_back(debug_data);
+	}
+
+	// Add a header to the snapshot with general data about the state of the game, not tied to any particular object.
+	p_snapshot_context["mem_usage"] = Memory::get_mem_usage();
+	p_snapshot_context["mem_max_usage"] = Memory::get_mem_max_usage();
+	p_snapshot_context["timestamp"] = Time::get_singleton()->get_unix_time_from_system();
+	p_snapshot_context["game_version"] = get_godot_version_string();
+	p_arr->push_back(p_snapshot_context);
+	for (SnapshotDataTransportObject &debug_data : debugger_objects) {
+		debug_data.serialize(*p_arr);
+		p_arr->push_back(debug_data.extra_debug_data);
+	}
+
+	print_verbose("Snapshot size: " + String::num_uint64(p_arr->size()));
+}
+
+Error SnapshotCollector::parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured) {
+	r_captured = true;
+	if (p_msg == "request_prepare_snapshot") {
+		int request_id = p_args[0];
+		Dictionary snapshot_context;
+		snapshot_context["editor_version"] = (String)p_args[1];
+		Array objects;
+		snapshot_objects(&objects, snapshot_context);
+		// Debugger networking has a limit on both how many objects can be queued to send and how
+		// many bytes can be queued to send. Serializing to a string means we never hit the object
+		// limit, and only have to deal with the byte limit.
+		// Compress the snapshot in the game client to make sending the snapshot from game to editor a little faster.
+		CoreBind::Marshalls *m = CoreBind::Marshalls::get_singleton();
+		Vector<uint8_t> objs_buffer = m->base64_to_raw(m->variant_to_base64(objects));
+		Vector<uint8_t> objs_buffer_compressed;
+		objs_buffer_compressed.resize(objs_buffer.size());
+		int new_size = Compression::compress(objs_buffer_compressed.ptrw(), objs_buffer.ptrw(), objs_buffer.size(), Compression::MODE_DEFLATE);
+		objs_buffer_compressed.resize(new_size);
+		pending_snapshots[request_id] = objs_buffer_compressed;
+
+		// Tell the editor how long the snapshot is.
+		Array resp = { request_id, pending_snapshots[request_id].size() };
+		EngineDebugger::get_singleton()->send_message("snapshot:snapshot_prepared", resp);
+
+	} else if (p_msg == "request_snapshot_chunk") {
+		int request_id = p_args[0];
+		int begin = p_args[1];
+		int end = p_args[2];
+
+		Array resp = { request_id, pending_snapshots[request_id].slice(begin, end) };
+		EngineDebugger::get_singleton()->send_message("snapshot:snapshot_chunk", resp);
+
+		// If we sent the last part of the string, delete it locally.
+		if (end >= pending_snapshots[request_id].size()) {
+			pending_snapshots.erase(request_id);
+		}
+	} else {
+		r_captured = false;
+	}
+	return OK;
+}
+
+String SnapshotCollector::get_godot_version_string() {
+	String hash = String(VERSION_HASH);
+	if (hash.length() != 0) {
+		hash = " " + vformat("[%s]", hash.left(9));
+	}
+	return "v" VERSION_FULL_BUILD + hash;
+}

+ 53 - 0
modules/objectdb_profiler/snapshot_collector.h

@@ -0,0 +1,53 @@
+/**************************************************************************/
+/*  snapshot_collector.h                                                  */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/debugger/scene_debugger.h"
+
+struct SnapshotDataTransportObject : public SceneDebuggerObject {
+	SnapshotDataTransportObject() :
+			SceneDebuggerObject() {}
+	SnapshotDataTransportObject(Object *p_obj) :
+			SceneDebuggerObject(p_obj) {}
+
+	Dictionary extra_debug_data;
+};
+
+class SnapshotCollector {
+	inline static HashMap<int, Vector<uint8_t>> pending_snapshots;
+
+public:
+	static void snapshot_objects(Array *p_arr, Dictionary &p_snapshot_context);
+	static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
+	static void initialize();
+	static void deinitialize();
+	static String get_godot_version_string();
+};

+ 22 - 17
scene/debugger/scene_debugger.cpp

@@ -727,18 +727,20 @@ void SceneDebugger::reload_cached_files(const PackedStringArray &p_files) {
 	}
 }
 
+SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) :
+		SceneDebuggerObject(ObjectDB::get_instance(p_id)) {
+}
+
 /// SceneDebuggerObject
-SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
-	id = ObjectID();
-	Object *obj = ObjectDB::get_instance(p_id);
-	if (!obj) {
+SceneDebuggerObject::SceneDebuggerObject(Object *p_obj) {
+	if (!p_obj) {
 		return;
 	}
 
-	id = p_id;
-	class_name = obj->get_class();
+	id = p_obj->get_instance_id();
+	class_name = p_obj->get_class();
 
-	if (ScriptInstance *si = obj->get_script_instance()) {
+	if (ScriptInstance *si = p_obj->get_script_instance()) {
 		// Read script instance constants and variables
 		if (!si->get_script().is_null()) {
 			Script *s = si->get_script().ptr();
@@ -746,7 +748,7 @@ SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
 		}
 	}
 
-	if (Node *node = Object::cast_to<Node>(obj)) {
+	if (Node *node = Object::cast_to<Node>(p_obj)) {
 		// For debugging multiplayer.
 		{
 			PropertyInfo pi(Variant::INT, String("Node/multiplayer_authority"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY);
@@ -761,17 +763,17 @@ SceneDebuggerObject::SceneDebuggerObject(ObjectID p_id) {
 			PropertyInfo pi(Variant::STRING, String("Node/path"));
 			properties.push_back(SceneDebuggerProperty(pi, "[Orphan]"));
 		}
-	} else if (Script *s = Object::cast_to<Script>(obj)) {
+	} else if (Script *s = Object::cast_to<Script>(p_obj)) {
 		// Add script constants (no instance).
 		_parse_script_properties(s, nullptr);
 	}
 
 	// Add base object properties.
 	List<PropertyInfo> pinfo;
-	obj->get_property_list(&pinfo, true);
+	p_obj->get_property_list(&pinfo, true);
 	for (const PropertyInfo &E : pinfo) {
 		if (E.usage & (PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CATEGORY)) {
-			properties.push_back(SceneDebuggerProperty(E, obj->get(E.name)));
+			properties.push_back(SceneDebuggerProperty(E, p_obj->get(E.name)));
 		}
 	}
 }
@@ -870,13 +872,16 @@ void SceneDebuggerObject::deserialize(const Array &p_arr) {
 	CHECK_TYPE(p_arr[1], STRING);
 	CHECK_TYPE(p_arr[2], ARRAY);
 
-	id = uint64_t(p_arr[0]);
-	class_name = p_arr[1];
-	Array props = p_arr[2];
+	deserialize(uint64_t(p_arr[0]), p_arr[1], p_arr[2]);
+}
+
+void SceneDebuggerObject::deserialize(uint64_t p_id, const String &p_class_name, const Array &p_props) {
+	id = p_id;
+	class_name = p_class_name;
 
-	for (int i = 0; i < props.size(); i++) {
-		CHECK_TYPE(props[i], ARRAY);
-		Array prop = props[i];
+	for (int i = 0; i < p_props.size(); i++) {
+		CHECK_TYPE(p_props[i], ARRAY);
+		Array prop = p_props[i];
 
 		ERR_FAIL_COND(prop.size() != 6);
 		CHECK_TYPE(prop[0], STRING);

+ 2 - 0
scene/debugger/scene_debugger.h

@@ -143,10 +143,12 @@ public:
 	List<SceneDebuggerProperty> properties;
 
 	SceneDebuggerObject(ObjectID p_id);
+	SceneDebuggerObject(Object *p_obj);
 	SceneDebuggerObject() {}
 
 	void serialize(Array &r_arr, int p_max_size = 1 << 20);
 	void deserialize(const Array &p_arr);
+	void deserialize(uint64_t p_id, const String &p_class_name, const Array &p_props);
 };
 
 class SceneDebuggerTree {

+ 39 - 0
scene/gui/tree.cpp

@@ -5807,6 +5807,16 @@ String Tree::get_column_title(int p_column) const {
 	return columns[p_column].title;
 }
 
+void Tree::set_column_title_tooltip_text(int p_column, const String &p_tooltip) {
+	ERR_FAIL_INDEX(p_column, columns.size());
+	columns.write[p_column].title_tooltip = p_tooltip;
+}
+
+String Tree::get_column_title_tooltip_text(int p_column) const {
+	ERR_FAIL_INDEX_V(p_column, columns.size(), "");
+	return columns[p_column].title_tooltip;
+}
+
 void Tree::set_column_title_alignment(int p_column, HorizontalAlignment p_alignment) {
 	ERR_FAIL_INDEX(p_column, columns.size());
 
@@ -6377,7 +6387,33 @@ int Tree::get_button_id_at_position(const Point2 &p_pos) const {
 String Tree::get_tooltip(const Point2 &p_pos) const {
 	Point2 pos = p_pos - theme_cache.panel_style->get_offset();
 	pos.y -= _get_title_button_height();
+
+	// `pos.y` less than 0 indicates we're in the header.
 	if (pos.y < 0) {
+		// Get the x position of the cursor.
+		real_t pos_x = p_pos.x;
+		if (is_layout_rtl()) {
+			pos_x = get_size().width - pos_x;
+		}
+		pos_x -= theme_cache.panel_style->get_offset().x;
+		if (h_scroll->is_visible_in_tree()) {
+			pos_x += h_scroll->get_value();
+		}
+
+		// Walk forwards until we know which column we're in.
+		int next_edge = 0;
+		int i = 0;
+		for (; i < columns.size(); i++) {
+			if (pos_x < next_edge) {
+				break;
+			}
+			next_edge += get_column_width(i);
+		}
+		if (!columns[i - 1].title_tooltip.is_empty()) {
+			return columns[i - 1].title_tooltip;
+		}
+
+		// If the column has no tooltip, use the default.
 		return Control::get_tooltip(p_pos);
 	}
 
@@ -6528,6 +6564,9 @@ void Tree::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_column_title", "column", "title"), &Tree::set_column_title);
 	ClassDB::bind_method(D_METHOD("get_column_title", "column"), &Tree::get_column_title);
 
+	ClassDB::bind_method(D_METHOD("set_column_title_tooltip_text", "column", "tooltip_text"), &Tree::set_column_title_tooltip_text);
+	ClassDB::bind_method(D_METHOD("get_column_title_tooltip_text", "column"), &Tree::get_column_title_tooltip_text);
+
 	ClassDB::bind_method(D_METHOD("set_column_title_alignment", "column", "title_alignment"), &Tree::set_column_title_alignment);
 	ClassDB::bind_method(D_METHOD("get_column_title_alignment", "column"), &Tree::get_column_title_alignment);
 

+ 4 - 0
scene/gui/tree.h

@@ -511,6 +511,7 @@ private:
 		int expand_ratio = 1;
 		bool expand = true;
 		bool clip_content = false;
+		String title_tooltip;
 		String title;
 		String xl_title;
 		HorizontalAlignment title_alignment = HORIZONTAL_ALIGNMENT_CENTER;
@@ -839,6 +840,9 @@ public:
 	void set_column_title(int p_column, const String &p_title);
 	String get_column_title(int p_column) const;
 
+	void set_column_title_tooltip_text(int p_column, const String &p_tooltip);
+	String get_column_title_tooltip_text(int p_column) const;
+
 	void set_column_title_alignment(int p_column, HorizontalAlignment p_alignment);
 	HorizontalAlignment get_column_title_alignment(int p_column) const;
 

+ 3 - 3
scene/main/node.cpp

@@ -3371,7 +3371,7 @@ void Node::_set_tree(SceneTree *p_tree) {
 #ifdef DEBUG_ENABLED
 static HashMap<ObjectID, List<String>> _print_orphan_nodes_map;
 
-static void _print_orphan_nodes_routine(Object *p_obj) {
+static void _print_orphan_nodes_routine(Object *p_obj, void *p_user_data) {
 	Node *n = Object::cast_to<Node>(p_obj);
 	if (!n) {
 		return;
@@ -3415,7 +3415,7 @@ void Node::print_orphan_nodes() {
 	_print_orphan_nodes_map.clear();
 
 	// Collect and print information about orphan nodes.
-	ObjectDB::debug_objects(_print_orphan_nodes_routine);
+	ObjectDB::debug_objects(_print_orphan_nodes_routine, nullptr);
 
 	for (const KeyValue<ObjectID, List<String>> &E : _print_orphan_nodes_map) {
 		print_line(itos(E.key) + " - Stray Node: " + E.value.get(0) + " (Type: " + E.value.get(1) + ") (Source:" + E.value.get(2) + ")");
@@ -3432,7 +3432,7 @@ TypedArray<int> Node::get_orphan_node_ids() {
 	_print_orphan_nodes_map.clear();
 
 	// Collect and return information about orphan nodes.
-	ObjectDB::debug_objects(_print_orphan_nodes_routine);
+	ObjectDB::debug_objects(_print_orphan_nodes_routine, nullptr);
 
 	for (const KeyValue<ObjectID, List<String>> &E : _print_orphan_nodes_map) {
 		ret.push_back(E.key);