| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/power_metrics/android_battery_metrics.h" |
| |
| #include "base/android/radio_utils.h" |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/optional.h" |
| #include "base/power_monitor/power_monitor.h" |
| #include "base/trace_event/trace_event.h" |
| #include "net/android/network_library.h" |
| #include "net/android/traffic_stats.h" |
| |
| const base::Feature kForegroundRadioStateCountWakeups{ |
| "ForegroundRadioStateCountWakeups", base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| namespace power_metrics { |
| namespace { |
| |
| void Report30SecondRadioUsage(int64_t tx_bytes, int64_t rx_bytes, int wakeups) { |
| if (!base::android::RadioUtils::IsSupported()) |
| return; |
| |
| if (base::android::RadioUtils::IsWifiConnected()) { |
| base::Optional<int32_t> maybe_level = net::android::GetWifiSignalLevel(); |
| if (!maybe_level.has_value()) |
| return; |
| |
| base::android::RadioSignalLevel wifi_level = |
| static_cast<base::android::RadioSignalLevel>(*maybe_level); |
| UMA_HISTOGRAM_ENUMERATION("Power.ForegroundRadio.SignalLevel.Wifi", |
| wifi_level); |
| |
| // Traffic sent over network during the last 30 seconds in kibibytes. |
| UMA_HISTOGRAM_SCALED_ENUMERATION( |
| "Power.ForegroundRadio.SentKiB.Wifi.30Seconds", wifi_level, tx_bytes, |
| 1024); |
| |
| // Traffic received over network during the last 30 seconds in kibibytes. |
| UMA_HISTOGRAM_SCALED_ENUMERATION( |
| "Power.ForegroundRadio.ReceivedKiB.Wifi.30Seconds", wifi_level, |
| rx_bytes, 1024); |
| } else { |
| base::Optional<base::android::RadioSignalLevel> maybe_level = |
| base::android::RadioUtils::GetCellSignalLevel(); |
| if (!maybe_level.has_value()) |
| return; |
| |
| base::android::RadioSignalLevel cell_level = *maybe_level; |
| UMA_HISTOGRAM_ENUMERATION("Power.ForegroundRadio.SignalLevel.Cell", |
| cell_level); |
| |
| // Traffic sent over network during the last 30 seconds in kibibytes. |
| UMA_HISTOGRAM_SCALED_ENUMERATION( |
| "Power.ForegroundRadio.SentKiB.Cell.30Seconds", cell_level, tx_bytes, |
| 1024); |
| |
| // Traffic received over network during the last 30 seconds in kibibytes. |
| UMA_HISTOGRAM_SCALED_ENUMERATION( |
| "Power.ForegroundRadio.ReceivedKiB.Cell.30Seconds", cell_level, |
| rx_bytes, 1024); |
| |
| // Number of radio wakeups during the last 30 seconds. |
| if (base::FeatureList::IsEnabled(kForegroundRadioStateCountWakeups) && |
| wakeups > 0) { |
| static const int kMaxLevel = |
| static_cast<int>(base::android::RadioSignalLevel::kMaxValue); |
| static const char kWakeupsHistogramName[] = |
| "Power.ForegroundRadio.Wakeups.Cell.30Seconds"; |
| STATIC_HISTOGRAM_POINTER_BLOCK( |
| kWakeupsHistogramName, |
| AddCount(static_cast<int>(cell_level), wakeups), |
| base::Histogram::FactoryGet( |
| kWakeupsHistogramName, 0, kMaxLevel, kMaxLevel + 1, |
| base::HistogramBase::kUmaTargetedHistogramFlag)); |
| } |
| } |
| } |
| |
| void Report30SecondDrain(int capacity_consumed, bool is_exclusive_measurement) { |
| // 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); |
| |
| // 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); |
| } |
| } |
| |
| 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. |
| static const char kName[] = "Power.ForegroundBatteryDrain.30SecondsAvg"; |
| STATIC_HISTOGRAM_POINTER_BLOCK( |
| kName, |
| AddCount(capacity_consumed / num_sampling_periods, num_sampling_periods), |
| base::Histogram::FactoryGet( |
| kName, /*min_value=*/1, /*max_value=*/100000, /*bucket_count=*/50, |
| base::HistogramBase::kUmaTargetedHistogramFlag)); |
| |
| if (is_exclusive_measurement) { |
| static const char kExclusiveName[] = |
| "Power.ForegroundBatteryDrain.30SecondsAvg.Exclusive"; |
| STATIC_HISTOGRAM_POINTER_BLOCK( |
| kExclusiveName, |
| AddCount(capacity_consumed / num_sampling_periods, |
| num_sampling_periods), |
| base::Histogram::FactoryGet( |
| kExclusiveName, /*min_value=*/1, /*max_value=*/100000, |
| /*bucket_count=*/50, |
| base::HistogramBase::kUmaTargetedHistogramFlag)); |
| } |
| |
| // Also report the time it took for us to detect this drop to see what the |
| // overall metric sensitivity is. |
| UMA_HISTOGRAM_LONG_TIMES_100( |
| "Power.ForegroundBatteryDrain.TimeBetweenEvents", |
| base::TimeDelta::FromSeconds(30 * num_sampling_periods)); |
| } |
| |
| } // namespace |
| |
| // static |
| constexpr base::TimeDelta AndroidBatteryMetrics::kMetricsInterval; |
| constexpr base::TimeDelta AndroidBatteryMetrics::kRadioStateInterval; |
| |
| AndroidBatteryMetrics::AndroidBatteryMetrics() |
| : app_visible_(false), |
| on_battery_power_(base::PowerMonitor::IsOnBatteryPower()) { |
| base::PowerMonitor::AddObserver(this); |
| UpdateMetricsEnabled(); |
| } |
| |
| AndroidBatteryMetrics::~AndroidBatteryMetrics() { |
| base::PowerMonitor::RemoveObserver(this); |
| } |
| |
| void AndroidBatteryMetrics::OnAppVisibilityChanged(bool visible) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| app_visible_ = visible; |
| UpdateMetricsEnabled(); |
| } |
| |
| void AndroidBatteryMetrics::OnPowerStateChange(bool on_battery_power) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| on_battery_power_ = on_battery_power; |
| UpdateMetricsEnabled(); |
| } |
| |
| void AndroidBatteryMetrics::UpdateMetricsEnabled() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // 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_ && on_battery_power_; |
| |
| if (should_be_enabled && !metrics_timer_.IsRunning()) { |
| // Capture first capacity measurement and enable the repeating timer. |
| last_remaining_capacity_uah_ = |
| base::PowerMonitor::GetRemainingBatteryCapacity(); |
| if (!net::android::traffic_stats::GetTotalTxBytes(&last_tx_bytes_)) |
| last_tx_bytes_ = -1; |
| if (!net::android::traffic_stats::GetTotalRxBytes(&last_rx_bytes_)) |
| last_rx_bytes_ = -1; |
| skipped_timers_ = 0; |
| observed_capacity_drops_ = 0; |
| |
| metrics_timer_.Start(FROM_HERE, kMetricsInterval, this, |
| &AndroidBatteryMetrics::CaptureAndReportMetrics); |
| if (base::FeatureList::IsEnabled(kForegroundRadioStateCountWakeups)) { |
| radio_state_timer_.Start(FROM_HERE, kRadioStateInterval, this, |
| &AndroidBatteryMetrics::MonitorRadioState); |
| } |
| } else if (!should_be_enabled && metrics_timer_.IsRunning()) { |
| // Capture one last measurement before disabling the timer. |
| CaptureAndReportMetrics(); |
| metrics_timer_.Stop(); |
| if (base::FeatureList::IsEnabled(kForegroundRadioStateCountWakeups)) { |
| radio_state_timer_.Stop(); |
| } |
| } |
| } |
| |
| void AndroidBatteryMetrics::MonitorRadioState() { |
| base::android::RadioDataActivity activity = |
| base::android::RadioUtils::GetCellDataActivity(); |
| if (last_activity_ == base::android::RadioDataActivity::kDormant && |
| activity != base::android::RadioDataActivity::kDormant) { |
| TRACE_EVENT_INSTANT0("power", "RadioWakeup", TRACE_EVENT_SCOPE_GLOBAL); |
| ++radio_wakeups_; |
| } |
| if (last_activity_ != base::android::RadioDataActivity::kDormant && |
| activity == base::android::RadioDataActivity::kDormant) { |
| TRACE_EVENT_INSTANT0("power", "RadioDormant", TRACE_EVENT_SCOPE_GLOBAL); |
| } |
| last_activity_ = activity; |
| } |
| |
| void AndroidBatteryMetrics::UpdateAndReportRadio() { |
| int64_t tx_bytes; |
| int64_t rx_bytes; |
| if (!net::android::traffic_stats::GetTotalTxBytes(&tx_bytes)) |
| tx_bytes = -1; |
| if (!net::android::traffic_stats::GetTotalRxBytes(&rx_bytes)) |
| rx_bytes = -1; |
| |
| if (last_tx_bytes_ > 0 && tx_bytes > 0 && last_rx_bytes_ > 0 && |
| rx_bytes > 0) { |
| Report30SecondRadioUsage(tx_bytes - last_tx_bytes_, |
| rx_bytes - last_rx_bytes_, radio_wakeups_); |
| } |
| last_tx_bytes_ = tx_bytes; |
| last_rx_bytes_ = rx_bytes; |
| radio_wakeups_ = 0; |
| } |
| |
| void AndroidBatteryMetrics::CaptureAndReportMetrics() { |
| int remaining_capacity_uah = |
| base::PowerMonitor::GetRemainingBatteryCapacity(); |
| |
| 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, IsMeasuringDrainExclusively()); |
| UpdateAndReportRadio(); |
| |
| 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, IsMeasuringDrainExclusively()); |
| UpdateAndReportRadio(); |
| |
| // 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. |
| 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::Sample 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; |
| } |
| |
| } // namespace power_metrics |