references/debug-workflows.md
# Debug & Profiler Workflows (load on demand)
> Official docs cover print/breakpoint basics. Load this when you need workflow depth beyond the symptom → script table in SKILL.md.
## Print debugging contract
```gdscript
print("Player health:", health) # labeled context
push_warning("Deprecated API path")
push_error("Recoverable fault")
assert(health > 0, "Health cannot be negative!") # debug only
```
Wrap dev prints: `if OS.is_debug_build():`
## Breakpoints
Editor gutter or `breakpoint` keyword in suspicious functions. Conditional: `if player.health <= 0: breakpoint`
## Remote debug
Run game (F5) → Debug → Remote Debug → inspect live SceneTree.
## Common patterns
### Null-safe nodes
```gdscript
var node := get_node_or_null("MaybeExists")
if node:
node.do_thing()
```
### Track property changes
```gdscript
var _health: int = 100
var health: int:
get: return _health
set(value):
print("Health changed: %d → %d" % [_health, value])
print_stack()
_health = value
```
## Profiler usage
- **Time profiler:** target < 16.67 ms/frame at 60 FPS
- **Monitor tab:** FPS, memory, draw calls, `OBJECT_ORPHAN_NODE_COUNT` (debug only)
- Profile **release** exports with V-Sync off
## Expert workflows (scripts)
| Workflow | Script |
|----------|--------|
| Headless CI exit codes | [automated_qa_suite.gd](../scripts/automated_qa_suite.gd) |
| Microbenchmarks | [high_precision_benchmarker.gd](../scripts/high_precision_benchmarker.gd) |
| GPU draw-call overlay | RenderingServer `RENDERING_INFO_*` — see baseline expert § Visual Profiler Extensions |
| Thread safety enforcement | `Thread.set_thread_safety_checks_enabled(true)` + [thread_safety_assert.gd](../scripts/thread_safety_assert.gd) |
| Orphan growth | [orphan_node_detector.gd](../scripts/orphan_node_detector.gd) |
| ObjectDB snapshots | Godot ObjectDB profiler docs + [memory_usage_threshold_alert.gd](../scripts/memory_usage_threshold_alert.gd) |
## File I/O error handling template
```gdscript
func load_save() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
push_warning("No save file found")
return {}
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if file == null:
push_error("Failed to open save: %s" % FileAccess.get_open_error())
return {}
var json := JSON.new()
if json.parse(file.get_as_text()) != OK:
push_error("JSON parse error: %s" % json.get_error_message())
return {}
return json.data
```
## Debug flags pattern
```gdscript
const DEBUG := true
func debug_log(message: String) -> void:
if DEBUG:
print("[DEBUG] ", message)
```
references/migration-notes.md
# Migration notes: godot-debugging-profiling
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 3.x → 4.0
Official: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html)
- `VisualServer` → `RenderingServer`; render info getter renamed.
- Debugger/monitors still apply — re-bind custom monitors after API renames.
- Threading: `Thread.start` Callable form; `is_active` → `is_alive`.
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `Object.get_meta_list` return type is `Array[StringName]` (was PackedStringArray).
- `WorkerThreadPool.wait_for_task_completion` now returns `Error`.
- `Basis`/`Transform3D.looking_at` and `Node3D.look_at*` gain optional `use_model_front`.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
- `NOTIFICATION_NODE_RECACHE_REQUESTED` removed from Node.
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- Binary serialization of scripted Objects/typed Arrays changed — re-test save/load of custom Resources.
- `PackedByteArray` may use compact base64 storage; older editors may not open 4.3 resources with large byte arrays.
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- `@export_file` stores `uid://` paths from the Inspector (breaking vs `res://` expectations).
- `FileAccess.store_*` methods return `bool` success.
- `Curve` enforces `min_value`/`max_value` — adjust curves that used points outside `[0, 1]`.
- `OS.read_string_from_stdin` requires `buffer_size`.
- `@export_file` Inspector assignments become `uid://` — scripts expecting `res://` strings must resolve UIDs or use `@export_file_path` (4.5+).
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- `Resource.duplicate(true)` deep-duplicates **only internal** resources; use `duplicate_deep(DEEP_DUPLICATE_ALL)` for old behavior.
- `Node.get_rpc_config` renamed to `get_node_rpc_config`.
- `JSONRPC.set_scope` replaced by `set_method`.
- `ProjectSettings.add_property_info` warns on invalid/`usage` keys.
- `add_property_info` validates keys more loudly.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- TSCN gains unique node IDs (large VCS diffs on first 4.6 save — expected).
- `FileAccess.get_as_text` drops `skip_cr` parameter.
- `Performance.add_custom_monitor` gains optional `type`.
- New project defaults: D3D12 on Windows; Jolt for 3D physics — document for templates.
- `MeshInstance3D.skeleton` default is empty NodePath — enable compatibility setting if old parent-skeleton behavior needed.
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- `Object.is_class` takes `StringName`.
- Setting an element of a packed array property no longer calls the property setter for the whole array.
- Overrides of methods with typed returns must actually `return` (add `return null` if needed).
- New projects default stretch `canvas_items` + aspect `expand` (was `disabled`/`keep`).
- New project stretch defaults `canvas_items` + `expand`.
- Sky reflection roughness_layers default restored toward 8.
- Packed array element assignment no longer triggers whole-property setter.
- Typed-return overrides require an explicit return statement.
scripts/advanced_backtrace_recorder.gd
# advanced_backtrace_recorder.gd
# Capturing the call stack with local variables
extends Node
# EXPERT NOTE: capture_script_backtraces(true) is expensive;
# it captures local variable states which blocks deallocation.
func generate_detailed_report():
var backtraces := Engine.capture_script_backtraces(true)
for frame in backtraces:
var file := frame.get_frame_file(0)
var line := frame.get_frame_line(0)
var func_name := frame.get_frame_function(0)
print("Frame: %s:%d in %s" % [file, line, func_name])
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# - https://docs.godotengine.org/en/stable/classes/class_engine.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — stack/locals capture APIs
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md — crash report fixtures in CI
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/automated_qa_suite.gd
# automated_qa_suite.gd
# Expert pattern for headless CLI-driven QA testing.
# Grounded in Godot 4.x headless mode execution.
extends SceneTree
## Main entry point for automated QA.
func _init() -> void:
print("=== Automated QA Suite: Initialization ===")
# Execute tests sequentially
var results := []
results.append(run_unit_tests())
results.append(run_smoke_tests())
# Report and Quit
var fail_count = results.count(false)
if fail_count > 0:
printerr("QA Suite: FAILED (%d failures)" % fail_count)
quit(1) # Exit with error code for CI
else:
print("QA Suite: PASSED")
quit(0)
func run_unit_tests() -> bool:
print("- Running Unit Tests...")
return true
func run_smoke_tests() -> bool:
print("- Running Smoke Tests (Scene Loading)...")
return true
## Usage Expert Tip:
## Run from terminal: godot --headless -s automated_qa_suite.gd
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md — GUT/CI assertion patterns
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-export-builds/SKILL.md — headless export template runs
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/break_on_condition.gd
# break_on_condition.gd
# Forcing the debugger to halt on invalid states
extends Node
# EXPERT NOTE: Hardcoded breakpoints are team-agnostic
# and don't rely on ephemeral editor UI configuration.
func validate_player_state(p: Node):
if p == null:
# Editor halts here immediately
breakpoint
if p.get("health") != null and p.health < -100:
# Catching extreme overflows
breakpoint
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — breakpoint keyword and asserts
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md — fail-fast invalid state checks
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/custom_debug_draw.gd
# custom_debug_draw.gd
# Visualizing AI paths and physics bounds in 2D
extends Node2D
# EXPERT NOTE: Use _draw() to visualize non-visual data.
# Redrawing every frame allows tracking moving targets.
var path: PackedVector2Array = []
func _draw():
if not OS.is_debug_build(): return
if path.size() < 2: return
draw_polyline(path, Color.CYAN, 3.0, true)
func _process(_delta):
queue_redraw()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# - https://docs.godotengine.org/en/stable/classes/class_node.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md — visualize query results
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md — path/debug overlays
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/custom_editor_monitor.gd
# custom_editor_monitor.gd
# Exposing game metrics to the Editor Debugger
extends Node
# EXPERT NOTE: add_custom_monitor lets you see game-specific
# bottlenecks (AI count, active projectiles) in the Monitors tab.
func _ready():
Performance.add_custom_monitor("Game/ActiveProjectiles", _get_projectile_count)
func _get_projectile_count() -> int:
return get_tree().get_nodes_in_group("Projectiles").size()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html
# - https://docs.godotengine.org/en/stable/classes/class_performance.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — act on custom monitor spikes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md — register monitors from Autoload
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/debug_overlay.gd
# skills/debugging-profiling/scripts/debug_overlay.gd
extends CanvasLayer
## Debug Overlay Expert Pattern
## In-game debug UI for performance monitoring and state inspection.
class_name DebugOverlay
@onready var label := Label.new()
var _update_interval: float = 0.5
var _time_since_update: float = 0.0
var _custom_metrics: Dictionary = {}
func _ready() -> void:
# Setup label
label.position = Vector2(10, 10)
label.add_theme_font_size_override("font_size", 14)
label.add_theme_color_override("font_color", Color.YELLOW)
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 2)
add_child(label)
# Only visible in debug builds
visible = OS.is_debug_build()
func _process(delta: float) -> void:
_time_since_update += delta
if _time_since_update >= _update_interval:
_update_overlay()
_time_since_update = 0.0
func _update_overlay() -> void:
var fps := Engine.get_frames_per_second()
var mem := Performance.get_monitor(Performance.MEMORY_STATIC) / 1024.0 / 1024.0
var objects := Performance.get_monitor(Performance.OBJECT_COUNT)
var orphans := Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
var text := "FPS: %d\n" % fps
text += "Memory: %.1f MB\n" % mem
text += "Objects: %d\n" % objects
if orphans > 0:
text += "⚠️ Orphans: %d\n" % orphans
# Custom metrics
for key in _custom_metrics:
text += "%s: %s\n" % [key, str(_custom_metrics[key])]
label.text = text
func add_metric(key: String, value) -> void:
_custom_metrics[key] = value
func remove_metric(key: String) -> void:
_custom_metrics.erase(key)
## EXPERT USAGE:
## Add as autoload: DebugOverlay
## DebugOverlay.add_metric("Enemies", enemy_count)
## DebugOverlay.add_metric("Player Health", player.health)
##
## Press F12 to toggle: DebugOverlay.visible = !DebugOverlay.visible
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_performance.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — interpret overlay FPS/mem
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md — CanvasLayer overlay layout
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/debugger_tab_plugin.gd
# debugger_tab_plugin.gd
# Injecting custom visual tabs into the bottom Debugger panel
@tool
extends EditorDebuggerPlugin
# EXPERT NOTE: This must be registered via an EditorPlugin
# to take effect in the Godot Editor UI.
func _setup_session(session_id: int):
var panel := VBoxContainer.new()
panel.name = "MyTools"
var label := Label.new()
label.text = "Custom Debug Info"
panel.add_child(label)
var session := get_session(session_id)
session.add_session_tab(panel)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_editordebuggerplugin.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html
# - https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md — EditorPlugin registration layout
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md — avoid Autoload-only debugger hooks
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/engine_editor_hint_logic.gd
# engine_editor_hint_logic.gd
# Debug tools that run inside the Editor
@tool
extends Node3D
# EXPERT NOTE: Use Engine.is_editor_hint() to run
# visualization tools safely while designing.
func _process(_delta):
if Engine.is_editor_hint():
# Update gizmo or helper mesh in real-time
pass
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html
# - https://docs.godotengine.org/en/stable/classes/class_engine.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md — @tool script project layout
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md — editor-time visualization gizmos
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/engine_error_interceptor.gd
# engine_error_interceptor.gd
# Piping C++ engine errors to custom logging backends
extends Node
# EXPERT NOTE: register_message_capture allows you to intercept
# underlying engine errors that usually only go to the console.
func _ready():
if OS.is_debug_build():
EngineDebugger.register_message_capture("custom_logger", _on_engine_error)
func _on_engine_error(message: String, data: Array) -> bool:
# Process or send error data to external analytics
print_rich("[color=orange]Intercepted Engine Error:[/color] ", message)
return true
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/output_panel.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md — global error capture Autoload
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-server-architecture/SKILL.md — server-side analytics sinks
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/high_precision_benchmarker.gd
# high_precision_benchmarker.gd
# Measuring execution time with microsecond precision
extends Node
# EXPERT NOTE: Milliseconds lack the precision for microbenchmarking;
# always use Time.get_ticks_usec() for CPU cycle measurements.
func benchmark_operation(callable: Callable):
var begin := Time.get_ticks_usec()
callable.call()
var end := Time.get_ticks_usec()
print("Operation took %d microseconds" % (end - begin))
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_time.html
# - https://docs.godotengine.org/en/stable/tutorials/performance/cpu_optimization.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — turn usec deltas into fixes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — timed balance sim loops
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/memory_usage_threshold_alert.gd
# memory_usage_threshold_alert.gd
# Catching memory bloat early
extends Node
# EXPERT NOTE: Monitor static memory and push a warning
# if it exceeds a project-defined threshold.
const MEMORY_LIMIT_MB = 1024
func _physics_process(_delta):
var usage_mb = Performance.get_monitor(Performance.MEMORY_STATIC) / 1024 / 1024
if usage_mb > MEMORY_LIMIT_MB:
push_warning("MEMORY USAGE HIGH: ", usage_mb, "MB")
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_performance.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/objectdb_profiler.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — memory leak remediation
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md — free transient scenes on threshold
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/orphan_node_detector.gd
# orphan_node_detector.gd
# Tracking nodes that were removed but never freed
extends Node
# EXPERT NOTE: OBJECT_ORPHAN_NODE_COUNT only works in debug builds.
# Use print_orphan_nodes() to dump the IDs for leak analysis.
func check_for_leaks():
if not OS.is_debug_build(): return
var orphans = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
if orphans > 0:
print_rich("[color=red]Memory Leak: %d Orphan nodes detected![/color]" % orphans)
Node.print_orphan_nodes()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_performance.html
# - https://docs.godotengine.org/en/stable/classes/class_node.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/objectdb_profiler.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md — scene free vs orphan growth
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — retained connections keep nodes alive
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/performance_plotter.gd
# skills/debugging-profiling/code/performance_plotter.gd
extends Node
## Performance Plotter Expert Pattern
## Hooks into Godot's Performance API for professional profiling.
func _ready() -> void:
if OS.is_debug_build():
# 1. Custom Monitors
# Track arbitrary variables in the engine's built-in monitor.
Performance.add_custom_monitor("Gameplay/ActiveProjectiles", _get_projectile_count)
Performance.add_custom_monitor("Gameplay/EnemyCount", _get_enemy_count)
func _get_projectile_count() -> int:
return get_tree().get_nodes_in_group("projectiles").size()
func _get_enemy_count() -> int:
return get_tree().get_nodes_in_group("enemies").size()
func capture_error_state(context: String) -> String:
# 2. Automated Diagnostic Capture
# Gathers stack traces and scene tree structure for bug reports.
var report = {
"timestamp": Time.get_datetime_string_from_system(),
"context": context,
"stack_trace": get_stack(),
"os": OS.get_name(),
"memory_usage": Performance.get_monitor(Performance.MEMORY_STATIC)
}
return JSON.stringify(report, "\t")
## EXPERT NOTE:
## Use 'push_error()' and 'push_warning()' instead of 'print()' for logic errors.
## These appear in the Debugger tab with red/yellow icons and stack traces,
## making them impossible to miss compared to standard console output.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_performance.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — plot then optimize bottlenecks
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-export-builds/SKILL.md — plot release exports not editor debug
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/property_watcher_gizmo.gd
# property_watcher_gizmo.gd
# Monitoring variables without print spam
extends Node
# EXPERT NOTE: Use a label or custom gizmo to track
# fast-changing variables (velocity, state) visually.
@onready var label = $DebugLabel
func _process(_delta):
var parent = get_parent()
if parent:
label.text = "State: %s\nVel: %s" % [parent.state, parent.velocity]
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/output_panel.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — property setters without print spam
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md — Label overlays for live values
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/push_error_safe_exit.gd
# push_error_safe_exit.gd
# Reporting errors without crashing the engine
extends Node
# EXPERT NOTE: push_error() sends to the Godot console
# and debugger without stopping execution like assert().
func load_critical_config(path: String):
if not FileAccess.file_exists(path):
push_error("CRITICAL CONFIG MISSING: ", path)
# Fallback to default avoid crash
return _generate_default_config()
return load(path)
func _generate_default_config():
return {}
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/output_panel.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — push_error vs assert semantics
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-save-load-systems/SKILL.md — soft-fail config/save IO
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/remote_debug_console.gd
# remote_debug_console.gd
# Real-time command console for mobile/deployed builds
extends CanvasLayer
# EXPERT NOTE: Remote builds don't show the Terminal.
# A custom UI console allows running commands on-device.
@onready var line_edit = $LineEdit
func _on_text_submitted(cmd: String):
match cmd:
"noclip": _toggle_noclip()
"gold": _add_gold(1000)
line_edit.clear()
func _toggle_noclip(): pass
func _add_gold(_amt: int): pass
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-platform-mobile/SKILL.md — on-device console when no terminal
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-export-builds/SKILL.md — debug consoles in device exports
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/scene_tree_dump.gd
# scene_tree_dump.gd
# Debugging orphan nodes and tree bloat
extends Node
# EXPERT NOTE: Use print_tree_pretty() to see a snapshot
# of the current active hierarchy in the terminal.
func log_tree_state():
print_rich("[color=yellow]--- SCENE TREE DUMP ---[/color]")
get_tree().root.print_tree_pretty()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_node.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md — tree bloat after scene swaps
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-composition/SKILL.md — unexpected component node counts
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/stack_trace_logger.gd
# stack_trace_logger.gd
# Capturing the execution path on failure
extends Node
# EXPERT NOTE: get_stack() provides a programmatic
# check of where a logic error originated.
func log_problem(msg: String):
var stack = get_stack()
printerr("PROBLEM: ", msg)
for frame in stack:
printerr(" -> ", frame.source, ":", frame.line, " in ", frame.function)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — get_stack / print_stack usage
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — trace unexpected emit callers
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/thread_safe_logger.gd
# thread_safe_logger.gd
# Writing custom log files without blocking the main thread
class_name ThreadSafeLogger extends Logger
var _mutex := Mutex.new()
var _log_file: FileAccess
# EXPERT NOTE: Subclassing Logger and using a Mutex ensures
# that logs from worker threads don't corrupt the file stream.
func _log_message(message: String, _error: bool):
_mutex.lock()
# Real implementations would write to _log_file here
_mutex.unlock()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html
# - https://docs.godotengine.org/en/stable/classes/class_logger.html
# - https://docs.godotengine.org/en/stable/tutorials/performance/using_multiple_threads.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md — register custom Logger Autoload
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-server-architecture/SKILL.md — worker-thread log sinks
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
scripts/thread_safety_assert.gd
# thread_safety_assert.gd
# Caught threading violations early
extends Node
# EXPERT NOTE: Writing to the SceneTree from a worker
# thread is a common source of crashes.
func update_main_thread_data():
# EXPERT: Verifies that this code runs on the Main Thread
assert(OS.get_main_thread_id() == OS.get_thread_caller_id())
# Safe to update UI or Nodes now
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/performance/thread_safe_apis.html
# - https://docs.godotengine.org/en/stable/tutorials/performance/using_multiple_threads.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-server-architecture/SKILL.md — server APIs vs SceneTree access
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — offload work without races
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-debugging-profiling
description: "Expert debugging and profiling for leaks, GPU/Visual Profiler, headless CI QA, orphan nodes, thread-safe logs, and custom Debugger monitors — not print/breakpoint tutorials. Trigger on OBJECT_ORPHAN_NODE_COUNT, ObjectDB growth, Visual Profiler GPU spikes, flaky headless exits, or remote device consoles. Keywords: orphan nodes, Performance.get_monitor, Visual Profiler, Time.get_ticks_usec, EditorDebuggerPlugin, headless QA, push_error, backtrace."
---
## NEVER Do
- **NEVER use `print()` without descriptive context** — `print(value)` is useless. Use `print("Player health:", health)` with labels.
- **NEVER leave debug prints in release builds** — Wrap in `if OS.is_debug_build()` or use custom DEBUG const. Prints slow down release.
- **NEVER ignore `push_warning()` messages** — Warnings indicate potential bugs (null refs, deprecated APIs). Fix them before they become errors.
- **NEVER use `assert()` for runtime validation in release** — Asserts are disabled in release builds. Use `if not condition: push_error()` for runtime checks.
- **NEVER profile in debug mode** — Debug builds are 5-10x slower. Always profile with release exports or `--release` flag.
- **NEVER assume `Engine.capture_script_backtraces(true)` is cheap** — Capturing locals allocates significant memory and can prevent objects from being deallocated, causing artificial leaks [19].
- **NEVER call `push_error()` or `print()` inside a custom `Logger._log_message` override** — This causes infinite recursion and crashes as the logger intercepts its own output [20].
- **NEVER leave the Visual Profiler running during gameplay tests** — Continuous polling degrades framerates significantly, invalidating actual performance metrics [21].
- **NEVER rely on `OS.get_ticks_msec()` for microbenchmarking** — Milliseconds lack precision for logic timing; ALWAYS use `Time.get_ticks_usec()` for microsecond precision [22].
- **NEVER assume `OBJECT_ORPHAN_NODE_COUNT` works in production** — This monitor is strictly debug-only; it safely returns 0 in release builds, potentially hiding leaks [23].
- **NEVER benchmark with V-Sync enabled** — V-Sync throttles metrics to the monitor refresh rate, masking the true CPU/GPU processing overhead [24].
- **NEVER leave `print_stack()` or `print_debug()` in release builds** — These are often stripped or useless outside the debugger. Use structured logging for production [25].
- **NEVER strip debugging symbols if using external C++ profilers** — Stripping destroys call stack readability for external tools like Perfetto or VerySleepy [26].
- **NEVER forget to unregister an `EditorDebuggerPlugin` in `_exit_tree()`** — Failing to clean up leaves "ghost" connections in the engine's debugging loop [27].
- **NEVER trust the Visual Profiler on macOS when using the Compatibility renderer** — Platform-specific driver limitations severely restrict OpenGL profiling accuracy on macOS [28].
## Symptom → Monitor → Script
> **MANDATORY** for the matching row. **Do NOT Load** every debug script for one bug.
| Symptom | Monitor / API | Script |
|---------|---------------|--------|
| Nodes removed but RAM climbs | `OBJECT_ORPHAN_NODE_COUNT` (debug) | **MANDATORY** [orphan_node_detector.gd](scripts/orphan_node_detector.gd) |
| ObjectDB / instance growth | custom monitors + dump | [memory_usage_threshold_alert.gd](scripts/memory_usage_threshold_alert.gd), [scene_tree_dump.gd](scripts/scene_tree_dump.gd) |
| GPU / overdraw mystery | Visual Profiler (briefly) | Pair with perf skill; use [performance_plotter.gd](scripts/performance_plotter.gd) for trends — do not leave Visual Profiler on |
| Flaky headless / CI exit | exit codes + asserts | **MANDATORY** [automated_qa_suite.gd](scripts/automated_qa_suite.gd), [push_error_safe_exit.gd](scripts/push_error_safe_exit.gd) |
| Microbenchmark lies | `Time.get_ticks_usec` | **MANDATORY** [high_precision_benchmarker.gd](scripts/high_precision_benchmarker.gd) |
| Crash needs locals | backtraces (debug only) | [advanced_backtrace_recorder.gd](scripts/advanced_backtrace_recorder.gd), [stack_trace_logger.gd](scripts/stack_trace_logger.gd) |
| Engine errors to backend | Logger intercept | [engine_error_interceptor.gd](scripts/engine_error_interceptor.gd) — never print inside Logger |
| Custom Debugger metrics | Monitors tab | [custom_editor_monitor.gd](scripts/custom_editor_monitor.gd), [debugger_tab_plugin.gd](scripts/debugger_tab_plugin.gd) |
| Mobile/console no stdout | in-game console | [remote_debug_console.gd](scripts/remote_debug_console.gd), [debug_overlay.gd](scripts/debug_overlay.gd) (debug builds only) |
| Thread races / log corruption | mutex logger / asserts | [thread_safe_logger.gd](scripts/thread_safe_logger.gd), [thread_safety_assert.gd](scripts/thread_safety_assert.gd) |
| Invisible logic (AI/physics) | debug draw / gizmos | [custom_debug_draw.gd](scripts/custom_debug_draw.gd), [property_watcher_gizmo.gd](scripts/property_watcher_gizmo.gd) |
| Conditional halt | hardcoded break | [break_on_condition.gd](scripts/break_on_condition.gd) |
| Editor vs runtime paths | `Engine.is_editor_hint` | [engine_editor_hint_logic.gd](scripts/engine_editor_hint_logic.gd) |
## Available Scripts (full catalog)
### Leaks & memory
- [orphan_node_detector.gd](scripts/orphan_node_detector.gd)
- [memory_usage_threshold_alert.gd](scripts/memory_usage_threshold_alert.gd)
- [scene_tree_dump.gd](scripts/scene_tree_dump.gd)
### Timing & QA
- [high_precision_benchmarker.gd](scripts/high_precision_benchmarker.gd)
- [automated_qa_suite.gd](scripts/automated_qa_suite.gd)
- [push_error_safe_exit.gd](scripts/push_error_safe_exit.gd)
- [performance_plotter.gd](scripts/performance_plotter.gd)
### Errors, stacks, threads
- [advanced_backtrace_recorder.gd](scripts/advanced_backtrace_recorder.gd)
- [stack_trace_logger.gd](scripts/stack_trace_logger.gd)
- [engine_error_interceptor.gd](scripts/engine_error_interceptor.gd)
- [thread_safe_logger.gd](scripts/thread_safe_logger.gd)
- [thread_safety_assert.gd](scripts/thread_safety_assert.gd)
### Editor / remote / viz
- [custom_editor_monitor.gd](scripts/custom_editor_monitor.gd)
- [debugger_tab_plugin.gd](scripts/debugger_tab_plugin.gd) — unregister in `_exit_tree()`
- [remote_debug_console.gd](scripts/remote_debug_console.gd)
- [debug_overlay.gd](scripts/debug_overlay.gd) — debug builds only
- [custom_debug_draw.gd](scripts/custom_debug_draw.gd)
- [property_watcher_gizmo.gd](scripts/property_watcher_gizmo.gd)
- [break_on_condition.gd](scripts/break_on_condition.gd)
- [engine_editor_hint_logic.gd](scripts/engine_editor_hint_logic.gd)
## Expert Pointers
- Profile release/`--release` with V-Sync off; never trust Debug-build timings.
- Prefer structured logs over `print_stack()` in anything that might ship.
- Escalate sustained FPS issues to `godot-performance-optimization` after the symptom tree identifies the bottleneck class.
> **MANDATORY** for print/breakpoint workflow depth, profiler interpretation, and expert CI/GPU/thread patterns: [debug-workflows.md](references/debug-workflows.md). **Do NOT Load** when the symptom → script table above already routes you.
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Overview of debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html) — Map of remote scene tree, breakpoints, Output, and profiler entry points before picking a deeper page.
- [Debugger panel](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html) — Stack, variables, breakpoints, errors, and monitors used for live remote debug sessions.
- [The profiler](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html) — Time and visual profilers: why you profile release-like builds and how frame charts isolate CPU/GPU spikes.
- [Output panel](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/output_panel.html) — How `print` / `push_warning` / `push_error` surface in the editor and why noisy release logs hide real faults.
- [ObjectDB profiler](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/objectdb_profiler.html) — Before/after ObjectDB snapshots for RefCounted cycles and leaked instances that orphan monitors miss.
- [Custom performance monitors](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html) — `Performance.add_custom_monitor` so game-specific metrics appear next to engine monitors.
- [Logging](https://docs.godotengine.org/en/stable/tutorials/scripting/logging.html) — Custom `Logger` registration, file sinks, and recursion hazards when logging from inside log handlers.
- [Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html) — Headless `--script` / export flags for automated QA and CI exit-code runners.
- [CPU optimization](https://docs.godotengine.org/en/stable/tutorials/performance/cpu_optimization.html) — Interpreting profiler hotspots into GDScript/server/thread fixes after measurement.
- [Using multiple threads](https://docs.godotengine.org/en/stable/tutorials/performance/using_multiple_threads.html) — Worker-thread rules that motivate thread-safety asserts and mutexed loggers.
- [Thread-safe APIs](https://docs.godotengine.org/en/stable/tutorials/performance/thread_safe_apis.html) — Which servers/APIs may be called off-main-thread without corrupting the SceneTree.
- [Performance](https://docs.godotengine.org/en/stable/classes/class_performance.html) — Built-in monitors (`OBJECT_ORPHAN_NODE_COUNT`, memory, render) used by overlays and leak detectors.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — Debug/release feature tags, project settings, and Autoload layout must exist before debugger plugins or global monitors.
- [godot-gdscript-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md) — Typed Callables, `assert`/`push_error`, and `Time` APIs underpin benchmarks, breakpoints, and stack helpers.
#### Complements
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — After the profiler names a hotspot, apply pooling, culling, and MultiMesh fixes from that skill.
- [godot-testing-patterns](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md) — GUT/assert/CI patterns pair with headless QA suites and deterministic `quit` exit codes.
- [godot-export-builds](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-export-builds/SKILL.md) — Profile and remote-debug against real export templates; debug symbols and strip settings matter for external profilers.
- [godot-scene-management](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md) — Scene swap lifetime bugs show up as orphan-node growth; use tree dumps when loaders fail to free.
- [godot-signal-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md) — Ghost listeners and deferred connects often explain “why is this still running?” stack traces.
- [godot-autoload-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md) — Global loggers, monitors, and error interceptors belong in Autoloads with clear boot order.
- [godot-server-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-server-architecture/SKILL.md) — When debug draw or metrics push into Rendering/Physics servers, keep server ownership separate from nodes.
#### Downstream / consumers
- [godot-auditor](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-auditor/SKILL.md) — Consumes debugger/profiler evidence when enforcing never-lists and architectural integrity reviews.
- [godot-platform-mobile](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-platform-mobile/SKILL.md) — Device remote debug and on-screen consoles are required when desktop Output is unavailable.
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — Headless microbenchmarks and CI QA feed balance sims that need stable timing and exit codes.
- [godot-multiplayer-networking](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md) — Networked games need remote inspectors and structured logs across peers without flooding the Output panel.
#### Master
- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — Library router and mirrored module entry; open when discovering which Domain Skill owns a cross-cutting debug or perf concern.