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:
shenlei
2026-09-29 18:14:19 +09:00
co-authored by Claude Opus 5.5
parent c2cc093ed0
commit 82503315c6
9 changed files with 453 additions and 19 deletions
+47 -5
View File
@@ -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_;
};