// 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::ProcessPriorityTracker::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::OnPriorityChanged(
    base::Process::Priority priority) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  app_foregrounded_ = (priority != base::Process::Priority::kBestEffort);
  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_foregrounded_ &&
      (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
