Skip to content

Commit

Permalink
Add a Engine method to get the 1% percentile low FPS
Browse files Browse the repository at this point in the history
Measuring performance by only looking at the average FPS can be misleading,
as microstutters will not be represented in the final value.
This can lead to confusion as a game might have an average FPS of over 60,
yet it can feel very stuttery if its 1% low FPS are in the single digits.

This adds a Engine method and profiler readout for the 1% low FPS over
the last 100 rendered frames. This is a moving metric by definition,
but it still helps make these FPS drops more noticeable compared to an
average readout.
  • Loading branch information
Calinou committed Jul 24, 2024
1 parent e4f7b69 commit a3601f3
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 8 deletions.
2 changes: 2 additions & 0 deletions core/config/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Engine {
int ips = 60;
double physics_jitter_fix = 0.5;
double _fps = 1;
double _fps_1_percent_low = 1;
int _max_fps = 0;
int _audio_output_latency = 0;
double _time_scale = 1.0;
Expand Down Expand Up @@ -114,6 +115,7 @@ class Engine {
virtual int get_audio_output_latency() const;

virtual double get_frames_per_second() const { return _fps; }
virtual double get_frames_per_second_1_percent_low() const { return _fps_1_percent_low; }

uint64_t get_frames_drawn();

Expand Down
5 changes: 5 additions & 0 deletions core/core_bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,10 @@ double Engine::get_frames_per_second() const {
return ::Engine::get_singleton()->get_frames_per_second();
}

double Engine::get_frames_per_second_1_percent_low() const {
return ::Engine::get_singleton()->get_frames_per_second_1_percent_low();
}

uint64_t Engine::get_physics_frames() const {
return ::Engine::get_singleton()->get_physics_frames();
}
Expand Down Expand Up @@ -1827,6 +1831,7 @@ void Engine::_bind_methods() {

ClassDB::bind_method(D_METHOD("get_frames_drawn"), &Engine::get_frames_drawn);
ClassDB::bind_method(D_METHOD("get_frames_per_second"), &Engine::get_frames_per_second);
ClassDB::bind_method(D_METHOD("get_frames_per_second_1_percent_low"), &Engine::get_frames_per_second_1_percent_low);
ClassDB::bind_method(D_METHOD("get_physics_frames"), &Engine::get_physics_frames);
ClassDB::bind_method(D_METHOD("get_process_frames"), &Engine::get_process_frames);

Expand Down
1 change: 1 addition & 0 deletions core/core_bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ class Engine : public Object {
int get_max_fps() const;

double get_frames_per_second() const;
double get_frames_per_second_1_percent_low() const;
uint64_t get_physics_frames() const;
uint64_t get_process_frames() const;

Expand Down
6 changes: 6 additions & 0 deletions doc/classes/Engine.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
Returns the average frames rendered every second (FPS), also known as the framerate.
</description>
</method>
<method name="get_frames_per_second_1_percent_low" qualifiers="const">
<return type="float" />
<description>
Returns the lowest 1 percentile of the running project's framerate as a rolling value. In other words, this returns the FPS equivalent to the [i]worst[/i] (highest) frametime in the last 100 rendered frames. This value is generally a better indicator of overall smoothness compared to [method get_frames_per_second]. The value of [method get_frames_per_second_1_percent_low] should ideally always be greater than or equal to the target framerate to ensure a smooth experience (typically 60 FPS).
</description>
</method>
<method name="get_license_info" qualifiers="const">
<return type="Dictionary" />
<description>
Expand Down
9 changes: 6 additions & 3 deletions doc/classes/Performance.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
</methods>
<constants>
<constant name="TIME_FPS" value="0" enum="Monitor">
The number of frames rendered in the last second. This metric is only updated once per second, even if queried more often. [i]Higher is better.[/i]
The number of frames rendered in the last second (identical to [method Engine.get_frames_per_second]). This metric is only updated once per second, even if queried more often. [i]Higher is better.[/i]
</constant>
<constant name="TIME_PROCESS" value="1" enum="Monitor">
Time it took to complete one frame, in seconds. [i]Lower is better.[/i]
Expand Down Expand Up @@ -192,7 +192,7 @@
Number of islands in the 3D physics engine. [i]Lower is better.[/i]
</constant>
<constant name="AUDIO_OUTPUT_LATENCY" value="23" enum="Monitor">
Output latency of the [AudioServer]. Equivalent to calling [method AudioServer.get_output_latency], it is not recommended to call this every frame.
Output latency of the [AudioServer]. Equivalent to calling [method AudioServer.get_output_latency], it is not recommended to call this every frame. [i]Lower is better.[/i]
</constant>
<constant name="NAVIGATION_ACTIVE_MAPS" value="24" enum="Monitor">
Number of active navigation maps in the [NavigationServer3D]. This also includes the two empty default navigation maps created by World2D and World3D.
Expand Down Expand Up @@ -221,7 +221,10 @@
<constant name="NAVIGATION_EDGE_FREE_COUNT" value="32" enum="Monitor">
Number of navigation mesh polygon edges that could not be merged in the [NavigationServer3D]. The edges still may be connected by edge proximity or with links.
</constant>
<constant name="MONITOR_MAX" value="33" enum="Monitor">
<constant name="TIME_FPS_1_PERCENT_LOW" value="33" enum="Monitor">
The worst 1% percentile of frametimes rendered in the last second, converted to a FPS value (identical to [method Engine.get_frames_per_second_1_percent_low]). This metric may be updated less than once per second at low framerates, even if queried more often. [i]Higher is better.[/i]
</constant>
<constant name="MONITOR_MAX" value="34" enum="Monitor">
Represents the size of the [enum Monitor] enum.
</constant>
</constants>
Expand Down
23 changes: 21 additions & 2 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3978,6 +3978,7 @@ uint64_t Main::last_ticks = 0;
uint32_t Main::frames = 0;
uint32_t Main::hide_print_fps_attempts = 3;
uint32_t Main::frame = 0;
int Main::recent_frametimes[100] = {};
bool Main::force_redraw_requested = false;
int Main::iterating = 0;

Expand Down Expand Up @@ -4141,15 +4142,33 @@ bool Main::iteration() {
frames++;
Engine::get_singleton()->_process_frames++;

// Determine the frame that took the most time to render in the last 100 rendered frames.
recent_frametimes[Engine::get_singleton()->get_frames_drawn() % 100] = ticks_elapsed;
int frametime_1_percent_low = 1;
for (int i = 0; i < 100; i++) {
frametime_1_percent_low = MAX(frametime_1_percent_low, recent_frametimes[i]);
}
Engine::get_singleton()->_fps_1_percent_low = 1'000'000.0 / frametime_1_percent_low;

if (frame > 1000000) {
// Wait a few seconds before printing FPS, as FPS reporting just after the engine has started is inaccurate.
if (hide_print_fps_attempts == 0) {
if (editor || project_manager) {
if (print_fps) {
print_line(vformat("Editor FPS: %d (%s mspf)", frames, rtos(1000.0 / frames).pad_decimals(2)));
print_line(
vformat("Editor FPS: %d (%s mspf) - 1%% low: %d (%s mspf)",
frames,
rtos(1000.0 / frames).pad_decimals(2),
Engine::get_singleton()->_fps_1_percent_low,
rtos(1000.0 / Engine::get_singleton()->_fps_1_percent_low).pad_decimals(2)));
}
} else if (print_fps || GLOBAL_GET("debug/settings/stdout/print_fps")) {
print_line(vformat("Project FPS: %d (%s mspf)", frames, rtos(1000.0 / frames).pad_decimals(2)));
print_line(
vformat("Project FPS: %d (%s mspf) - 1%% low: %d (%s mspf)",
frames,
rtos(1000.0 / frames).pad_decimals(2),
Engine::get_singleton()->_fps_1_percent_low,
rtos(1000.0 / Engine::get_singleton()->_fps_1_percent_low).pad_decimals(2)));
}
} else {
hide_print_fps_attempts--;
Expand Down
1 change: 1 addition & 0 deletions main/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Main {
static uint32_t hide_print_fps_attempts;
static uint32_t frames;
static uint32_t frame;
static int recent_frametimes[100];
static bool force_redraw_requested;
static int iterating;

Expand Down
8 changes: 5 additions & 3 deletions main/performance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ void Performance::_bind_methods() {
BIND_ENUM_CONSTANT(NAVIGATION_EDGE_MERGE_COUNT);
BIND_ENUM_CONSTANT(NAVIGATION_EDGE_CONNECTION_COUNT);
BIND_ENUM_CONSTANT(NAVIGATION_EDGE_FREE_COUNT);
BIND_ENUM_CONSTANT(TIME_FPS_1_PERCENT_LOW);
BIND_ENUM_CONSTANT(MONITOR_MAX);
}

Expand Down Expand Up @@ -141,7 +142,7 @@ String Performance::get_monitor_name(Monitor p_monitor) const {
PNAME("navigation/edges_merged"),
PNAME("navigation/edges_connected"),
PNAME("navigation/edges_free"),

PNAME("time/fps_1_percent_low"),
};

return names[p_monitor];
Expand Down Expand Up @@ -225,7 +226,8 @@ double Performance::get_monitor(Monitor p_monitor) const {
return NavigationServer3D::get_singleton()->get_process_info(NavigationServer3D::INFO_EDGE_CONNECTION_COUNT);
case NAVIGATION_EDGE_FREE_COUNT:
return NavigationServer3D::get_singleton()->get_process_info(NavigationServer3D::INFO_EDGE_FREE_COUNT);

case TIME_FPS_1_PERCENT_LOW:
return Math::round(Engine::get_singleton()->get_frames_per_second_1_percent_low());
default: {
}
}
Expand Down Expand Up @@ -272,7 +274,7 @@ Performance::MonitorType Performance::get_monitor_type(Monitor p_monitor) const
MONITOR_TYPE_QUANTITY,
MONITOR_TYPE_QUANTITY,
MONITOR_TYPE_QUANTITY,

MONITOR_TYPE_QUANTITY,
};

return types[p_monitor];
Expand Down
1 change: 1 addition & 0 deletions main/performance.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Performance : public Object {
NAVIGATION_EDGE_MERGE_COUNT,
NAVIGATION_EDGE_CONNECTION_COUNT,
NAVIGATION_EDGE_FREE_COUNT,
TIME_FPS_1_PERCENT_LOW,
MONITOR_MAX
};

Expand Down

0 comments on commit a3601f3

Please sign in to comment.