浏览代码

Add an ObjectDB Profiling Tool

A new tab is added to the debugger that can help profile a game's memory usage.

Specifically, this lets you save a snapshot of all the objects in a running
game's ObjectDB to disk. It then lets you view the snapshot and diff two
snapshots against each other. This is meant to work similarly to Chrome's
heap snapshot tool or Unity's memory profiler.
Aleksander Litynski 5 月之前
父节点
当前提交
78f1543e35
共有 40 个文件被更改,包括 4262 次插入109 次删除
  1. 5 3
      core/object/object.cpp
  2. 2 2
      core/object/object.h
  3. 15 0
      doc/classes/Tree.xml
  4. 130 0
      editor/editor_json_visualizer.cpp
  5. 51 0
      editor/editor_json_visualizer.h
  6. 6 81
      editor/shader/editor_native_shader_source_visualizer.cpp
  7. 1 3
      editor/shader/editor_native_shader_source_visualizer.h
  8. 20 0
      modules/objectdb_profiler/SCsub
  9. 9 0
      modules/objectdb_profiler/config.py
  10. 274 0
      modules/objectdb_profiler/editor/data_viewers/class_view.cpp
  11. 73 0
      modules/objectdb_profiler/editor/data_viewers/class_view.h
  12. 163 0
      modules/objectdb_profiler/editor/data_viewers/json_view.cpp
  13. 57 0
      modules/objectdb_profiler/editor/data_viewers/json_view.h
  14. 262 0
      modules/objectdb_profiler/editor/data_viewers/node_view.cpp
  15. 85 0
      modules/objectdb_profiler/editor/data_viewers/node_view.h
  16. 251 0
      modules/objectdb_profiler/editor/data_viewers/object_view.cpp
  17. 63 0
      modules/objectdb_profiler/editor/data_viewers/object_view.h
  18. 310 0
      modules/objectdb_profiler/editor/data_viewers/refcounted_view.cpp
  19. 61 0
      modules/objectdb_profiler/editor/data_viewers/refcounted_view.h
  20. 248 0
      modules/objectdb_profiler/editor/data_viewers/shared_controls.cpp
  21. 127 0
      modules/objectdb_profiler/editor/data_viewers/shared_controls.h
  22. 70 0
      modules/objectdb_profiler/editor/data_viewers/snapshot_view.cpp
  23. 54 0
      modules/objectdb_profiler/editor/data_viewers/snapshot_view.h
  24. 282 0
      modules/objectdb_profiler/editor/data_viewers/summary_view.cpp
  25. 66 0
      modules/objectdb_profiler/editor/data_viewers/summary_view.h
  26. 445 0
      modules/objectdb_profiler/editor/objectdb_profiler_panel.cpp
  27. 102 0
      modules/objectdb_profiler/editor/objectdb_profiler_panel.h
  28. 66 0
      modules/objectdb_profiler/editor/objectdb_profiler_plugin.cpp
  29. 65 0
      modules/objectdb_profiler/editor/objectdb_profiler_plugin.h
  30. 388 0
      modules/objectdb_profiler/editor/snapshot_data.cpp
  31. 111 0
      modules/objectdb_profiler/editor/snapshot_data.h
  32. 54 0
      modules/objectdb_profiler/register_types.cpp
  33. 36 0
      modules/objectdb_profiler/register_types.h
  34. 183 0
      modules/objectdb_profiler/snapshot_collector.cpp
  35. 52 0
      modules/objectdb_profiler/snapshot_collector.h
  36. 22 17
      scene/debugger/scene_debugger.cpp
  37. 2 0
      scene/debugger/scene_debugger.h
  38. 44 0
      scene/gui/tree.cpp
  39. 4 0
      scene/gui/tree.h
  40. 3 3
      scene/main/node.cpp

+ 5 - 3
core/object/object.cpp

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

+ 2 - 2
core/object/object.h

