How the Godot Engine Profiler Tracks Performance Metrics and Frame Timing

The Godot Engine profiler tracks performance metrics and frame timing through a layered architecture involving the Performance singleton for raw data collection, the EngineProfiler abstraction for extensibility, and the RemoteDebugger for transmitting data to the editor UI.

The godotengine/godot repository implements a sophisticated profiling system that captures real-time performance data from the engine core and streams it to the editor for visualization. Understanding how the profiler tracks performance metrics and frame timing is essential for optimizing games and identifying bottlenecks in both development and production environments.

Core Data Collection via the Performance Singleton

The foundation of Godot's profiling system rests on the Performance singleton, a globally accessible object instantiated at engine startup in main/performance.cpp.

Frame Timing Updates in the Main Loop

Every frame, the main loop in main/main.cpp (lines 18–21) captures timing data and pushes it to the Performance singleton:

performance->set_process_time( USEC_TO_SEC(process_max) );
performance->set_physics_process_time( USEC_TO_SEC(physics_process_max) );
performance->set_navigation_process_time( USEC_TO_SEC(navigation_process_max) );

These calls store the duration of process, physics, and navigation steps as seconds, enabling the profiler to track frame timing with microsecond precision.

The Monitor Enum and Built-in Metrics

The Performance class defines a comprehensive Monitor enum in main/performance.h that categorizes metrics into timers, memory usage, object counts, and physics statistics. Built-in monitors include:

  • Time monitors: TIME_PROCESS, TIME_PHYSICS_PROCESS, TIME_NAVIGATION_PROCESS
  • Memory monitors: MEMORY_STATIC, MEMORY_DYNAMIC, MEMORY_MESSAGE_BUFFER
  • Object monitors: OBJECT_COUNT, OBJECT_RESOURCE_COUNT, OBJECT_NODE_COUNT
  • Physics monitors: PHYSICS_2D_ACTIVE_OBJECTS, PHYSICS_3D_ACTIVE_OBJECTS

Values are retrieved via Performance::get_monitor(Monitor p_monitor), which the profiler calls to aggregate data for transmission.

Custom Monitors for Extended Tracking

Developers can extend the profiler's capabilities through Performance::add_custom_monitor. This method accepts a name, a Callable that returns the metric value, and a MonitorType classification:

Performance::get_singleton()->add_custom_monitor(
    "live_particles",
    Callable(this, "get_live_particle_count"),
    PackedVector<int>(),
    Performance::MONITOR_TYPE_QUANTITY);

Custom monitors automatically appear in the editor's performance panel when the remote debugger is active.

The EngineProfiler Abstraction Layer

Godot decouples data collection from profiling implementations through the EngineProfiler interface defined in core/debugger/engine_profiler.h.

Virtual Interface Methods

The EngineProfiler class specifies three virtual methods that concrete implementations must override:

virtual void toggle(bool p_enable, const Array &p_opts);
virtual void add(const Array &p_data);
virtual void tick(double p_frame_time,
                  double p_process_time,
                  double p_physics_time,
                  double p_physics_frame_time);

The tick method receives frame timing data every frame, while toggle handles activation states and add receives arbitrary profiler-specific data arrays.

Registration with EngineDebugger

Profiler implementations register with the global EngineDebugger singleton via EngineProfiler::bind and unregister with unbind. The EngineDebugger maintains a list of active profilers and invokes their tick methods once per frame through EngineDebugger::profiler_tick, which originates from the main loop after frame timings are stored.

Remote Debugger Implementation

The RemoteDebugger in core/debugger/remote_debugger.cpp implements the bridge between the running game and the editor's profiler UI.

PerformanceProfiler Class

Nested within RemoteDebugger, the PerformanceProfiler class inherits from EngineProfiler and overrides the virtual interface to capture and transmit metrics. It maintains internal state to track when data was last sent and what custom monitors were previously registered.

Data Transmission Protocol

When active, PerformanceProfiler::tick executes once per second (throttled via last_perf_time comparison). During each execution:

  1. It retrieves current custom monitor names and types from the Performance singleton
  2. If the custom monitor list changed, it transmits a "performance:profile_names" message to the editor
  3. It builds an array containing all built-in monitor values via Performance::get_monitor(i) plus all custom monitor values
  4. It transmits the array via EngineDebugger::send_message("performance:profile_frame", arr)

