
Godot Debugging Profiling
- 194 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-debugging-profiling for development tasks
About
godot-debugging-profiling: A skill for development. This provides functionality for development workflows.
- godot-debugging-profiling
Godot Debugging Profiling by the numbers
- 194 all-time installs (skills.sh)
- +14 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,058 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-debugging-profilingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 194 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-debugging-profiling for development tasks
Files
Debugging & Profiling
Expert guidance for finding and fixing bugs efficiently with Godot's debugging tools.
NEVER Do
- NEVER use `print()` without descriptive context —
print(value)is useless. Useprint("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
--releaseflag. - 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].
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
high_precision_benchmarker.gd
Micrometer-precision execution timing using Time.get_ticks_usec(), essential for identifying CPU micro-bottlenecks.
orphan_node_detector.gd
Automated detection and logging of "Orphan Nodes" (nodes removed from tree but not freed) using internal Performance monitors.
advanced_backtrace_recorder.gd
Capturing detailed script backtraces programmatically, including local variable snapshots for deep crash reporting.
engine_error_interceptor.gd
Intercepting underlying C++ engine errors and piping them to custom backend logs or analytics services.
custom_editor_monitor.gd
Exposing game-specific performance metrics (AI counts, bullet physics) directly to the Godot Editor's Debugger > Monitors tab.
debugger_tab_plugin.gd
Project-specific debugger extensions that inject custom visual tabs and data into the Godot bottom panel.
thread_safe_logger.gd
Mutext-locked logger subclass for thread-safe writing of logs from worker threads to external files.
custom_debug_draw.gd
Pro-level visualization patterns for non-visual data like pathfinding nodes, physics raycasts, and local AI influence maps.
break_on_condition.gd
Hardcoded breakpoint triggers for halting execution on invalid logic states in a team-agnostic manner.
remote_debug_console.gd
In-game command console for debugging mobile and console builds where standard terminal output is inaccessible.
Do NOT Load debug_overlay.gd in release builds - wrap usage in if OS.is_debug_build().---
Print Debugging
# Basic print
print("Value: ", some_value)
# Formatted print
print("Player at %s with health %d" % [position, health])
# Print with caller info
print_debug("Debug info here")
# Warning (non-fatal)
push_warning("This might be a problem")
# Error (non-fatal)
push_error("Something went wrong!")
# Assert (fatal in debug)
assert(health > 0, "Health cannot be negative!")Breakpoints
Set Breakpoint:
- Click line number gutter in script editor
- Or use
breakpointkeyword:
func suspicious_function() -> void:
breakpoint # Execution stops here
var result := calculate_something()Debugger Panel
Debug → Debugger (Ctrl+Shift+D)
Tabs:
- Stack Trace: Call stack when paused
- Variables: Inspect local/member variables
- Breakpoints: Manage all breakpoints
- Errors: Runtime errors and warnings
Remote Debug
Debug running game: 1. Run project (F5) 2. Debug → Remote Debug → Select running instance 3. Inspect live game state
Common Debugging Patterns
Null Reference
# ❌ Crash: null reference
$NonExistentNode.do_thing()
# ✅ Safe: check first
var node := get_node_or_null("MaybeExists")
if node:
node.do_thing()Track State Changes
var _health: int = 100
var health: int:
get:
return _health
set(value):
print("Health changed: %d → %d" % [_health, value])
print_stack() # Show who changed it
_health = valueVisualize Raycasts
func _draw() -> void:
if Engine.is_editor_hint():
draw_line(Vector2.ZERO, ray_direction * ray_length, Color.RED, 2.0)Debug Draw in 3D
# Use DebugDraw addon or create debug meshes
func debug_draw_sphere(pos: Vector3, radius: float) -> void:
var mesh := SphereMesh.new()
mesh.radius = radius
var instance := MeshInstance3D.new()
instance.mesh = mesh
instance.global_position = pos
add_child(instance)Error Handling
# Handle file errors
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()
var error := json.parse(file.get_as_text())
if error != OK:
push_error("JSON parse error: %s" % json.get_error_message())
return {}
return json.dataProfiler
Debug → Profiler (F3)
Time Profiler
- Shows function execution times
- Identify slow functions
- Target: < 16.67ms per frame (60 FPS)
Monitor
- FPS, physics, memory
- Object count
- Draw calls
Common Performance Issues
Issue: Low FPS
# Check in _process
func _process(delta: float) -> void:
print(Engine.get_frames_per_second()) # Monitor FPSIssue: Memory Leaks
# Check with print
func _exit_tree() -> void:
print("Node freed: ", name)
# Use groups to track
add_to_group("tracked")
print("Active objects: ", get_tree().get_nodes_in_group("tracked").size())Issue: Orphaned Nodes
# Check for orphans
func check_orphans() -> void:
print("Orphan nodes: ", Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT))Debug Console
# Runtime debug console
var console_visible := false
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.keycode == KEY_QUOTELEFT:
console_visible = not console_visible
$DebugConsole.visible = console_visibleBest Practices
1. Use Debug Flags
const DEBUG := true
func debug_log(message: String) -> void:
if DEBUG:
print("[DEBUG] ", message)2. Conditional Breakpoints
# Only break on specific condition
if player.health <= 0:
breakpoint3. Scene Tree Inspector
Debug → Remote Debug → Inspect scene tree
See live node hierarchyExpert Debugging Patterns
1. Automated-QA-Suite (Headless CI/CD)
Pattern for verifying game state in automated pipelines with deterministic exit codes.
- Headless Execution: Use
godot --headless -s test_runner.gdto run tests without a display server. - Verification: Evaluate state and call
get_tree().quit(0)for success orquit(1)for failure to pass exit codes back to the CI runner. - Implementation:
func _run() -> void: # Main entry for --script
var success := _run_all_tests()
if success:
print("[TEST_RESULT] PASS")
get_tree().quit(0)
else:
printerr("[TEST_RESULT] FAIL")
get_tree().quit(1)- CLI Flags: Use
--gpu-validationand--gpu-abortto catch driver-level errors in CI.
2. Visual-Profiler-Extensions (GPU Costs)
Custom diagnostic overlays to monitor rendering overhead in-game.
- Metric Querying: Use
RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME)for draw calls. - GPU Profiling: Enable
debug/settings/stdout/print_gpu_profilein Project Settings to dump a per-second breakdown of CanvasItem, shadow, and glow costs. - VRAM Tracking: Use
Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)to track total GPU memory consumption. - Implementation:
func _process(_delta):
var calls = RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME)
var primitives = RenderingServer.get_rendering_info(RenderingServer.RENDERING_INFO_TOTAL_PRIMITIVES_IN_FRAME)
# Display on-screen overlay3. Thread-Safety-Analyzer (Race Conditions)
Ensuring safe access to the SceneTree and data from worker threads.
- Safety Checks: Use
Thread.set_thread_safety_checks_enabled(true)to force Godot to throw errors when unsafe SceneTree access occurs from a thread. - Deferred Access: ALWAYS use
call_deferred()orset_deferred()when a worker thread needs to modify the SceneTree. - Server Safety: Servers (Rendering/Physics) are thread-safe ONLY if enabled in Project Settings under
threading/worker_pool/allow_group_tasks. - Implementation:
func _ready():
Thread.set_thread_safety_checks_enabled(true) # Global enforcement4. Memory-Leak-Tracker (Transient Scenes)
Identifying leaks in scenes that are instantiated and freed frequently.
- Orphan Detection: Periodically check
Node.get_orphan_node_ids(). If the count grows indefinitely after closing transient scenes, you have a leak. - ObjectDB Snapshots: Use the Godot 4.6 ObjectDB Profiler to take "Before" and "After" snapshots. Diffing these reveals exactly which
RefCountedobjects are causing circular reference leaks.
Reference
Related
- Master Skill: godot-master
# 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])
# 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
# 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
# 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()
# 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()
# 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
# 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)
# 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
# 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
# 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))
# 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")
# 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()
# 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.
# 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]
# 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 {}
# 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
# 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()
# 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)
# 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()
# 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