@@ -1046,7 +1046,7 @@ class ObjectDB {
 	static void setup();
 
 public:
-	typedef void (*DebugFunc)(Object *p_obj);
+	typedef void (*DebugFunc)(Object *p_obj, void *p_user_data);
 
 	_ALWAYS_INLINE_ static Object *get_instance(ObjectID p_instance_id) {
 		uint64_t id = p_instance_id;
@@ -1078,6 +1078,6 @@ public:
 	template <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" />

+ 130 - 0
editor/editor_json_visualizer.cpp

@@ -0,0 +1,130 @@
+/**************************************************************************/
+/*  editor_json_visualizer.cpp                                            */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "editor_json_visualizer.h"
+
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/text_edit.h"
+#include "servers/rendering/shader_language.h"
+
+EditorJsonVisualizerSyntaxHighlighter::EditorJsonVisualizerSyntaxHighlighter(const List<String> &p_keywords) {
+	set_number_color(EDITOR_GET("text_editor/theme/highlighting/number_color"));
+	set_symbol_color(EDITOR_GET("text_editor/theme/highlighting/symbol_color"));
+	set_function_color(EDITOR_GET("text_editor/theme/highlighting/function_color"));
+	set_member_variable_color(EDITOR_GET("text_editor/theme/highlighting/member_variable_color"));
+
+	clear_keyword_colors();
+	const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color");
+	const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color");
+
+	for (const String &keyword : p_keywords) {
+		if (ShaderLanguage::is_control_flow_keyword(keyword)) {
+			add_keyword_color(keyword, control_flow_keyword_color);
+		} else {
+			add_keyword_color(keyword, keyword_color);
+		}
+	}
+
+	// Colorize comments.
+	const Color comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color");
+	clear_color_regions();
+	add_color_region("/*", "*/", comment_color, false);
+	add_color_region("//", "", comment_color, true);
+
+	// Colorize preprocessor statements.
+	const Color user_type_color = EDITOR_GET("text_editor/theme/highlighting/user_type_color");
+	add_color_region("#", "", user_type_color, true);
+
+	set_uint_suffix_enabled(true);
+}
+
+void EditorJsonVisualizer::load_theme(Ref<EditorJsonVisualizerSyntaxHighlighter> p_syntax_highlighter) {
+	set_editable(false);
+	set_syntax_highlighter(p_syntax_highlighter);
+	add_theme_font_override(SceneStringName(font), get_theme_font("source", EditorStringName(EditorFonts)));
+	add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size("source_size", EditorStringName(EditorFonts)));
+	add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing"));
+
+	// Appearance: Caret
+	set_caret_type((TextEdit::CaretType)EDITOR_GET("text_editor/appearance/caret/type").operator int());
+	set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink"));
+	set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval"));
+	set_highlight_current_line(EDITOR_GET("text_editor/appearance/caret/highlight_current_line"));
+	set_highlight_all_occurrences(EDITOR_GET("text_editor/appearance/caret/highlight_all_occurrences"));
+
+	// Appearance: Gutters
+	set_draw_line_numbers(EDITOR_GET("text_editor/appearance/gutters/show_line_numbers"));
+	set_line_numbers_zero_padded(EDITOR_GET("text_editor/appearance/gutters/line_numbers_zero_padded"));
+
+	// Appearance: Minimap
+	set_draw_minimap(EDITOR_GET("text_editor/appearance/minimap/show_minimap"));
+	set_minimap_width((int)EDITOR_GET("text_editor/appearance/minimap/minimap_width") * EDSCALE);
+
+	// Appearance: Lines
+	set_line_folding_enabled(EDITOR_GET("text_editor/appearance/lines/code_folding"));
+	set_draw_fold_gutter(EDITOR_GET("text_editor/appearance/lines/code_folding"));
+	set_line_wrapping_mode((TextEdit::LineWrappingMode)EDITOR_GET("text_editor/appearance/lines/word_wrap").operator int());
+	set_autowrap_mode((TextServer::AutowrapMode)EDITOR_GET("text_editor/appearance/lines/autowrap_mode").operator int());
+
+	// Appearance: Whitespace
+	set_draw_tabs(EDITOR_GET("text_editor/appearance/whitespace/draw_tabs"));
+	set_draw_spaces(EDITOR_GET("text_editor/appearance/whitespace/draw_spaces"));
+	add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing"));
+
+	// Behavior: Navigation
+	set_scroll_past_end_of_file_enabled(EDITOR_GET("text_editor/behavior/navigation/scroll_past_end_of_file"));
+	set_smooth_scroll_enabled(EDITOR_GET("text_editor/behavior/navigation/smooth_scrolling"));
+	set_v_scroll_speed(EDITOR_GET("text_editor/behavior/navigation/v_scroll_speed"));
+	set_drag_and_drop_selection_enabled(EDITOR_GET("text_editor/behavior/navigation/drag_and_drop_selection"));
+
+	// Behavior: Indent
+	set_indent_size(EDITOR_GET("text_editor/behavior/indent/size"));
+	set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent"));
+	set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines"));
+}
+
+void EditorJsonVisualizer::_notification(int p_what) {
+	if (p_what == NOTIFICATION_THEME_CHANGED) {
+		Ref<Font> source_font = get_theme_font("source", EditorStringName(EditorFonts));
+		int source_font_size = get_theme_font_size("source_size", EditorStringName(EditorFonts));
+		int line_spacing = EDITOR_GET("text_editor/theme/line_spacing");
+		if (get_theme_font(SceneStringName(font)) != source_font) {
+			add_theme_font_override(SceneStringName(font), source_font);
+		}
+		if (get_theme_font_size(SceneStringName(font_size)) != source_font_size) {
+			add_theme_font_size_override(SceneStringName(font_size), source_font_size);
+		}
+		if (get_theme_constant("line_spacing") != line_spacing) {
+			add_theme_constant_override("line_spacing", line_spacing);
+		}
+	}
+}

+ 51 - 0
editor/editor_json_visualizer.h

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

+ 6 - 81
editor/shader/editor_native_shader_source_visualizer.cpp

@@ -37,40 +37,6 @@
 #include "scene/gui/text_edit.h"
 #include "servers/rendering/shader_language.h"
 
-void EditorNativeShaderSourceVisualizer::_load_theme_settings() {
-	syntax_highlighter->set_number_color(EDITOR_GET("text_editor/theme/highlighting/number_color"));
-	syntax_highlighter->set_symbol_color(EDITOR_GET("text_editor/theme/highlighting/symbol_color"));
-	syntax_highlighter->set_function_color(EDITOR_GET("text_editor/theme/highlighting/function_color"));
-	syntax_highlighter->set_member_variable_color(EDITOR_GET("text_editor/theme/highlighting/member_variable_color"));
-
-	syntax_highlighter->clear_keyword_colors();
-
-	List<String> keywords;
-	ShaderLanguage::get_keyword_list(&keywords);
-	const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color");
-	const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color");
-
-	for (const String &keyword : keywords) {
-		if (ShaderLanguage::is_control_flow_keyword(keyword)) {
-			syntax_highlighter->add_keyword_color(keyword, control_flow_keyword_color);
-		} else {
-			syntax_highlighter->add_keyword_color(keyword, keyword_color);
-		}
-	}
-
-	// Colorize comments.
-	const Color comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color");
-	syntax_highlighter->clear_color_regions();
-	syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
-	syntax_highlighter->add_color_region("//", "", comment_color, true);
-
-	// Colorize preprocessor statements.
-	const Color user_type_color = EDITOR_GET("text_editor/theme/highlighting/user_type_color");
-	syntax_highlighter->add_color_region("#", "", user_type_color, true);
-
-	syntax_highlighter->set_uint_suffix_enabled(true);
-}
-
 void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) {
 	if (versions) {
 		memdelete(versions);
@@ -79,7 +45,10 @@ void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) {
 
 	RS::ShaderNativeSourceCode nsc = RS::get_singleton()->shader_get_native_source_code(p_shader);
 
-	_load_theme_settings();
+	List<String> keywords;
+	ShaderLanguage::get_keyword_list(&keywords);
+	Ref<EditorJsonVisualizerSyntaxHighlighter> syntax_highlighter;
+	syntax_highlighter.instantiate(keywords);
 
 	versions = memnew(TabContainer);
 	versions->set_tab_alignment(TabBar::ALIGNMENT_CENTER);
@@ -93,50 +62,8 @@ void EditorNativeShaderSourceVisualizer::_inspect_shader(RID p_shader) {
 		vtab->set_h_size_flags(Control::SIZE_EXPAND_FILL);
 		versions->add_child(vtab);
 		for (int j = 0; j < nsc.versions[i].stages.size(); j++) {
-			CodeEdit *code_edit = memnew(CodeEdit);
-			code_edit->set_editable(false);
-			code_edit->set_syntax_highlighter(syntax_highlighter);
-			code_edit->add_theme_font_override(SceneStringName(font), get_theme_font("source", EditorStringName(EditorFonts)));
-			code_edit->add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size("source_size", EditorStringName(EditorFonts)));
-			code_edit->add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing"));
-
-			// Appearance: Caret
-			code_edit->set_caret_type((TextEdit::CaretType)EDITOR_GET("text_editor/appearance/caret/type").operator int());
-			code_edit->set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink"));
-			code_edit->set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval"));
-			code_edit->set_highlight_current_line(EDITOR_GET("text_editor/appearance/caret/highlight_current_line"));
-			code_edit->set_highlight_all_occurrences(EDITOR_GET("text_editor/appearance/caret/highlight_all_occurrences"));
-
-			// Appearance: Gutters
-			code_edit->set_draw_line_numbers(EDITOR_GET("text_editor/appearance/gutters/show_line_numbers"));
-			code_edit->set_line_numbers_zero_padded(EDITOR_GET("text_editor/appearance/gutters/line_numbers_zero_padded"));
-
-			// Appearance: Minimap
-			code_edit->set_draw_minimap(EDITOR_GET("text_editor/appearance/minimap/show_minimap"));
-			code_edit->set_minimap_width((int)EDITOR_GET("text_editor/appearance/minimap/minimap_width") * EDSCALE);
-
-			// Appearance: Lines
-			code_edit->set_line_folding_enabled(EDITOR_GET("text_editor/appearance/lines/code_folding"));
-			code_edit->set_draw_fold_gutter(EDITOR_GET("text_editor/appearance/lines/code_folding"));
-			code_edit->set_line_wrapping_mode((TextEdit::LineWrappingMode)EDITOR_GET("text_editor/appearance/lines/word_wrap").operator int());
-			code_edit->set_autowrap_mode((TextServer::AutowrapMode)EDITOR_GET("text_editor/appearance/lines/autowrap_mode").operator int());
-
-			// Appearance: Whitespace
-			code_edit->set_draw_tabs(EDITOR_GET("text_editor/appearance/whitespace/draw_tabs"));
-			code_edit->set_draw_spaces(EDITOR_GET("text_editor/appearance/whitespace/draw_spaces"));
-			code_edit->add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing"));
-
-			// Behavior: Navigation
-			code_edit->set_scroll_past_end_of_file_enabled(EDITOR_GET("text_editor/behavior/navigation/scroll_past_end_of_file"));
-			code_edit->set_smooth_scroll_enabled(EDITOR_GET("text_editor/behavior/navigation/smooth_scrolling"));
-			code_edit->set_v_scroll_speed(EDITOR_GET("text_editor/behavior/navigation/v_scroll_speed"));
-			code_edit->set_drag_and_drop_selection_enabled(EDITOR_GET("text_editor/behavior/navigation/drag_and_drop_selection"));
-
-			// Behavior: Indent
-			code_edit->set_indent_size(EDITOR_GET("text_editor/behavior/indent/size"));
-			code_edit->set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent"));
-			code_edit->set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines"));
-
+			EditorJsonVisualizer *code_edit = memnew(EditorJsonVisualizer);
+			code_edit->load_theme(syntax_highlighter);
 			code_edit->set_name(nsc.versions[i].stages[j].name);
 			code_edit->set_text(nsc.versions[i].stages[j].code);
 			code_edit->set_v_size_flags(Control::SIZE_EXPAND_FILL);
@@ -153,8 +80,6 @@ void EditorNativeShaderSourceVisualizer::_bind_methods() {
 }
 
 EditorNativeShaderSourceVisualizer::EditorNativeShaderSourceVisualizer() {
-	syntax_highlighter.instantiate();
-
 	add_to_group("_native_shader_source_visualizer");
 	set_title(TTR("Native Shader Source Inspector"));
 }

+ 1 - 3
editor/shader/editor_native_shader_source_visualizer.h

@@ -30,16 +30,14 @@
 
 #pragma once
 
+#include "editor/editor_json_visualizer.h"
 #include "scene/gui/dialogs.h"
 #include "scene/gui/tab_container.h"
-#include "scene/resources/syntax_highlighter.h"
 
 class EditorNativeShaderSourceVisualizer : public AcceptDialog {
 	GDCLASS(EditorNativeShaderSourceVisualizer, AcceptDialog)
 	TabContainer *versions = nullptr;
-	Ref<CodeHighlighter> syntax_highlighter;
 
-	void _load_theme_settings();
 	void _inspect_shader(RID p_shader);
 
 protected:

+ 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_mp = env_modules.Clone()
+
+module_obj = []
+
+# Only include in editor and debug builds.
+if env_mp.debug_features:
+    env_mp.add_source_files(module_obj, "*.cpp")
+
+    # Only the editor needs these files, don't include them in the game.
+    if env.editor_build:
+        env_mp.add_source_files(module_obj, "editor/*.cpp")
+        env_mp.add_source_files(module_obj, "editor/data_viewers/*.cpp")
+
+env.modules_sources += module_obj

+ 9 - 0
modules/objectdb_profiler/config.py

@@ -0,0 +1,9 @@
+# config.py
+
+
+def can_build(env, platform):
+    return env.debug_features
+
+
+def configure(env):
+    pass

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

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

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

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

+ 163 - 0
modules/objectdb_profiler/editor/data_viewers/json_view.cpp

@@ -0,0 +1,163 @@
+/**************************************************************************/
+/*  json_view.cpp                                                         */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "json_view.h"
+
+#include "core/io/json.h"
+#include "scene/gui/center_container.h"
+#include "scene/gui/panel_container.h"
+#include "scene/gui/split_container.h"
+#include "shared_controls.h"
+
+SnapshotJsonView::SnapshotJsonView() {
+	set_name(TTR("JSON"));
+}
+
+void SnapshotJsonView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
+	// Lock isn't released until the data processing background thread has finished running
+	// and the json has been passed back to the main thread and displayed.
+	SnapshotView::show_snapshot(p_data, p_diff_data);
+
+	HSplitContainer *box = memnew(HSplitContainer);
+	box->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	add_child(box);
+
+	loading_panel = memnew(DarkPanelContainer);
+	CenterContainer *loading_center = memnew(CenterContainer);
+	Label *loading_label = memnew(Label(TTR("Loading")));
+	add_child(loading_panel);
+	loading_panel->add_child(loading_center);
+	loading_center->add_child(loading_label);
+	loading_panel->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+	loading_center->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
+
+	VBoxContainer *json_box = memnew(VBoxContainer);
+	json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	box->add_child(json_box);
+	String hdr_a_text = diff_data ? TTR("Snapshot A JSON") : TTR("Snapshot JSON");
+	SpanningHeader *hdr_a = memnew(SpanningHeader(hdr_a_text));
+	if (diff_data) {
+		hdr_a->set_tooltip_text(TTR("Snapshot A: ") + snapshot_data->name);
+	}
+	json_box->add_child(hdr_a);
+
+	Ref<EditorJsonVisualizerSyntaxHighlighter> syntax_highlighter;
+	syntax_highlighter.instantiate(List<String>());
+
+	json_content = memnew(EditorJsonVisualizer);
+	json_content->load_theme(syntax_highlighter);
+	json_content->set_name(hdr_a_text);
+	json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+	json_box->add_child(json_content);
+
+	if (diff_data) {
+		VBoxContainer *diff_json_box = memnew(VBoxContainer);
+		diff_json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		diff_json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		box->add_child(diff_json_box);
+		String hrd_b_text = TTR("Snapshot B JSON");
+		SpanningHeader *hdr_b = memnew(SpanningHeader(hrd_b_text));
+		hdr_b->set_tooltip_text(TTR("Snapshot B: ") + diff_data->name);
+		diff_json_box->add_child(hdr_b);
+
+		diff_json_content = memnew(EditorJsonVisualizer);
+		diff_json_content->load_theme(syntax_highlighter);
+		diff_json_content->set_name(hrd_b_text);
+		diff_json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		diff_json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
+		diff_json_box->add_child(diff_json_content);
+	}
+
+	WorkerThreadPool::get_singleton()->add_native_task(&SnapshotJsonView::_serialization_worker, this);
+}
+
+String SnapshotJsonView::_snapshot_to_json(GameStateSnapshot *p_snapshot) {
+	if (p_snapshot == nullptr) {
+		return "";
+	}
+	Dictionary json_data;
+	json_data["name"] = p_snapshot->name;
+	Dictionary objects;
+	for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : p_snapshot->objects) {
+		Dictionary obj_data;
+		obj_data["type_name"] = obj.value->type_name;
+
+		Array prop_list;
+		for (const PropertyInfo &prop : obj.value->prop_list) {
+			prop_list.push_back((Dictionary)prop);
+		}
+		objects["prop_list"] = prop_list;
+
+		Dictionary prop_values;
+		for (const KeyValue<StringName, Variant> &prop : obj.value->prop_values) {
+			// should only ever be one entry in this context
+			prop_values[prop.key] = prop.value;
+		}
+		obj_data["prop_values"] = prop_values;
+
+		objects[obj.key] = obj_data;
+	}
+	json_data["objects"] = objects;
+	return JSON::stringify(json_data, "    ", true, true);
+}
+
+void SnapshotJsonView::_serialization_worker(void *p_ud) {
+	// About 0.3s to serialize snapshots in a small game.
+	SnapshotJsonView *self = static_cast<SnapshotJsonView *>(p_ud);
+	GameStateSnapshot *snapshot_data = self->snapshot_data;
+	GameStateSnapshot *diff_data = self->diff_data;
+	// let the message queue figure out if self is still a valid object or if it's been destroyed.
+	MessageQueue::get_singleton()->push_call(self, "_update_text",
+			snapshot_data, diff_data,
+			_snapshot_to_json(snapshot_data),
+			_snapshot_to_json(diff_data));
+}
+
+void SnapshotJsonView::_update_text(GameStateSnapshot *p_data_ptr, GameStateSnapshot *p_diff_ptr, const String &p_data_str, const String &p_diff_data_str) {
+	if (p_data_ptr != snapshot_data || p_diff_ptr != diff_data) {
+		// If the GameStateSnapshots we generated strings for no longer match the snapshots we asked for,
+		// throw these results away. We'll get more from a different worker process.
+		return;
+	}
+
+	// About 5s to insert the string into the editor.
+	json_content->set_text(p_data_str);
+	if (diff_data) {
+		diff_json_content->set_text(p_diff_data_str);
+	}
+	loading_panel->queue_free();
+	// Loading json done, release the lock.
+}
+
+void SnapshotJsonView::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("_update_text", "p_data_ptr", "p_diff_ptr", "p_data_str", "p_diff_data_str"), &SnapshotJsonView::_update_text);
+}

