// 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_