chromium/tools/mac/power/power_sampler/battery_sampler.h

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef TOOLS_MAC_POWER_POWER_SAMPLER_BATTERY_SAMPLER_H_
#define TOOLS_MAC_POWER_POWER_SAMPLER_BATTERY_SAMPLER_H_

#include <stdint.h>

#include <cstdint>
#include <memory>
#include <optional>

#include "base/mac/scoped_ioobject.h"
#include "tools/mac/power/power_sampler/sampler.h"

namespace power_sampler {

// The battery sampler provides data retrieved from the IOPMPowerSource. The
// |GetSample| method is ideally invoked on change notification from the
// IOPMPowerSource in order to sample new power data immediately.
//
// The sampler provides battery voltage, as well as the "max capacity" and
// "current capacity" of the battery.
//
// This sampler also provides an average power consumption estimate when
// possible, which is when the net "current capacity" of the battery has changed
// from a previous sample. This may not occur every time GetSample is invoked,
// as the "capacity" data is in integral units of 1mAh, which represents a fair
// bit of energy. For a three-cell LiIon battery with a nominal voltage of
// 3*3.7V or 11.1V, a single mAh consumed over the period of one minute
// represents (1mAh * 11.1V)/60s or ~0.67W average power consumption. The M1
// MacBooks, in particular, have been observed to consume much lower power than
// this when the backlight is turned down, and so it may take multiple sampling
// intervals for a capacity change of 1mAh to accrue. To estimate the average
// power consumption, this class uses simple linear interpolation over the
// interval in question, e.g. the assumption is that the battery voltage changes
// linearly from the start to the end of the interval.
//
// This sampler assumes that the battery capacity reported by macOS has changed
// just before the first call to GetSample(). If it isn't the case, the computed
// average power consumption will be incorrect. For example:
//  t = -2 minute : current capacity = 51, max capacity = 60
//  t = -1 minute : current capacity = 50, max capacity = 60
//  t =  0 minute : current capacity = 50, max capacity = 60 -> GetSample
//  t =  1 minute : current capacity = 49, max capacity = 60 -> GetSample
// The 2nd call to GetSample() will assume that current capacity decreased by 1
// over a 1 minute interval, but it really decreased by 1 over a 2 minutes
// interval.
class BatterySampler : public Sampler {
 public:
  static constexpr char kSamplerName[] = "battery";

  ~BatterySampler() override;

  // Creates and initializes a new sampler, if possible.
  // Returns nullptr on failure.
  static std::unique_ptr<BatterySampler> Create();

  // Sampler implementation.
  std::string GetName() override;
  DatumNameUnits GetDatumNameUnits() override;
  Sample GetSample(base::TimeTicks sample_time) override;

 protected:
  // Exposed for testing only.
  struct BatteryData {
    bool external_connected;
    int64_t voltage_mv;
    int64_t current_capacity_mah;
    int64_t max_capacity_mah;
    int64_t update_time_seconds_since_epoch;
  };
  using MaybeGetBatteryDataFn =
      std::optional<BatteryData> (*)(io_service_t power_source);
  using GetSecondsSinceEpochFn = int64_t (*)();

  // TODO(siggi): It'd be possible to test the data extraction part of this
  //     function by splitting it in two and passing it a dictionary to
  //     dissect.
  static std::optional<BatteryData> MaybeGetBatteryData(
      io_service_t power_source);

  struct AvgConsumption {
    double watts;
    int64_t mah;
  };

  // Yields average for |prev_data|, |new_data| and |duration| if the current
  // capacity has changed between |prev_data| and |new_data|.
  static std::optional<AvgConsumption> MaybeComputeAvgConsumption(
      base::TimeDelta duration,
      const BatteryData& prev_data,
      const BatteryData& new_data);

  static std::unique_ptr<BatterySampler> CreateImpl(
      MaybeGetBatteryDataFn maybe_get_battery_data_fn,
      GetSecondsSinceEpochFn get_seconds_since_epoch_fn,
      base::mac::ScopedIOObject<io_service_t> power_source);

  BatterySampler(MaybeGetBatteryDataFn maybe_get_battery_data_fn,
                 GetSecondsSinceEpochFn get_seconds_since_epoch_fn,
                 base::mac::ScopedIOObject<io_service_t> power_source,
                 BatteryData initial_battery_data);

 private:
  void StoreBatteryData(base::TimeTicks sample_time,
                        const BatteryData& battery_data);

  const MaybeGetBatteryDataFn maybe_get_battery_data_fn_;
  const GetSecondsSinceEpochFn get_seconds_since_epoch_fn_;
  const base::mac::ScopedIOObject<io_service_t> power_source_;

  // To compute the average power consumed between non-identical
  // "current capacity" samples, keep track of the voltage, max capacity and
  // current capacity last seen, as well as the time the current capacity last
  // changed.
  // Note that the capacity of a battery is load-dependent, and the capacity
  // estimate provided by macOS takes this into account. To see what this looks
  // like, take a look at the data sheet for e.g. any lithium ion battery, and
  // see how the datasheet will specify multiple discharge curves at different
  // "C" discharge levels.
  //
  // This means that the reported max capacity of the battery may change
  // drastically on load changes, whether downwards on load increase, or
  // upwards on load decrease.
  // It's been observed that whenever the reported max capacity of the battery
  // changes, the same delta is also applied to the reported current capacity
  // value. Hence, by subtracting the max capacity change from the current
  // capacity change, it's possible to keep track of the actual current
  // consumption.
  base::TimeTicks prev_battery_sample_time_ = base::TimeTicks::Min();
  std::optional<BatteryData> prev_battery_data_;
};

}  // namespace power_sampler

#endif  // TOOLS_MAC_POWER_POWER_SAMPLER_BATTERY_SAMPLER_H_