+ 57 - 0
modules/objectdb_profiler/editor/data_viewers/json_view.h

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

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

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

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

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

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

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

+ 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;
+};

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

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

+ 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;
+};

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

@@ -0,0 +1,248 @@
+/**************************************************************************/
+/*  shared_controls.cpp                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "shared_controls.h"
+
+#include "editor/editor_node.h"
+#include "editor/editor_string_names.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/label.h"
+#include "scene/gui/menu_button.h"
+#include "scene/resources/style_box_flat.h"
+
+SpanningHeader::SpanningHeader(const String &p_text) {
+	Ref<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("panel", content_wrapper_sbf);
+}
+
+void TreeSortAndFilterBar::_apply_filter(TreeItem *p_current_node) {
+	if (!p_current_node) {
+		p_current_node = managed_tree->get_root();
+	}
+
+	if (!p_current_node) {
+		return;
+	}
+
+	// Reset ourself to default state.
+	p_current_node->set_visible(true);
+	p_current_node->clear_custom_color(0);
+
+	// Go through each child and filter them.
+	bool any_child_visible = false;
+	for (TreeItem *child = p_current_node->get_first_child(); child; child = child->get_next()) {
+		_apply_filter(child);
+		if (child->is_visible()) {
+			any_child_visible = true;
+		}
+	}
+
+	// Check if we match the filter.
+	String filter_str = filter_edit->get_text().strip_edges(true, true).to_lower();
+
+	// We are visible.
+	bool matches_filter = false;
+	for (int i = 0; i < managed_tree->get_columns(); i++) {
+		if (p_current_node->get_text(i).to_lower().contains(filter_str)) {
+			matches_filter = true;
+			break;
+		}
+	}
+	if (matches_filter || filter_str.is_empty()) {
+		p_current_node->set_visible(true);
+	} else if (any_child_visible) {
+		// We have a visible child.
+		p_current_node->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+	} else {
+		// We and out children aren't visible.
+		p_current_node->set_visible(false);
+	}
+}
+
+void TreeSortAndFilterBar::_apply_sort() {
+	if (!sort_button->is_visible()) {
+		return;
+	}
+	for (int i = 0; i != sort_button->get_popup()->get_item_count(); i++) {
+		// Update the popup buttons to be checked/unchecked.
+		sort_button->get_popup()->set_item_checked(i, (i == (int)current_sort));
+	}
+
+	SortItem sort = sort_items[current_sort];
+
+	List<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();
+
+		List<TreeItemColumn> items;
+		for (int i = 0; i < to_sort->get_child_count(); i++) {
+			items.push_back(TreeItemColumn(to_sort->get_child(i), sort.column));
+		}
+
+		if (sort.type == ALPHA_SORT && sort.ascending == true) {
+			items.sort_custom<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_ENTER_TREE:
+		case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
+		case NOTIFICATION_THEME_CHANGED:
+		case NOTIFICATION_TRANSLATION_CHANGED: {
+			filter_edit->set_right_icon(get_editor_theme_icon(SNAME("Search")));
+			sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
+
+			apply();
+
+		} break;
+	}
+}
+
+TreeSortAndFilterBar::SortOptionIndexes TreeSortAndFilterBar::add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default) {
+	sort_button->set_visible(true);
+	bool is_first_item = sort_items.is_empty();
+	SortItem item_ascending(sort_items.size(), TTR("Sort By ") + p_new_option + TTR(" (Ascending)"), p_sort_type, true, p_sort_column);
+	sort_items[item_ascending.id] = item_ascending;
+	sort_button->get_popup()->add_radio_check_item(item_ascending.label, item_ascending.id);
+
+	SortItem item_descending(sort_items.size(), TTR("Sort By ") + p_new_option + TTR(" (Descending)"), p_sort_type, false, p_sort_column);
+	sort_items[item_descending.id] = item_descending;
+	sort_button->get_popup()->add_radio_check_item(item_descending.label, item_descending.id);
+
+	if (is_first_item) {
+		sort_button->get_popup()->set_item_checked(0, true);
+	}
+
+	SortOptionIndexes indexes;
+	indexes.ascending = item_ascending.id;
+	indexes.descending = item_descending.id;
+	return indexes;
+}
+
+void TreeSortAndFilterBar::clear_filter() {
+	filter_edit->clear();
+}
+
+void TreeSortAndFilterBar::clear() {
+	sort_button->set_visible(false);
+	sort_button->get_popup()->clear();
+	filter_edit->clear();
+}
+
+void TreeSortAndFilterBar::select_sort(int p_item_id) {
+	_sort_changed(p_item_id);
+}
+
+void TreeSortAndFilterBar::apply() {
+	if (!managed_tree || !managed_tree->get_root()) {
+		return;
+	}
+
+	OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort");
+	_apply_sort();
+	OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort");
+	OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter");
+	_apply_filter();
+	OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter");
+}

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,102 @@
+/**************************************************************************/
+/*  objectdb_profiler_panel.h                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/io/dir_access.h"
+#include "core/templates/lru.h"
+#include "data_viewers/snapshot_view.h"
+#include "snapshot_data.h"
+
+class TabContainer;
+class Tree;
+
+const int SNAPSHOT_CACHE_MAX_SIZE = 10;
+
+// UI loaded by the debugger.
+class ObjectDBProfilerPanel : public Control {
+	GDCLASS(ObjectDBProfilerPanel, Control);
+
+protected:
+	enum OdbProfilerMenuOptions {
+		ODB_MENU_RENAME,
+		ODB_MENU_SHOW_IN_FOLDER,
+		ODB_MENU_DELETE,
+	};
+
+	struct PartialSnapshot {
+		int total_size;
+		Vector<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, String> diff_options;
+	HashMap<int, PartialSnapshot> partial_snapshots;
+
+	List<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();
+	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 _apply_diff(int p_item_idx);
+	void _update_diff_items();
+	void _update_enabled_diff_items();
+	void _edit_snapshot_name();
+	void _view_tab_changed(int p_tab_idx);
+	String _to_mb(int p_x);
+
+public:
+	ObjectDBProfilerPanel();
+
+	void receive_snapshot(int p_request_id);
+	void show_snapshot(const String &p_snapshot_file_name, const String &p_snapshot_diff_file_name);
+	void clear_snapshot();
+	Ref<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(SNAME("started"), callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(true));
+	session->connect(SNAME("stopped"), callable_mp(debugger_panel, &ObjectDBProfilerPanel::set_enabled).bind(false));
+	session->add_session_tab(debugger_panel);
+}
+
+ObjectDBProfilerPlugin::ObjectDBProfilerPlugin() {
+	debugger.instantiate();
+}
+
+void ObjectDBProfilerPlugin::_notification(int p_what) {
+	switch (p_what) {
+		case Node::NOTIFICATION_ENTER_TREE: {
+			add_debugger_plugin(debugger);
+		} break;
+		case Node::NOTIFICATION_EXIT_TREE: {
+			remove_debugger_plugin(debugger);
+		}
+	}
+}

+ 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/plugins/editor_debugger_plugin.h"
+#include "editor/plugins/editor_plugin.h"
+
+class ObjectDBProfilerPanel;
+class ObjectDBProfilerDebuggerPlugin;
+
+// First, ObjectDBProfilerPlugin is loaded. Then it loads ObjectDBProfilerDebuggerPlugin.
+class ObjectDBProfilerPlugin : public EditorPlugin {
+	GDCLASS(ObjectDBProfilerPlugin, EditorPlugin);
+
+protected:
+	Ref<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;
+};

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

@@ -0,0 +1,388 @@
+/**************************************************************************/
+/*  snapshot_data.cpp                                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "snapshot_data.h"
+
+#include "core/core_bind.h"
+#include "core/version.h"
+#if defined(MODULE_GDSCRIPT_ENABLED) && defined(DEBUG_ENABLED)
+#include "modules/gdscript/gdscript.h"
+#else
+#include "core/object/script_language.h"
+#endif
+#include "scene/debugger/scene_debugger.h"
+#include "zlib.h"
+
+SnapshotDataObject::SnapshotDataObject(SceneDebuggerObject &p_obj, GameStateSnapshot *p_snapshot, ResourceCache &resource_cache) :
+		snapshot(p_snapshot) {
+	remote_object_id = p_obj.id;
+	type_name = p_obj.class_name;
+
+	for (const SceneDebuggerObject::SceneDebuggerProperty &prop : p_obj.properties) {
+		PropertyInfo pinfo = prop.first;
+		Variant pvalue = prop.second;
+		// pinfo.name = pinfo.name.trim_prefix("Node/").trim_prefix("Members/");
+
+		if (pinfo.type == Variant::OBJECT && pvalue.is_string()) {
+			String path = pvalue;
+			// If a resource is followed by a ::, it is a nested resource (like a sub_resource in a .tscn file).
+			// To get a reference to it, first we load the parent resource (the .tscn, for example), then,
+			// we load the child resource. The parent resource (dependency) should not be destroyed before the child
+			// resource (pvalue) is loaded.
+			if (path.contains("::")) {
+				// Built-in resource.
+				String base_path = path.get_slice("::", 0);
+				if (!resource_cache.cache.has(base_path)) {
+					resource_cache.cache[base_path] = ResourceLoader::load(base_path);
+					resource_cache.misses++;
+				} else {
+					resource_cache.hits++;
+				}
+			}
+			if (!resource_cache.cache.has(path)) {
+				resource_cache.cache[path] = ResourceLoader::load(path);
+				resource_cache.misses++;
+			} else {
+				resource_cache.hits++;
+			}
+			pvalue = resource_cache.cache[path];
+
+			if (pinfo.hint_string == "Script") {
+				if (get_script() != pvalue) {
+					set_script(Ref<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.
+	if (!get_script().is_null()) {
+		Object *maybe_script_obj = get_script().get_validated_object();
+
+		if (maybe_script_obj->is_class(Script::get_class_static())) {
+			Ref<Script> script_obj = Ref<Script>((Script *)maybe_script_obj);
+
+			String full_name;
+			while (script_obj.is_valid()) {
+				String global_name = _get_script_name(script_obj);
+				if (global_name != "") {
+					if (full_name != "") {
+						full_name = global_name + "/" + full_name;
+					} else {
+						full_name = global_name;
+					}
+				}
+				script_obj = script_obj->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,
+		List<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;
+		List<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.get(0);
+	if (first_item.get_type() != Variant::DICTIONARY) {
+		ERR_PRINT("ObjectDB Snapshot could not be parsed. First item is not a Dictionary.");
+		return nullptr;
+	}
+	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]);
+
+		if (snapshot_data[i + 3].get_type() != Variant::DICTIONARY) {
+			ERR_PRINT("ObjectDB Snapshot could not be parsed. Extra debug data is not a Dictionary.");
+			return nullptr;
+		}
+		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_line("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) {
+		memfree(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;
+	String type_name;
+	List<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 inheritly 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, List<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"
+
+#ifdef TOOLS_ENABLED
+#include "editor/objectdb_profiler_plugin.h"
+#endif // TOOLS_ENABLED
+#include "snapshot_collector.h"
+
+void initialize_objectdb_profiler_module(ModuleInitializationLevel p_level) {
+#ifdef TOOLS_ENABLED
+	if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
+		EditorPlugins::add_by_type<ObjectDBProfilerPlugin>();
+	}
+#endif // TOOLS_ENABLED
+
+	if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
+		SnapshotCollector::initialize();
+	}
+}
+
+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);

+ 183 - 0
modules/objectdb_profiler/snapshot_collector.cpp

@@ -0,0 +1,183 @@
+/**************************************************************************/
+/*  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/window.h"
+
+HashMap<int, Vector<uint8_t>> SnapshotCollector::pending_snapshots;
+
+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.
+	List<Pair<ObjectID, String>> debugger_object_ids;
+	ObjectDB::debug_objects([](Object *p_obj, void *p_user_data) {
+		List<Pair<ObjectID, String>> *debugger_object_ids_ptr = (List<Pair<ObjectID, String>> *)p_user_data;
+		debugger_object_ids_ptr->push_back(Pair<ObjectID, String>(p_obj->get_instance_id(), p_obj->get_class_name()));
+	},
+			(void *)&debugger_object_ids);
+
+	// Get SnapshotDataTransportObject from ObjectID list now that DB is unlocked.
+	List<SnapshotDataTransportObject> debugger_objects;
+	for (Pair<ObjectID, String> ids : debugger_object_ids) {
+		ObjectID oid = ids.first;
+		Object *obj = ObjectDB::get_instance(oid);
+
+		if (obj == nullptr) {
+			print_error("An object was deleted while the ObjectDB was being snapshotted. \
+				The debugger is automatically paused when snapshots are taken, so this should not happen. \
+				The missing object's ID was " +
+					String::num_uint64(oid) + ". \
+				 It's class was " +
+					ids.second + ". Consider reporting this.");
+			continue;
+		}
+
+		if (obj->get_class_name() == SNAME("EditorInterface")) {
+			// The EditorInterface + EditorNode is _kind of_ constructored in a debug game, but many properties rae 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 seialized,
+		// 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.
+		if (ClassDB::is_parent_class(obj->get_class_name(), RefCounted::get_class_static())) {
+			RefCounted *ref = (RefCounted *)obj;
+			debug_data.extra_debug_data["ref_count"] = ref->get_reference_count();
+		}
+
+		if (ClassDB::is_parent_class(obj->get_class_name(), Node::get_class_static())) {
+			Node *node = (Node *)obj;
+			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_available"] = Memory::get_mem_available();
+	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 = (int)p_args.get(0);
+		Dictionary snapshot_context;
+		snapshot_context["editor_version"] = (String)p_args.get(1);
+		Array objects;
+		SnapshotCollector::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;
+		resp.push_back(request_id);
+		resp.push_back(pending_snapshots[request_id].size());
+		EngineDebugger::get_singleton()->send_message("snapshot:snapshot_prepared", resp);
+
+	} else if (p_msg == "request_snapshot_chunk") {
+		int request_id = (int)p_args.get(0);
+		int begin = (int)p_args.get(1);
+		int end = (int)p_args.get(2);
+
+		Array resp;
+		resp.push_back(request_id);
+		resp.push_back(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;
+}

+ 52 - 0
modules/objectdb_profiler/snapshot_collector.h

@@ -0,0 +1,52 @@
+/**************************************************************************/
+/*  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 {
+public:
+	static HashMap<int, Vector<uint8_t>> pending_snapshots;
+	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

@@ -697,18 +697,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();
@@ -716,7 +718,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);
@@ -731,17 +733,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)));
 		}
 	}
 }
@@ -840,13 +842,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

@@ -141,10 +141,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 {

+ 44 - 0
scene/gui/tree.cpp

@@ -5785,6 +5785,21 @@ 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());
+
+	if (columns.write[p_column].title_tooltip == p_tooltip) {
+		return;
+	}
+
+	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());
 
@@ -6346,7 +6361,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);
 	}
 
@@ -6497,6 +6538,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

@@ -510,6 +510,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;
@@ -835,6 +836,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

@@ -3326,7 +3326,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;
@@ -3370,7 +3370,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) + ")");
@@ -3387,7 +3387,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);