Been chasing a frame time spike for a week that only showed up mid-level, around 3 minutes into a run. Godot's built-in profiler is useful but I kept having to alt-tab to the editor during gameplay to check it, which obviously changes the conditions. Wanted something I could leave running in debug builds that shows per-system timing directly on screen.
Built a lightweight overlay using Time.get_ticks_usec() brackets. You register named zones, they accumulate per-frame, and the display shows the top offenders sorted by last-frame cost. Add it as an autoload and call it from anywhere:
class_name PerfOverlay
extends CanvasLayer
var _timings: Dictionary = {}
var _starts: Dictionary = {}
func begin(zone: String) -> void:
_starts[zone] = Time.get_ticks_usec()
func end(zone: String) -> void:
if zone not in _starts:
return
_timings[zone] = Time.get_ticks_usec() - _starts[zone]
_starts.erase(zone)
func _process(_delta: float) -> void:
if _timings.is_empty():
return
var keys = _timings.keys()
keys.sort_custom(func(a, b): return _timings[a] > _timings[b])
var text = ""
for k in keys.slice(0, 8):
text += "%s: %.2fms\n" % [k, _timings[k] / 1000.0]
$Label.text = text.strip_edges()
_timings.clear()
Usage anywhere in a node:
PerfOverlay.begin("pathfinding")
_run_pathfind()
PerfOverlay.end("pathfinding")
The _timings.clear() at end of _process means zones that didn't run that frame just vanish from the display. I actually like this. Tells you when systems are idle. Downside is if you forget to call end(), the measurement evaporates silently. Could add orphan detection but trying to keep it minimal.
What I found: my spatial hash lookup that I assumed was "fast" was consistently the second-worst offender after pathfinding. The actual spike turned out to be a debug draw call I'd left ungated — rebuilding a full line mesh every frame for a large visibility graph. Not behind an OS.is_debug_build() check. Classic.
Anyone else doing something like this or just living with the editor profiler? Also curious if there's a clean way to handle nested zones — right now overlapping begin/end pairs would give completely garbage numbers.