This protocol ensures minimal overhead during gameplay while providing real-time updates in the editor.

Throttling and Frame Aggregation

The one-second throttling mechanism prevents network flooding and maintains game performance. The last_perf_time variable tracks the last transmission timestamp, and the tick method returns early if less than one second has elapsed since the last send.

Editor-Side Visualization

The editor receives profiler data through EditorPerformanceProfiler implemented in editor/debugger/editor_performance_profiler.cpp.

EditorPerformanceProfiler UI

This class manages the Debugger → Performance panel in the Godot editor. It maintains a hash map of monitor histories (List<float> history) indexed by monitor name. When a "performance:profile_frame" message arrives, it appends new values to the appropriate history lists and triggers a UI redraw.

Graph Rendering and History

The _monitor_draw method handles visualization:

  • It normalizes values based on the monitor's MonitorType (quantity, memory, time, percentage)
  • It formats labels via _format_label to display appropriate units (bytes, milliseconds, percentages)
  • It draws the historical data as line graphs with configurable time windows

The UI also provides a reset button (calling EditorPerformanceProfiler::reset) to clear histories and a marker system to flag specific frames for inspection.

Implementing Custom Performance Monitors

Developers can extend the profiler to track application-specific metrics using the Performance singleton's custom monitor API.

GDScript Example

extends Node

var enemy_count := 0

func _ready():
    Performance.add_custom_monitor(
        "gameplay/enemy_count",
        Callable(self, "_get_enemy_count"),
        [],
        Performance.MONITOR_TYPE_QUANTITY
    )

func _get_enemy_count() -> int:
    return enemy_count

C++ Module Example

// In your module's initialization
void MyModule::initialize() {
    Performance *perf = Performance::get_singleton();
    perf->add_custom_monitor(
        "my_module/active_tasks",
        callable_mp(this, &MyModule::get_active_task_count),
        Vector<Variant>(),
        Performance::MONITOR_TYPE_QUANTITY
    );
}

Custom monitors appear automatically in the editor's performance panel under the "Custom" category when the remote debugger is connected.

Summary

  • The Performance singleton (main/performance.cpp) collects frame timing data every frame in main/main.cpp and maintains built-in monitors for memory, objects, and physics.
  • The EngineProfiler abstraction (core/debugger/engine_profiler.h) defines the interface for profiler implementations, with tick called every frame to receive timing data.
  • The RemoteDebugger (core/debugger/remote_debugger.cpp) transmits performance data to the editor once per second via the PerformanceProfiler nested class.
  • The EditorPerformanceProfiler (editor/debugger/editor_performance_profiler.cpp) visualizes historical data as graphs in the Debugger → Performance panel.
  • Custom monitors can be added via Performance::add_custom_monitor to expose application-specific metrics in the editor.

Frequently Asked Questions

How does the Godot profiler collect frame timing data?

The profiler collects frame timing data through the Performance singleton, which receives updates every frame from the main loop in main/main.cpp. The main loop calls set_process_time, set_physics_process_time, and set_navigation_process_time with microsecond-precision measurements converted to seconds, storing these values for later retrieval by profiler implementations.

What is the difference between the Performance singleton and EngineProfiler?

The Performance singleton (main/performance.h) is a core engine component that stores raw metric values and frame timings, acting as a centralized data repository. EngineProfiler (core/debugger/engine_profiler.h) is an abstract interface that defines how profiling data is consumed; concrete implementations like RemoteDebugger::PerformanceProfiler register with the engine debugger to receive periodic updates from the Performance singleton and transmit them to the editor.

How can I add custom metrics to the Godot profiler?

You can add custom metrics by calling Performance::add_custom_monitor from GDScript or C++, passing a unique name, a Callable that returns the metric value, and a MonitorType classification (quantity, memory, time, or percentage). Once registered, these custom monitors automatically appear in the editor's Performance panel when remote debugging is active, updating once per second alongside built-in metrics.

Why does the profiler data update only once per second in the editor?

The profiler updates once per second to minimize network overhead and runtime performance impact. The RemoteDebugger::PerformanceProfiler::tick method implements throttling by tracking the last transmission time (last_perf_time) and returning early if less than one second has elapsed. This aggregation ensures that performance monitoring does not interfere with the game's frame rate while still providing sufficiently granular data for optimization work.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →