| // 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. |
| |
| #include "tools/mac/power/power_sampler/battery_sampler.h" |
| |
| #import <Foundation/Foundation.h> |
| #include <IOKit/IOKitLib.h> |
| #include <IOKit/ps/IOPSKeys.h> |
| #include <cstdint> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/apple/mach_logging.h" |
| #include "base/apple/scoped_cftyperef.h" |
| #include "base/logging.h" |
| #include "base/mac/scoped_ioobject.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/time/time.h" |
| |
| namespace power_sampler { |
| namespace { |
| |
| // Returns the value corresponding to |key| in the dictionary |description|. |
| // Returns |default_value| if the dictionary does not contain |key|, the |
| // corresponding value is nullptr or it could not be converted to SInt64. |
| absl::optional<SInt64> GetValueAsSInt64(CFDictionaryRef description, |
| CFStringRef key) { |
| CFNumberRef number_ref = |
| base::apple::GetValueFromDictionary<CFNumberRef>(description, key); |
| |
| SInt64 value; |
| if (number_ref && CFNumberGetValue(number_ref, kCFNumberSInt64Type, &value)) |
| return value; |
| |
| return absl::nullopt; |
| } |
| |
| absl::optional<bool> GetValueAsBoolean(CFDictionaryRef description, |
| CFStringRef key) { |
| CFBooleanRef boolean = |
| base::apple::GetValueFromDictionary<CFBooleanRef>(description, key); |
| if (!boolean) |
| return absl::nullopt; |
| return CFBooleanGetValue(boolean); |
| } |
| |
| } // namespace |
| |
| BatterySampler::~BatterySampler() = default; |
| |
| // static |
| std::unique_ptr<BatterySampler> BatterySampler::Create() { |
| // Retrieve the IOPMPowerSource service. |
| base::mac::ScopedIOObject<io_service_t> power_source( |
| IOServiceGetMatchingService(kIOMasterPortDefault, |
| IOServiceMatching("IOPMPowerSource"))); |
| if (power_source == IO_OBJECT_NULL) |
| return nullptr; |
| |
| auto get_seconds_since_epoch_fn = []() -> int64_t { |
| return (base::Time::Now() - base::Time::UnixEpoch()).InSeconds(); |
| }; |
| |
| return CreateImpl(MaybeGetBatteryData, get_seconds_since_epoch_fn, |
| std::move(power_source)); |
| } |
| |
| std::string BatterySampler::GetName() { |
| return kSamplerName; |
| } |
| |
| Sampler::DatumNameUnits BatterySampler::GetDatumNameUnits() { |
| DatumNameUnits ret; |
| ret.emplace("external_connected", "bool"); |
| ret.emplace("voltage", "V"); |
| ret.emplace("current_capacity", "Ah"); |
| ret.emplace("max_capacity", "Ah"); |
| // https://en.wikipedia.org/wiki/Power_(physics) |
| ret.emplace("avg_power", "W"); |
| // https://en.wikipedia.org/wiki/Electric_charge |
| ret.emplace("electric_charge_delta", "mAh"); |
| ret.emplace("sample_age", "s"); |
| |
| return ret; |
| } |
| |
| Sampler::Sample BatterySampler::GetSample(base::TimeTicks sample_time) { |
| absl::optional<BatteryData> new_battery_data = |
| maybe_get_battery_data_fn_(power_source_.get()); |
| if (!new_battery_data.has_value()) |
| return Sample(); |
| |
| Sample sample; |
| const BatteryData& new_data = new_battery_data.value(); |
| if (prev_battery_data_.has_value()) { |
| // There's a previous sample to refer to. Compute the average power if |
| // there's been any reported current consumption since that sample. |
| // Note that the current consumption is reported in integral units of mAh, |
| // and that the underlying sampling when on battery is once a minute. |
| auto avg_consumption = |
| MaybeComputeAvgConsumption(sample_time - prev_battery_sample_time_, |
| prev_battery_data_.value(), new_data); |
| |
| if (avg_consumption.has_value()) { |
| sample.emplace("avg_power", avg_consumption->watts); |
| sample.emplace("electric_charge_delta", avg_consumption->mah); |
| |
| // The previous sample is consumed, store the new one. |
| StoreBatteryData(sample_time, new_data); |
| } |
| } |
| |
| sample.emplace("external_connected", new_data.external_connected); |
| sample.emplace("voltage", new_data.voltage_mv / 1000.0); |
| sample.emplace("current_capacity", new_data.current_capacity_mah / 1000.0); |
| sample.emplace("max_capacity", new_data.max_capacity_mah / 1000.0); |
| sample.emplace("sample_age", get_seconds_since_epoch_fn_() - |
| new_data.update_time_seconds_since_epoch); |
| |
| if (!prev_battery_data_.has_value()) { |
| // Store an initial sample. |
| StoreBatteryData(sample_time, new_data); |
| } |
| |
| return sample; |
| } |
| |
| // static |
| absl::optional<BatterySampler::BatteryData> BatterySampler::MaybeGetBatteryData( |
| io_service_t power_source) { |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> dict; |
| kern_return_t result = IORegistryEntryCreateCFProperties( |
| power_source, dict.InitializeInto(), 0, 0); |
| if (result != KERN_SUCCESS) { |
| MACH_LOG(ERROR, result) << "IORegistryEntryCreateCFProperties"; |
| return absl::nullopt; |
| } |
| |
| absl::optional<bool> external_connected = |
| GetValueAsBoolean(dict, CFSTR("ExternalConnected")); |
| absl::optional<SInt64> voltage_mv = |
| GetValueAsSInt64(dict, CFSTR(kIOPSVoltageKey)); |
| absl::optional<SInt64> current_capacity_mah = |
| GetValueAsSInt64(dict, CFSTR("AppleRawCurrentCapacity")); |
| absl::optional<SInt64> max_capacity_mah = |
| GetValueAsSInt64(dict, CFSTR("AppleRawMaxCapacity")); |
| absl::optional<SInt64> update_time = |
| GetValueAsSInt64(dict, CFSTR("UpdateTime")); |
| |
| if (!external_connected.has_value() || !voltage_mv.has_value() || |
| !current_capacity_mah.has_value() || !max_capacity_mah.has_value()) { |
| return absl::nullopt; |
| } |
| |
| BatteryData data{.external_connected = external_connected.value(), |
| .voltage_mv = voltage_mv.value(), |
| .current_capacity_mah = current_capacity_mah.value(), |
| .max_capacity_mah = max_capacity_mah.value(), |
| .update_time_seconds_since_epoch = update_time.value()}; |
| |
| return data; |
| } |
| |
| // static |
| absl::optional<BatterySampler::AvgConsumption> |
| BatterySampler::MaybeComputeAvgConsumption(base::TimeDelta duration, |
| const BatteryData& prev_data, |
| const BatteryData& new_data) { |
| // The gauging hardware measures current consumed (or charged), but reports |
| // the remaining capacity with respect to a load-dependent max capacity. |
| // Here, however, we care about the delta capacity consumed rather than the |
| // capacity remaining. To get to capacity consumed, we flip the capacity |
| // remaining estimates to capacity consumed and work from there. It's been |
| // experimentally determined that this backs out the effects of any |
| // load-dependent max capacity estimates to yield the capacity consumed. |
| int64_t prev_current_consumed_mah = |
| prev_data.max_capacity_mah - prev_data.current_capacity_mah; |
| int64_t new_current_consumed_mah = |
| new_data.max_capacity_mah - new_data.current_capacity_mah; |
| |
| // Compute the consumed capacity delta. |
| int64_t delta_current_consumed_mah = |
| prev_current_consumed_mah - new_current_consumed_mah; |
| if (delta_current_consumed_mah == 0) |
| return absl::nullopt; |
| |
| double avg_voltage_v = |
| (new_data.voltage_mv + prev_data.voltage_mv) / (2.0 * 1000.0); |
| |
| constexpr double kAsPerMAh = |
| static_cast<double>(base::TimeTicks::kSecondsPerHour) / 1000.0; |
| double avg_current_a = |
| delta_current_consumed_mah * kAsPerMAh / duration.InSecondsF(); |
| |
| // Arbitrarily use positive values to represent energy being consumed |
| // (charging the battery will produce negative values). |
| return AvgConsumption{.watts = avg_voltage_v * -avg_current_a, |
| .mah = -delta_current_consumed_mah}; |
| } |
| |
| // static |
| std::unique_ptr<BatterySampler> BatterySampler::CreateImpl( |
| MaybeGetBatteryDataFn maybe_get_battery_data_fn, |
| GetSecondsSinceEpochFn get_seconds_since_epoch_fn, |
| base::mac::ScopedIOObject<io_service_t> power_source) { |
| // Validate that we can work with this source. |
| auto battery_data = maybe_get_battery_data_fn(power_source.get()); |
| if (!battery_data.has_value()) |
| return nullptr; |
| |
| return base::WrapUnique( |
| new BatterySampler(maybe_get_battery_data_fn, get_seconds_since_epoch_fn, |
| std::move(power_source), *battery_data)); |
| } |
| |
| BatterySampler::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) |
| : maybe_get_battery_data_fn_(maybe_get_battery_data_fn), |
| get_seconds_since_epoch_fn_(get_seconds_since_epoch_fn), |
| power_source_(std::move(power_source)) {} |
| |
| void BatterySampler::StoreBatteryData(base::TimeTicks sample_time, |
| const BatteryData& battery_data) { |
| prev_battery_sample_time_ = sample_time; |
| prev_battery_data_ = battery_data; |
| } |
| |
| } // namespace power_sampler |