blob: 24ce39ad85d786cf959981134d5a4d57f32fa45e [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/android/battery_metrics.h"
#include "base/android/application_status_listener.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/power_monitor/energy_monitor_android.h"
#include "base/power_monitor/power_monitor.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/trace_event/application_state_proto_android.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "base/tracing/protos/chrome_track_event.pbzero.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/blink/public/mojom/webpreferences/web_preferences.mojom.h"
namespace content {
namespace {
using ScenarioScope = performance_scenarios::ScenarioScope;
using LoadingScenario = performance_scenarios::LoadingScenario;
using InputScenario = performance_scenarios::InputScenario;
using Subsystem = AndroidBatteryMetrics::EnergyConsumedTracker::Subsystem;
perfetto::protos::pbzero::DeviceThermalState ToTraceEnum(
base::PowerThermalObserver::DeviceThermalState state) {
switch (state) {
case base::PowerThermalObserver::DeviceThermalState::kUnknown:
return perfetto::protos::pbzero::DEVICE_THERMAL_STATE_UNKNOWN;
case base::PowerThermalObserver::DeviceThermalState::kNominal:
return perfetto::protos::pbzero::DEVICE_THERMAL_STATE_NOMINAL;
case base::PowerThermalObserver::DeviceThermalState::kFair:
return perfetto::protos::pbzero::DEVICE_THERMAL_STATE_FAIR;
case base::PowerThermalObserver::DeviceThermalState::kSerious:
return perfetto::protos::pbzero::DEVICE_THERMAL_STATE_SERIOUS;
case base::PowerThermalObserver::DeviceThermalState::kCritical:
return perfetto::protos::pbzero::DEVICE_THERMAL_STATE_CRITICAL;
}
}
std::string GetLoadingScenarioSuffix(std::optional<LoadingScenario> scenario) {
// LINT.IfChange(LoadingScenarioSuffix)
if (!scenario.has_value()) {
return "UnknownLoadingScenario";
}
switch (*scenario) {
case LoadingScenario::kFocusedPageLoading:
return "FocusedPageLoading";
case LoadingScenario::kVisiblePageLoading:
return "VisiblePageLoading";
case LoadingScenario::kBackgroundPageLoading:
return "BackgroundPageLoading";
case LoadingScenario::kNoPageLoading:
return "NoPageLoading";
}
// LINT.ThenChange(/tools/metrics/histograms/metadata/power/histograms.xml:LoadingScenarioSuffix)
NOTREACHED();
}
std::string GetInputScenarioSuffix(std::optional<InputScenario> scenario) {
// LINT.IfChange(InputScenarioSuffix)
if (!scenario.has_value()) {
return "UnknownInputScenario";
}
switch (*scenario) {
case InputScenario::kScroll:
return "Scroll";
case InputScenario::kTap:
return "Tap";
case InputScenario::kTyping:
return "Typing";
case InputScenario::kNoInput:
return "NoInput";
}
// LINT.ThenChange(/tools/metrics/histograms/metadata/power/histograms.xml:InputScenarioSuffix)
NOTREACHED();
}
std::string GetSubsystemSuffix(Subsystem subsystem) {
// LINT.IfChange(SubsystemSuffix)
switch (subsystem) {
case Subsystem::kCpu:
return "Cpu";
case Subsystem::kGpu:
return "Gpu";
case Subsystem::kDisplay:
return "Display";
case Subsystem::kOther:
return "Other";
}
// LINT.ThenChange(/tools/metrics/histograms/metadata/power/histograms.xml:SubsystemSuffix)
NOTREACHED();
}
Subsystem ClassifyConsumer(std::string_view consumer) {
if (consumer.find("CPU") != std::string::npos) {
return Subsystem::kCpu;
}
if (consumer.find("GPU") != std::string::npos) {
return Subsystem::kGpu;
}
if (consumer.find("DISPLAY") != std::string::npos) {
return Subsystem::kDisplay;
}
return Subsystem::kOther;
}
base::flat_map<Subsystem, int64_t> AttributePowerMonitorReadingsToSubsystems(
const std::vector<base::android::PowerMonitorReading>& readings) {
base::flat_map<Subsystem, int64_t> energy_per_subsystem;
for (const auto& reading : readings) {
energy_per_subsystem[ClassifyConsumer(reading.consumer)] +=
reading.total_energy;
}
return energy_per_subsystem;
}
void ReportPerSubsystemEnergyDeltas(
const std::vector<AndroidBatteryMetrics::EnergyConsumedTracker::Delta>&
energy_deltas,
const std::string& base_histogram_name) {
// If there're no energy deltas for any subsystem, there's an error in the
// latest or the previous reading. Do not report any metrics.
if (energy_deltas.empty()) {
return;
}
int64_t total_energy_consumed_mwh = 0;
for (const auto& delta : energy_deltas) {
total_energy_consumed_mwh += delta.energy_consumed_mwh;
base::UmaHistogramCounts100000(
base::StrCat(
{base_histogram_name, ".", GetSubsystemSuffix(delta.subsystem)}),
delta.energy_consumed_mwh);
}
base::UmaHistogramCounts100000(base_histogram_name,
total_energy_consumed_mwh);
}
void Report30SecondDrain(
int capacity_consumed,
const std::vector<AndroidBatteryMetrics::EnergyConsumedTracker::Delta>&
energy_deltas,
bool is_exclusive_measurement,
const std::string& scenario) {
// Drain over the last 30 seconds in uAh. We assume a max current of 10A which
// translates to a little under 100mAh capacity drain over 30 seconds.
UMA_HISTOGRAM_COUNTS_100000("Power.ForegroundBatteryDrain.30Seconds",
capacity_consumed);
base::UmaHistogramCounts100000(
std::string("Power.ForegroundBatteryDrainPerScenario.30Seconds.") +
scenario,
capacity_consumed);
ReportPerSubsystemEnergyDeltas(
energy_deltas,
std::string("Power.ForegroundEnergyConsumedPerScenario.30Seconds.") +
scenario);
// Record a separate metric for power drain that was completely observed while
// we were the foreground app. This avoids attributing power draw from other
// apps to us.
if (is_exclusive_measurement) {
UMA_HISTOGRAM_COUNTS_100000(
"Power.ForegroundBatteryDrain.30Seconds.Exclusive", capacity_consumed);
base::UmaHistogramCounts100000(
std::string(
"Power.ForegroundBatteryDrainPerScenario.30Seconds.Exclusive.") +
scenario,
capacity_consumed);
ReportPerSubsystemEnergyDeltas(
energy_deltas,
std::string(
"Power.ForegroundEnergyConsumedPerScenario.30Seconds.Exclusive.") +
scenario);
}
}
base::HistogramBase* GetAvgBatteryDrainHistogram(const char* suffix) {
static constexpr char kAvgDrainHistogramPrefix[] =
"Power.ForegroundBatteryDrain.30SecondsAvg2";
return base::Histogram::FactoryGet(
std::string(kAvgDrainHistogramPrefix) + suffix, 1, 100000, 50,
base::HistogramBase::kUmaTargetedHistogramFlag);
}
// Dark mode histograms are reported on the UI thread, because they depend on
// the current darkening state of the web contents -- which we can only inspect
// on the UI thread.
void ReportDarkModeDrains(int capacity_consumed_avg,
bool is_exclusive_measurement,
int num_sampling_periods) {
size_t no_darkening_count = 0, user_agent_darkening_count = 0,
web_page_or_user_agent_darkening_count = 0,
web_page_darkening_count = 0;
auto all_webcontents = WebContentsImpl::GetAllWebContents();
auto total_webcontents_count = all_webcontents.size();
for (WebContentsImpl* wc : all_webcontents) {
auto dark_theme = wc->GetOrCreateWebPreferences().preferred_color_scheme ==
blink::mojom::PreferredColorScheme::kDark;
auto force_dark = wc->GetOrCreateWebPreferences().force_dark_mode_enabled;
if (force_dark) {
if (dark_theme) {
web_page_or_user_agent_darkening_count++;
} else {
user_agent_darkening_count++;
}
} else {
if (dark_theme) {
web_page_darkening_count++;
} else {
no_darkening_count++;
}
}
}
base::HistogramBase* dark_mode_histogram = nullptr;
base::HistogramBase* exclusive_dark_mode_histogram = nullptr;
if (user_agent_darkening_count + web_page_or_user_agent_darkening_count ==
total_webcontents_count) {
// All WebContents have at least user-agent darkening.
dark_mode_histogram = GetAvgBatteryDrainHistogram(".ForcedDarkMode");
exclusive_dark_mode_histogram =
GetAvgBatteryDrainHistogram(".Exclusive.ForcedDarkMode");
} else if (web_page_darkening_count == total_webcontents_count) {
// All WebContents have only web page darkening.
dark_mode_histogram = GetAvgBatteryDrainHistogram(".DarkMode");
exclusive_dark_mode_histogram =
GetAvgBatteryDrainHistogram(".Exclusive.DarkMode");
} else if (no_darkening_count == total_webcontents_count) {
// None of the WebContents have any darkening.
dark_mode_histogram = GetAvgBatteryDrainHistogram(".LightMode");
exclusive_dark_mode_histogram =
GetAvgBatteryDrainHistogram(".Exclusive.LightMode");
} else {
// Some WebContents have some kind of darkening and some might not have any.
dark_mode_histogram = GetAvgBatteryDrainHistogram(".MixedMode");
exclusive_dark_mode_histogram =
GetAvgBatteryDrainHistogram(".Exclusive.MixedMode");
}
DCHECK(dark_mode_histogram);
DCHECK(exclusive_dark_mode_histogram);
dark_mode_histogram->AddCount(capacity_consumed_avg, num_sampling_periods);
if (is_exclusive_measurement) {
exclusive_dark_mode_histogram->AddCount(capacity_consumed_avg,
num_sampling_periods);
}
}
void ReportAveragedDrain(int capacity_consumed,
bool is_exclusive_measurement,
int num_sampling_periods) {
// Averaged drain over 30 second intervals in uAh. We assume a max current of
// 10A which translates to a little under 100mAh capacity drain over 30
// seconds.
auto capacity_consumed_avg = capacity_consumed / num_sampling_periods;
GetAvgBatteryDrainHistogram("")->AddCount(capacity_consumed_avg,
num_sampling_periods);
if (is_exclusive_measurement) {
GetAvgBatteryDrainHistogram(".Exclusive")
->AddCount(capacity_consumed_avg, num_sampling_periods);
}
GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
->PostTask(
FROM_HERE,
base::BindOnce(&ReportDarkModeDrains, capacity_consumed_avg,
is_exclusive_measurement, num_sampling_periods));
}
} // namespace
// static
constexpr base::TimeDelta AndroidBatteryMetrics::kMetricsInterval;
// static
void AndroidBatteryMetrics::CreateInstance() {
static base::NoDestructor<AndroidBatteryMetrics> instance;
}
AndroidBatteryMetrics::AndroidBatteryMetrics()
: task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
DETACH_FROM_SEQUENCE(sequence_checker_);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&AndroidBatteryMetrics::InitializeOnSequence,
base::Unretained(this)));
}
AndroidBatteryMetrics::~AndroidBatteryMetrics() {
// Never called, this is a no-destruct singleton.
NOTREACHED();
}
void AndroidBatteryMetrics::InitializeOnSequence() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto* power_monitor = base::PowerMonitor::GetInstance();
battery_power_status_ =
power_monitor->AddPowerStateObserverAndReturnBatteryPowerStatus(this);
power_monitor->AddPowerThermalObserver(this);
// The observer is never removed because this class uses base::NoDestructor.
content::ProcessVisibilityTracker::GetInstance()->AddObserver(this);
// TODO(b/339859756): Update this call to take into account the unknown battery
// status.
UpdateMetricsEnabled();
}
void AndroidBatteryMetrics::TryObservePerformanceScenarios() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_observing_performance_scenarios_) {
return;
}
// We're using ScenarioScope::kGlobal because we're interested in the overall
// app state, not just the current process.
if (auto observer_list =
performance_scenarios::PerformanceScenarioObserverList::GetForScope(
ScenarioScope::kGlobal)) {
is_observing_performance_scenarios_ = true;
performance_scenario_tracker_.UpdateLoadingScenario(
performance_scenarios::GetLoadingScenario(ScenarioScope::kGlobal)
->load(std::memory_order_relaxed));
performance_scenario_tracker_.UpdateInputScenario(
performance_scenarios::GetInputScenario(ScenarioScope::kGlobal)
->load(std::memory_order_relaxed));
observer_list->AddObserver(this);
}
}
void AndroidBatteryMetrics::OnVisibilityChanged(bool visible) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
app_visible_ = visible;
UpdateMetricsEnabled();
}
void AndroidBatteryMetrics::OnBatteryPowerStatusChange(
base::PowerStateObserver::BatteryPowerStatus battery_power_status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
battery_power_status_ = battery_power_status;
UpdateMetricsEnabled();
}
void AndroidBatteryMetrics::OnLoadingScenarioChanged(
ScenarioScope scope,
LoadingScenario old_scenario,
LoadingScenario new_scenario) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
performance_scenario_tracker_.UpdateLoadingScenario(new_scenario);
}
void AndroidBatteryMetrics::OnInputScenarioChanged(ScenarioScope scope,
InputScenario old_scenario,
InputScenario new_scenario) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
performance_scenario_tracker_.UpdateInputScenario(new_scenario);
}
void AndroidBatteryMetrics::OnThermalStateChange(DeviceThermalState new_state) {
TRACE_EVENT_INSTANT(
"base.power", "OnThermalStateChange", perfetto::Track::Global(0),
[&](perfetto::EventContext ctx) {
auto* event = ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>();
event->set_chrome_application_state_info()->set_application_state(
base::trace_event::ApplicationStateToTraceEnum(
base::android::ApplicationStatusListener::GetState()));
event->set_device_thermal_state(ToTraceEnum(new_state));
});
}
void AndroidBatteryMetrics::OnSpeedLimitChange(int speed_limit) {}
void AndroidBatteryMetrics::UpdateMetricsEnabled() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TryObservePerformanceScenarios();
// We want to attribute battery drain to chromium while the embedding app is
// visible. Battery drain will only be reflected in remaining battery capacity
// when the device is not on a charger.
bool should_be_enabled =
app_visible_ && (battery_power_status_ ==
PowerStateObserver::BatteryPowerStatus::kBatteryPower);
if (should_be_enabled && !metrics_timer_.IsRunning()) {
// Capture first capacity measurement and enable the repeating timer.
last_remaining_capacity_uah_ = base::android::GetRemainingBatteryCapacity();
energy_consumed_tracker_.UpdatePowerMonitorReadings(
base::android::GetTotalEnergyConsumed());
skipped_timers_ = 0;
observed_capacity_drops_ = 0;
performance_scenario_tracker_.UseLatestScenarios();
metrics_timer_.Start(
FROM_HERE, kMetricsInterval,
base::BindRepeating(&AndroidBatteryMetrics::CaptureAndReportMetrics,
base::Unretained(this),
/*disabling=*/false));
} else if (!should_be_enabled && metrics_timer_.IsRunning()) {
// Capture one last measurement before disabling the timer.
CaptureAndReportMetrics(/*disabling=*/true);
metrics_timer_.Stop();
}
}
void AndroidBatteryMetrics::CaptureAndReportMetrics(bool disabling) {
int remaining_capacity_uah = base::android::GetRemainingBatteryCapacity();
const auto power_monitor_readings = base::android::GetTotalEnergyConsumed();
std::vector<EnergyConsumedTracker::Delta> energy_deltas;
// The underlying API has throttling (in which case the old value is reported
// again, but no errors), so if we call this method because the battery state
// is changing rather than by timer, the deltas are likely to be
// (misleadingly) 0. Let's not report such values.
if (!disabling) {
energy_deltas = energy_consumed_tracker_.GetDeltas(power_monitor_readings);
}
energy_consumed_tracker_.UpdatePowerMonitorReadings(power_monitor_readings);
const std::string scenario = performance_scenario_tracker_.GetMetricSuffix();
performance_scenario_tracker_.UseLatestScenarios();
if (remaining_capacity_uah >= last_remaining_capacity_uah_) {
// No change in battery capacity, or it increased. The latter could happen
// if we detected the switch off battery power to a charger late, or if the
// device reports bogus values. We don't change last_remaining_capacity_uah_
// here to avoid overreporting in case of fluctuating values.
skipped_timers_++;
Report30SecondDrain(0, energy_deltas, IsMeasuringDrainExclusively(),
scenario);
if (disabling) {
// Disabling the timer, but without a change in capacity counter -- We
// should still emit values for the elapsed time intervals into the
// average histograms. We exclude exclusive metrics here, because these
// metrics exclude the measurements before the first capacity drop and
// after the last drop. Member fields will be reset when tracking
// is resumed after foregrounding again later.
ReportAveragedDrain(0, /*is_exclusive_measurement=*/false,
skipped_timers_);
}
return;
}
observed_capacity_drops_++;
// Report the consumed capacity delta over the last 30 seconds.
int capacity_consumed = last_remaining_capacity_uah_ - remaining_capacity_uah;
Report30SecondDrain(capacity_consumed, energy_deltas,
IsMeasuringDrainExclusively(), scenario);
// Also record drain over 30 second intervals, but averaged since the last
// time we recorded an increase (or started recording samples). Because the
// underlying battery capacity counter is often low-resolution (usually
// between .5 and 50 mAh), it may only increment after multiple sampling
// points. For example, a 20 mAh drop over two successive periods of 30
// seconds will be reported as two samples of 10 mAh.
ReportAveragedDrain(capacity_consumed, IsMeasuringDrainExclusively(),
skipped_timers_ + 1);
// Also track the total capacity consumed in a single-bucket-histogram,
// emitting one sample for every 100 uAh drained.
static constexpr base::Histogram::Sample32 kSampleFactor = 100;
UMA_HISTOGRAM_SCALED_EXACT_LINEAR("Power.ForegroundBatteryDrain",
/*sample=*/1, capacity_consumed,
/*sample_max=*/1, kSampleFactor);
if (IsMeasuringDrainExclusively()) {
UMA_HISTOGRAM_SCALED_EXACT_LINEAR("Power.ForegroundBatteryDrain.Exclusive",
/*sample=*/1, capacity_consumed,
/*sample_max=*/1, kSampleFactor);
}
last_remaining_capacity_uah_ = remaining_capacity_uah;
skipped_timers_ = 0;
}
bool AndroidBatteryMetrics::IsMeasuringDrainExclusively() const {
return observed_capacity_drops_ >= 2;
}
void AndroidBatteryMetrics::PerformanceScenarioTracker::UpdateLoadingScenario(
performance_scenarios::LoadingScenario new_scenario) {
latest_loading_scenario_ = new_scenario;
if (loading_scenario_to_report_.value_or(LoadingScenario::kNoPageLoading) <=
new_scenario) {
loading_scenario_to_report_ = new_scenario;
}
}
void AndroidBatteryMetrics::PerformanceScenarioTracker::UpdateInputScenario(
performance_scenarios::InputScenario new_scenario) {
latest_input_scenario_ = new_scenario;
if (input_scenario_to_report_.value_or(InputScenario::kNoInput) <=
new_scenario) {
input_scenario_to_report_ = new_scenario;
}
}
std::string AndroidBatteryMetrics::PerformanceScenarioTracker::GetMetricSuffix()
const {
return GetLoadingScenarioSuffix(loading_scenario_to_report_) + "_" +
GetInputScenarioSuffix(input_scenario_to_report_);
}
void AndroidBatteryMetrics::PerformanceScenarioTracker::UseLatestScenarios() {
loading_scenario_to_report_ = latest_loading_scenario_;
input_scenario_to_report_ = latest_input_scenario_;
}
AndroidBatteryMetrics::EnergyConsumedTracker::EnergyConsumedTracker() = default;
AndroidBatteryMetrics::EnergyConsumedTracker::~EnergyConsumedTracker() =
default;
void AndroidBatteryMetrics::EnergyConsumedTracker::UpdatePowerMonitorReadings(
const std::vector<base::android::PowerMonitorReading>& readings) {
last_total_energy_uws_ = AttributePowerMonitorReadingsToSubsystems(readings);
}
std::vector<AndroidBatteryMetrics::EnergyConsumedTracker::Delta>
AndroidBatteryMetrics::EnergyConsumedTracker::GetDeltas(
const std::vector<base::android::PowerMonitorReading>& readings) const {
base::flat_map<Subsystem, int64_t> total_energy_uws =
AttributePowerMonitorReadingsToSubsystems(readings);
std::vector<Delta> deltas;
for (const auto& [subsystem, energy_uws] : total_energy_uws) {
// 0 total energy means an error, in which case we can't report the detla.
if (energy_uws == 0) {
continue;
}
auto it = last_total_energy_uws_.find(subsystem);
if (it == last_total_energy_uws_.end() || it->second == 0) {
continue;
}
int64_t delta_mwh =
std::max(static_cast<int64_t>(0), (energy_uws - it->second) / 3600);
deltas.push_back({subsystem, delta_mwh});
}
return deltas;
}
} // namespace content