| // 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/power_monitor.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 { |
| |
| 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; |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| 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_); |
| on_battery_power_ = |
| base::PowerMonitor::AddPowerStateObserverAndReturnOnBatteryState(this); |
| base::PowerMonitor::AddPowerThermalObserver(this); |
| content::ProcessVisibilityTracker::GetInstance()->AddObserver(this); |
| UpdateMetricsEnabled(); |
| } |
| |
| void AndroidBatteryMetrics::OnVisibilityChanged(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::OnThermalStateChange(DeviceThermalState new_state) { |
| TRACE_EVENT_INSTANT( |
| "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)); |
| }); |
| |
| if (!app_visible_) |
| return; |
| |
| base::UmaHistogramEnumeration( |
| "Power.ForegroundThermalState.ChangeEvent.Android", new_state); |
| } |
| |
| void AndroidBatteryMetrics::OnSpeedLimitChange(int speed_limit) {} |
| |
| 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(); |
| skipped_timers_ = 0; |
| observed_capacity_drops_ = 0; |
| |
| 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::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()); |
| |
| 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, IsMeasuringDrainExclusively()); |
| |
| // 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::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 content |