native: frame-rate setting, idle frame rate and battery power telemetry
- MtFrameRate setting (30 / 60 default / display max) as app.Get/SetFrameRateMode, with a "画面帧率" row injected into the system option dialog (mt_framerate); saved in the SDL pref path. - FrameRatePolicy in the live loop: 30 fps after 15 s without input (not while auto hunting), display switched to 60 Hz unless the highest mode, ADPF setPreferPowerEfficiency, vsync pacing when the display already runs at the target. --fps-mode / --idle-fps / --idle-after; --fps-cap / --refresh-rate still pin a test run. - Perf log: battery current/voltage/power, plugged, fps_mode, idle columns; perf_summary groups power by mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c2cc093ed0
commit
82503315c6
@@ -10,11 +10,18 @@
|
||||
// - PerformanceHint: ADPF APerformanceHint (API 33). The loop reports each frame's CPU work against the
|
||||
// frame budget, so the governor raises the clocks before a frame misses instead of idling the CPU at
|
||||
// its lowest step between short bursts (the 556-748 MHz seen in perf-20260929-153205.csv).
|
||||
// prefer_power_efficiency(): APerformanceHint_setPreferPowerEfficiency (API 35), set by the frame-rate
|
||||
// policy (main.cpp FrameRatePolicy) except in the highest mode.
|
||||
// - set_display_refresh_rate() / max_refresh_rate(): MainActivity switches the display mode at run time
|
||||
// for the frame-rate setting (60 Hz unless the setting wants more).
|
||||
// - battery_power(): BatteryManager current + voltage for the perf log's power columns; the power is only
|
||||
// meaningful off the charger (USB adb counts as a charger: test over wireless adb).
|
||||
//
|
||||
// Every NDK entry point is looked up at run time: minSdk is 24. On other platforms all of this is a no-op.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -50,11 +57,48 @@ inline double call_activity_float(const char* name) {
|
||||
return hz;
|
||||
}
|
||||
|
||||
inline void call_activity_set_float(const char* name, float value) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (!env || !activity) return;
|
||||
jclass cls = env->GetObjectClass(activity);
|
||||
if (jmethodID method = env->GetMethodID(cls, name, "(F)V"))
|
||||
env->CallVoidMethod(activity, method, jfloat(value));
|
||||
if (env->ExceptionCheck()) env->ExceptionClear();
|
||||
env->DeleteLocalRef(cls);
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
|
||||
inline double display_refresh_rate() { return call_activity_float("currentRefreshRate"); }
|
||||
// The highest refresh rate the display offers at its current resolution; -1 if unknown.
|
||||
inline double max_refresh_rate() { return call_activity_float("maxRefreshRate"); }
|
||||
// Switches the display mode to the refresh rate closest to `hz` (MainActivity.preferredDisplayModeId).
|
||||
inline void set_display_refresh_rate(float hz) { call_activity_set_float("setDisplayRefreshRate", hz); }
|
||||
// The app-vsync rate (Choreographer); -1 until the first measurement.
|
||||
inline double render_rate() { return call_activity_float("currentRenderRate"); }
|
||||
inline double battery_celsius() { return call_activity_float("batteryTemperature"); }
|
||||
|
||||
struct BatteryPower {
|
||||
double current_ma = 0; // drawn from the battery, positive while discharging
|
||||
double voltage_mv = -1;
|
||||
double power_mw = -1; // current x voltage; -1 while plugged in or unknown
|
||||
bool plugged = false;
|
||||
};
|
||||
|
||||
// BatteryManager's CURRENT_NOW is uA by the docs but mA on some vendors, and its sign convention varies:
|
||||
// a game draws well over 20 mA, so a magnitude above 20000 can only be uA; unplugged, the whole current is
|
||||
// the draw. Plugged in, the charger supplies part of it and the reading says nothing about the game.
|
||||
inline BatteryPower battery_power() {
|
||||
BatteryPower out;
|
||||
const double raw = call_activity_float("batteryCurrentRaw");
|
||||
out.current_ma = std::fabs(raw) > 20000.0 ? std::fabs(raw) / 1000.0 : std::fabs(raw);
|
||||
out.voltage_mv = call_activity_float("batteryVoltage");
|
||||
out.plugged = call_activity_float("batteryPlugged") > 0.5;
|
||||
if (!out.plugged && out.current_ma > 0 && out.voltage_mv > 0)
|
||||
out.power_mw = out.current_ma * out.voltage_mv / 1000.0;
|
||||
return out;
|
||||
}
|
||||
|
||||
// false when the call is unavailable (API < 30) or rejected.
|
||||
inline bool request_frame_rate(SDL_Window* window, float fps) {
|
||||
using SetFrameRate = int32_t (*)(ANativeWindow*, float, int8_t);
|
||||
@@ -79,6 +123,8 @@ public:
|
||||
update_target_ = reinterpret_cast<UpdateTarget>(dlsym(lib, "APerformanceHint_updateTargetWorkDuration"));
|
||||
report_ = reinterpret_cast<Report>(dlsym(lib, "APerformanceHint_reportActualWorkDuration"));
|
||||
close_ = reinterpret_cast<Close>(dlsym(lib, "APerformanceHint_closeSession"));
|
||||
prefer_efficiency_ = reinterpret_cast<PreferEfficiency>(
|
||||
dlsym(lib, "APerformanceHint_setPreferPowerEfficiency"));
|
||||
if (!get_manager_ || !create_ || !report_ || !close_) return false;
|
||||
void* manager = get_manager_();
|
||||
if (!manager) return false;
|
||||
@@ -103,17 +149,29 @@ public:
|
||||
if (session_ && actual_ns > 0) report_(session_, actual_ns);
|
||||
}
|
||||
|
||||
// API 35: the scheduler may favour efficiency cores and lower clocks for the session's threads.
|
||||
// false when unavailable.
|
||||
bool prefer_power_efficiency(bool enabled) {
|
||||
if (!session_ || !prefer_efficiency_) return false;
|
||||
if (enabled == prefer_efficiency_on_) return true;
|
||||
prefer_efficiency_on_ = enabled;
|
||||
return prefer_efficiency_(session_, enabled) == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
using GetManager = void* (*)();
|
||||
using CreateSession = void* (*)(void*, const int32_t*, size_t, int64_t);
|
||||
using UpdateTarget = int (*)(void*, int64_t);
|
||||
using Report = int (*)(void*, int64_t);
|
||||
using Close = void (*)(void*);
|
||||
using PreferEfficiency = int (*)(void*, bool);
|
||||
GetManager get_manager_ = nullptr;
|
||||
CreateSession create_ = nullptr;
|
||||
UpdateTarget update_target_ = nullptr;
|
||||
Report report_ = nullptr;
|
||||
Close close_ = nullptr;
|
||||
PreferEfficiency prefer_efficiency_ = nullptr;
|
||||
bool prefer_efficiency_on_ = false;
|
||||
void* session_ = nullptr;
|
||||
std::int64_t target_ns_ = 0;
|
||||
};
|
||||
@@ -125,7 +183,14 @@ inline double display_refresh_rate() {
|
||||
return mode ? double(mode->refresh_rate) : -1.0;
|
||||
}
|
||||
inline double render_rate() { return display_refresh_rate(); }
|
||||
inline double max_refresh_rate() { return display_refresh_rate(); }
|
||||
inline void set_display_refresh_rate(float) {}
|
||||
inline double battery_celsius() { return -1.0; }
|
||||
struct BatteryPower {
|
||||
double current_ma = 0, voltage_mv = -1, power_mw = -1;
|
||||
bool plugged = false;
|
||||
};
|
||||
inline BatteryPower battery_power() { return {}; }
|
||||
inline bool request_frame_rate(SDL_Window*, float) { return false; }
|
||||
class PerformanceHint {
|
||||
public:
|
||||
@@ -133,6 +198,7 @@ public:
|
||||
bool active() const { return false; }
|
||||
void set_target(std::int64_t) {}
|
||||
void report(std::int64_t) {}
|
||||
bool prefer_power_efficiency(bool) { return false; }
|
||||
};
|
||||
inline int32_t current_thread_id() { return 0; }
|
||||
#endif
|
||||
|
||||
+149
-7
@@ -12,6 +12,7 @@
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#include "platform/UserInterface/ServerClock.h"
|
||||
#include "platform/EterBase/TraceErrorObserver.h"
|
||||
#include "platform/EterBase/FrameRateMode.h"
|
||||
#include "../tests/port_login_flow_server.h"
|
||||
#include <thread>
|
||||
#endif
|
||||
@@ -1033,6 +1034,13 @@ public:
|
||||
out.write(reinterpret_cast<const char*>(file.data()), std::streamsize(file.size()));
|
||||
}
|
||||
|
||||
// Seconds since the last touch, mouse or key event; 0 while a finger or mouse button is held (a held
|
||||
// joystick or attack button sends no events). The frame-rate policy's idle test.
|
||||
double input_idle_seconds() const {
|
||||
if (fingers_down_ > 0 || buttons_down_ > 0) return 0.0;
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - last_input_).count();
|
||||
}
|
||||
|
||||
void request_screenshot(std::string path, std::uint64_t frame) {
|
||||
screenshot_path_ = std::move(path);
|
||||
screenshot_frame_ = frame;
|
||||
@@ -1277,6 +1285,7 @@ public:
|
||||
#endif
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
note_input(event);
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
// Touches arrive as SDL_EVENT_FINGER_*; the touch-synthesized mouse copy
|
||||
// would register as a second finger (101) and turn every drag into a pinch.
|
||||
@@ -3570,6 +3579,27 @@ private:
|
||||
VkExtent2D extent_{};
|
||||
std::vector<VkImageView> image_views_;
|
||||
std::vector<VkImage> swapchain_images_;
|
||||
void note_input(const SDL_Event& event) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_FINGER_DOWN: ++fingers_down_; break;
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
case SDL_EVENT_FINGER_CANCELED: fingers_down_ = std::max(0, fingers_down_ - 1); break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN: ++buttons_down_; break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP: buttons_down_ = std::max(0, buttons_down_ - 1); break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WILL_ENTER_BACKGROUND:
|
||||
// An up event lost with the focus must not keep the client out of the idle rate for good.
|
||||
fingers_down_ = buttons_down_ = 0;
|
||||
break;
|
||||
case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_WHEEL:
|
||||
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: case SDL_EVENT_TEXT_INPUT:
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_DID_ENTER_FOREGROUND: break;
|
||||
default: return;
|
||||
}
|
||||
last_input_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
std::chrono::steady_clock::time_point last_input_ = std::chrono::steady_clock::now();
|
||||
int fingers_down_ = 0, buttons_down_ = 0;
|
||||
|
||||
// --screenshot-out: read back the swapchain image of one frame into a BMP file.
|
||||
std::string screenshot_path_;
|
||||
std::uint64_t screenshot_frame_ = 0;
|
||||
@@ -3739,6 +3769,74 @@ int g_fps_cap = 0;
|
||||
// --refresh-rate N: the surface's preferred frame rate (Android ANativeWindow_setFrameRate; MainActivity
|
||||
// also picks the matching display mode). 0 leaves the system default.
|
||||
int g_refresh_rate = 0;
|
||||
// --fps-mode N: the frame-rate setting for this run (a test override: the saved setting stays);
|
||||
// --idle-fps N: the rate after --idle-after S seconds without input (0 = never lower it).
|
||||
int g_fps_mode = -1;
|
||||
int g_idle_fps = 30;
|
||||
double g_idle_after_s = 15.0;
|
||||
|
||||
// The frame-rate setting (system option dialog, platform/EterBase/FrameRateMode.h) turned into a frame
|
||||
// rate, a display refresh rate and the ADPF budget; --fps-cap / --refresh-rate pin those for a test run.
|
||||
// Frames the player does not watch closely cost the same power as the ones they do, so after a while
|
||||
// without input (and no auto hunt running) the rate drops to --idle-fps until the next touch. The display
|
||||
// is asked for 60 Hz unless the setting wants more: a 120 Hz panel scanning out a 60 fps game costs power
|
||||
// for nothing.
|
||||
class FrameRatePolicy {
|
||||
public:
|
||||
struct Decision {
|
||||
int mode = -1;
|
||||
int fps = 0; // frames per second the loop aims at
|
||||
int display_hz = 0; // the refresh rate asked of the display
|
||||
bool idle = false;
|
||||
bool operator==(const Decision&) const = default;
|
||||
};
|
||||
|
||||
bool pinned() const { return g_fps_cap > 0 || g_refresh_rate > 0; }
|
||||
|
||||
void load() {
|
||||
path_ = settings_path();
|
||||
int mode = MtFrameRate::MODE_STANDARD;
|
||||
if (std::ifstream in{path_}; in) in >> mode;
|
||||
if (g_fps_mode >= 0) mode = g_fps_mode;
|
||||
MtFrameRate::Set(mode);
|
||||
saved_mode_ = MtFrameRate::Get();
|
||||
max_hz_ = int(std::lround(android_perf::max_refresh_rate()));
|
||||
if (max_hz_ < 30) max_hz_ = 60;
|
||||
}
|
||||
|
||||
Decision decide(double idle_seconds, bool auto_hunt) {
|
||||
Decision d;
|
||||
d.mode = MtFrameRate::Get();
|
||||
if (d.mode != saved_mode_) save(saved_mode_ = d.mode);
|
||||
const int active = d.mode == MtFrameRate::MODE_SAVER ? 30
|
||||
: d.mode == MtFrameRate::MODE_HIGH ? max_hz_ : std::min(60, max_hz_);
|
||||
d.idle = g_idle_fps > 0 && g_idle_fps < active && idle_seconds >= g_idle_after_s && !auto_hunt;
|
||||
d.fps = d.idle ? g_idle_fps : active;
|
||||
d.display_hz = d.mode == MtFrameRate::MODE_HIGH && !d.idle ? max_hz_ : std::min(60, max_hz_);
|
||||
return d;
|
||||
}
|
||||
|
||||
int max_hz() const { return max_hz_; }
|
||||
|
||||
private:
|
||||
static std::string settings_path() {
|
||||
std::string dir;
|
||||
if (char* pref = SDL_GetPrefPath("metin2port", "client")) {
|
||||
dir = pref;
|
||||
SDL_free(pref);
|
||||
}
|
||||
return dir + "frame_rate.cfg";
|
||||
}
|
||||
|
||||
void save(int mode) {
|
||||
std::ofstream out{path_, std::ios::trunc};
|
||||
out << mode << '\n';
|
||||
}
|
||||
|
||||
std::string path_;
|
||||
int saved_mode_ = -1;
|
||||
int max_hz_ = 60;
|
||||
};
|
||||
|
||||
bool py_exec(const std::string& code) {
|
||||
std::string err;
|
||||
@@ -4083,16 +4181,21 @@ int run_live_client(
|
||||
perf_log.set_refresh_rate_source([] { return android_perf::display_refresh_rate(); });
|
||||
perf_log.set_render_rate_source([] { return android_perf::render_rate(); });
|
||||
perf_log.set_battery_source([] { return android_perf::battery_celsius(); });
|
||||
perf_log.set_power_source([] {
|
||||
const auto p = android_perf::battery_power();
|
||||
return native_perf::PowerSample{p.current_ma, p.voltage_mv, p.power_mw, p.plugged};
|
||||
});
|
||||
// ADPF: each frame's CPU work (everything but the vsync/fence waits and the cap's sleep) against the
|
||||
// frame budget. Opened once the script thread exists, i.e. here.
|
||||
using steady = std::chrono::steady_clock;
|
||||
// The target is 80% of the frame period: the governor settles the clocks so the reported work just
|
||||
// meets the target, so a target equal to the period leaves every other frame late (cap 60 on the
|
||||
// test phone: 54 fps against the period, 60 against 80% of it).
|
||||
const auto frame_budget = [] {
|
||||
const int fps = g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60);
|
||||
return std::chrono::nanoseconds(800'000'000LL / fps);
|
||||
}();
|
||||
const auto budget_for = [](int fps) { return std::chrono::nanoseconds(800'000'000LL / std::max(1, fps)); };
|
||||
FrameRatePolicy policy;
|
||||
policy.load();
|
||||
const auto frame_budget = budget_for(
|
||||
g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60));
|
||||
android_perf::PerformanceHint hint;
|
||||
{
|
||||
std::vector<int32_t> tids{android_perf::current_thread_id()};
|
||||
@@ -4106,10 +4209,46 @@ int run_live_client(
|
||||
int completed = 0;
|
||||
std::vector<double> frame_ms;
|
||||
if (frames > 0) frame_ms.reserve(static_cast<std::size_t>(frames));
|
||||
const auto cap_period = g_fps_cap > 0 ? std::chrono::nanoseconds(1'000'000'000LL / g_fps_cap)
|
||||
: std::chrono::nanoseconds(0);
|
||||
auto cap_period = g_fps_cap > 0 ? std::chrono::nanoseconds(1'000'000'000LL / g_fps_cap)
|
||||
: std::chrono::nanoseconds(0);
|
||||
auto next_frame_at = steady::now();
|
||||
FrameRatePolicy::Decision decision;
|
||||
auto display_checked_at = steady::time_point{};
|
||||
// The loop sleeps to the policy's rate unless the display already runs at it (vsync then paces the
|
||||
// loop, and a second clock beating against it would drop frames).
|
||||
const auto apply_policy = [&] {
|
||||
if (policy.pinned()) return;
|
||||
const auto now = steady::now();
|
||||
const auto next = policy.decide(renderer.input_idle_seconds(), PythonBoot::AutoHuntIsEnabled());
|
||||
const bool changed = !(next == decision);
|
||||
if (changed) {
|
||||
if (next.display_hz != decision.display_hz) {
|
||||
android_perf::set_display_refresh_rate(float(next.display_hz));
|
||||
android_perf::request_frame_rate(renderer.sdl_window(), float(next.display_hz));
|
||||
}
|
||||
hint.set_target(budget_for(next.fps).count());
|
||||
// Efficiency cores and lower clocks, except where the setting asks for the highest rate.
|
||||
hint.prefer_power_efficiency(next.mode != MtFrameRate::MODE_HIGH || next.idle);
|
||||
perf_log.set_fps_cap(next.fps);
|
||||
perf_log.set_frame_policy(next.mode, next.idle);
|
||||
SDL_Log("frame rate: mode %d -> %d fps, display %d Hz%s", next.mode, next.fps, next.display_hz,
|
||||
next.idle ? " (idle)" : "");
|
||||
decision = next;
|
||||
}
|
||||
if (changed || now - display_checked_at > std::chrono::seconds(1)) {
|
||||
display_checked_at = now;
|
||||
const double hz = android_perf::display_refresh_rate();
|
||||
const bool vsync_paced = hz > 0 && std::fabs(hz - decision.fps) < 2.0;
|
||||
const auto period = vsync_paced ? std::chrono::nanoseconds(0)
|
||||
: std::chrono::nanoseconds(1'000'000'000LL / decision.fps);
|
||||
if (period != cap_period) {
|
||||
cap_period = period;
|
||||
next_frame_at = now;
|
||||
}
|
||||
}
|
||||
};
|
||||
while ((frames == 0 || completed < frames) && renderer.poll(true) && PythonBoot::IsAppLooping()) {
|
||||
apply_policy();
|
||||
double pace_ms = 0.0;
|
||||
if (cap_period.count()) {
|
||||
// Fixed cadence: a late frame starts the next one at once, a frame more than one period
|
||||
@@ -4255,6 +4394,9 @@ int main(int argc, char** argv) {
|
||||
else if (arg == "--perf-log") g_perf_log = true;
|
||||
else if (arg == "--fps-cap" && i+1 < argc) g_fps_cap = std::max(0, std::stoi(argv[++i]));
|
||||
else if (arg == "--refresh-rate" && i+1 < argc) g_refresh_rate = std::max(0, std::stoi(argv[++i]));
|
||||
else if (arg == "--fps-mode" && i+1 < argc) g_fps_mode = std::stoi(argv[++i]);
|
||||
else if (arg == "--idle-fps" && i+1 < argc) g_idle_fps = std::max(0, std::stoi(argv[++i]));
|
||||
else if (arg == "--idle-after" && i+1 < argc) g_idle_after_s = std::max(0.0, std::stod(argv[++i]));
|
||||
#endif
|
||||
else if (arg == "--fake-idle-ms" && i+1 < argc) setenv("MT_FAKE_IDLE_MS", argv[++i], 1);
|
||||
else if (arg == "--frames-in-flight" && i+1 < argc) frames_in_flight = std::stoi(argv[++i]);
|
||||
@@ -4266,7 +4408,7 @@ int main(int argc, char** argv) {
|
||||
"[--draws N] [--triangles-per-draw N] [--capture FILE] [--animate-first-draw] "
|
||||
"[--animate-bones] [--no-vsync] [--live-client DIR] [--fake-mobs N] "
|
||||
"[--gpu-skinning|--no-gpu-skinning] [--no-terrain] [--mobile] [--capture-out FILE] [--screenshot-out FILE.bmp] [--show-fps] [--perf-log] "
|
||||
"[--fps-cap N] [--refresh-rate HZ] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--fake-idle-ms MS] [--cpu-shadow]");
|
||||
"[--fps-cap N] [--refresh-rate HZ] [--fps-mode 0|1|2] [--idle-fps N] [--idle-after S] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--fake-idle-ms MS] [--cpu-shadow]");
|
||||
}
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (native_perf::PerfLog::requested_by_env()) g_perf_log = true;
|
||||
|
||||
@@ -47,6 +47,12 @@ struct RendererTotals {
|
||||
const std::string* last_texture = nullptr; // name of the texture uploaded last
|
||||
};
|
||||
|
||||
// One battery reading (android_perf::battery_power). power_mw < 0: unknown or on a charger.
|
||||
struct PowerSample {
|
||||
double current_ma = 0, voltage_mv = -1, power_mw = -1;
|
||||
bool plugged = false;
|
||||
};
|
||||
|
||||
struct FrameInput {
|
||||
double frame_ms = 0; // whole loop iteration
|
||||
double app_ms = 0; // PythonBoot::UIUpdate: the script thread's Process() plus the baton handoff
|
||||
@@ -61,6 +67,7 @@ public:
|
||||
static constexpr double kIntervalS = 2.0;
|
||||
static constexpr double kHitchMs = 100.0;
|
||||
static constexpr int kKeepFiles = 10;
|
||||
static constexpr double kPowerSampleS = 0.25;
|
||||
|
||||
static bool requested_by_env() {
|
||||
const char* env = std::getenv("MT_PERF_LOG");
|
||||
@@ -75,7 +82,11 @@ public:
|
||||
void set_render_rate_source(std::function<double()> source) { render_rate_ = std::move(source); }
|
||||
// Battery temperature in C (Android: the ACTION_BATTERY_CHANGED sticky intent; sysfs is SELinux-denied).
|
||||
void set_battery_source(std::function<double()> source) { battery_ = std::move(source); }
|
||||
// Sampled every kPowerSampleS and averaged over the row: one reading swings with each frame's burst.
|
||||
void set_power_source(std::function<PowerSample()> source) { power_ = std::move(source); }
|
||||
void set_fps_cap(int cap) { fps_cap_ = cap; }
|
||||
// The frame-rate setting (FrameRateMode.h) and whether the idle rate is in effect, as of the row's end.
|
||||
void set_frame_policy(int mode, bool idle) { fps_mode_ = mode; idle_ = idle; }
|
||||
const std::string& path() const { return path_; }
|
||||
|
||||
// `context` goes into the file header (device, present mode, MSAA, size...).
|
||||
@@ -104,7 +115,8 @@ public:
|
||||
"render_game_ms,shadow_raster_ms,py_ms,py_calls,py_missing,host_ms,render_ms,pace_ms,sync_ms,fence_ms,"
|
||||
"prepare_ms,geo_upload_ms,tex_upload_ms,"
|
||||
"submit_ms,present_ms,gpu_ms,draws,skinned_draws,ui_batches,vertices,geo_uploads,geo_kb,tex_uploads,tex_kb,"
|
||||
"script_cpu,main_cpu,cpu_mhz,cpu_max_mhz,thermal,batt_c,rss_mb,last_texture\n");
|
||||
"script_cpu,main_cpu,cpu_mhz,cpu_max_mhz,thermal,batt_c,rss_mb,batt_ma,batt_mv,power_mw,plugged,fps_mode,idle,"
|
||||
"last_texture\n");
|
||||
std::fflush(file_);
|
||||
MtPerf::Enabled() = true;
|
||||
MtPerf::Take();
|
||||
@@ -163,9 +175,25 @@ public:
|
||||
script_cpu_ = game.scriptCpu;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (power_ && std::chrono::duration<double>(now - power_sampled_at_).count() >= kPowerSampleS) {
|
||||
power_sampled_at_ = now;
|
||||
const PowerSample sample = power_();
|
||||
power_sum_.current_ma += sample.current_ma;
|
||||
power_sum_.voltage_mv += sample.voltage_mv;
|
||||
if (sample.power_mw >= 0) {
|
||||
power_sum_.power_mw += sample.power_mw;
|
||||
++power_valid_;
|
||||
}
|
||||
power_sum_.plugged = power_sum_.plugged || sample.plugged;
|
||||
++power_samples_;
|
||||
}
|
||||
const double elapsed = std::chrono::duration<double>(now - window_start_).count();
|
||||
if (elapsed < kIntervalS) return;
|
||||
write_row(elapsed, map_name());
|
||||
power_sum_ = {};
|
||||
power_sum_.voltage_mv = 0;
|
||||
power_sum_.power_mw = 0;
|
||||
power_samples_ = power_valid_ = 0;
|
||||
window_ = {};
|
||||
frame_times_.clear();
|
||||
window_start_ = now;
|
||||
@@ -354,13 +382,18 @@ private:
|
||||
const int main_cpu = current_cpu();
|
||||
const double refresh = refresh_rate_ ? refresh_rate_() : -1.0;
|
||||
const double render_rate = render_rate_ ? render_rate_() : -1.0;
|
||||
const double ns = double(power_samples_);
|
||||
const double batt_ma = power_samples_ ? power_sum_.current_ma / ns : -1.0;
|
||||
const double batt_mv = power_samples_ ? power_sum_.voltage_mv / ns : -1.0;
|
||||
// Only when every sample of the row was off the charger.
|
||||
const double power_mw = power_valid_ && power_valid_ == power_samples_ ? power_sum_.power_mw / ns : -1.0;
|
||||
std::fprintf(file_,
|
||||
"%.1f,%s,%d,%llu,%.1f,%.1f,%.1f,%d,%.2f,%.2f,%.2f,%d,%d,"
|
||||
"%.2f,%.2f,%.2f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,"
|
||||
"%.3f,%.3f,%.3f,%.1f,%.1f,%.3f,%.2f,%.2f,%.2f,%.2f,"
|
||||
"%.2f,%.3f,%.3f,"
|
||||
"%.3f,%.3f,%.2f,%llu,%llu,%llu,%llu,%.2f,%.1f,%.2f,%.1f,"
|
||||
"%d,%d,%s,%s,%d,%.1f,%.0f,%s\n",
|
||||
"%d,%d,%s,%s,%d,%.1f,%.0f,%.0f,%.0f,%.0f,%d,%d,%d,%s\n",
|
||||
t, csv_field(map).c_str(), actors_, (unsigned long long)w.frames, fps, refresh, render_rate, fps_cap_, w.frame_ms / n,
|
||||
p95, sorted.back(), jank33, jank50,
|
||||
w.app_ms / n, g(MtPerf::SECTION_PROCESS), (w.app_ms - w.game_ms[MtPerf::SECTION_PROCESS]) / n,
|
||||
@@ -373,17 +406,20 @@ private:
|
||||
w.submit_ms / n, w.present_ms / n, gpu, (unsigned long long)draws_, (unsigned long long)skinned_draws_,
|
||||
(unsigned long long)ui_batches_, (unsigned long long)vertices_, double(w.geo_uploads) / n,
|
||||
double(w.geo_bytes) / 1024.0 / n, double(w.tex_uploads) / n, double(w.tex_bytes) / 1024.0 / n,
|
||||
script_cpu_, main_cpu, mhz.c_str(), max_mhz.c_str(), thermal, batt, rss, csv_field(last_texture_).c_str());
|
||||
script_cpu_, main_cpu, mhz.c_str(), max_mhz.c_str(), thermal, batt, rss, batt_ma, batt_mv, power_mw,
|
||||
int(power_sum_.plugged), fps_mode_, int(idle_), csv_field(last_texture_).c_str());
|
||||
std::fflush(file_);
|
||||
if (!w.tex_uploads) last_texture_.clear();
|
||||
log("perf fps=%.1f @%.0fHz/%.0f cap=%d frame=%.1f/p95 %.1f/max %.1f jank33=%d | app=%.1f (upd=%.1f rnd=%.1f "
|
||||
"shadow=%.1f py=%.1f) | render=%.1f (sync=%.1f fence=%.1f prep=%.1f up=%.1f/%.1f) pace=%.1f gpu=%.1f | "
|
||||
"draws=%llu actors=%d map=%s thermal=%d batt=%.1f cpu=%s max=%s tex_uploads=%.2f last_tex=%s",
|
||||
"draws=%llu actors=%d map=%s thermal=%d batt=%.1f cpu=%s max=%s tex_uploads=%.2f | pwr=%.0fmW %.0fmA%s mode=%d%s "
|
||||
"last_tex=%s",
|
||||
fps, refresh, render_rate, fps_cap_, w.frame_ms / n, p95, sorted.back(), jank33, w.app_ms / n,
|
||||
g(MtPerf::SECTION_UPDATE_GAME), g(MtPerf::SECTION_RENDER_GAME), g(MtPerf::SECTION_SHADOW_RASTER),
|
||||
g(MtPerf::SECTION_PYTHON), w.render_ms / n, w.sync_ms / n, w.fence_ms / n, w.prepare_ms / n,
|
||||
w.geo_upload_ms / n, w.tex_upload_ms / n, w.pace_ms / n, gpu, (unsigned long long)draws_, actors_,
|
||||
map.c_str(), thermal, batt, mhz.c_str(), max_mhz.c_str(), double(w.tex_uploads) / n, last_texture_.c_str());
|
||||
map.c_str(), thermal, batt, mhz.c_str(), max_mhz.c_str(), double(w.tex_uploads) / n, power_mw, batt_ma,
|
||||
power_sum_.plugged ? " plugged" : "", fps_mode_, idle_ ? " idle" : "", last_texture_.c_str());
|
||||
}
|
||||
|
||||
void write_hitch(const Window& f, const MtPerf::SCounters& game) {
|
||||
@@ -413,6 +449,12 @@ private:
|
||||
std::function<double()> refresh_rate_;
|
||||
std::function<double()> render_rate_;
|
||||
std::function<double()> battery_;
|
||||
std::function<PowerSample()> power_;
|
||||
std::chrono::steady_clock::time_point power_sampled_at_{};
|
||||
PowerSample power_sum_{0, 0, 0, false};
|
||||
int power_samples_ = 0, power_valid_ = 0;
|
||||
int fps_mode_ = -1;
|
||||
bool idle_ = false;
|
||||
std::string last_texture_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user