summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Dmitri Plotnikov <dplotnikov@google.com> 2024-04-22 14:05:14 -0700
committer Dmitri Plotnikov <dplotnikov@google.com> 2024-05-20 15:59:28 -0700
commitc4dfcb6c35a0c8b21a9446875b0e467f68a994ab (patch)
tree2fe75a31904b5c8abe1dd925637b0794d0d68b48
parentbbe32aed42b7ba2bd7e1373cb7bf4cdfa34c1710 (diff)
Introduce PowerStatsProcessor for Bluetooth
Bug: 323970018 Test: atest PowerStatsTestsRavenwood && atest PowerStatsTests Flag: com.android.server.power.optimization.streamlined_connectivity_battery_stats Change-Id: I55d25393844a41bd058ac43f9229555942adeac1
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java15
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java4
-rw-r--r--services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java304
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java540
4 files changed, 862 insertions, 1 deletions
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 2fc63a664467..7c0325e7376a 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -124,6 +124,7 @@ import com.android.server.power.stats.BatteryExternalStatsWorker;
import com.android.server.power.stats.BatteryStatsDumpHelperImpl;
import com.android.server.power.stats.BatteryStatsImpl;
import com.android.server.power.stats.BatteryUsageStatsProvider;
+import com.android.server.power.stats.BluetoothPowerStatsProcessor;
import com.android.server.power.stats.CpuPowerStatsProcessor;
import com.android.server.power.stats.MobileRadioPowerStatsProcessor;
import com.android.server.power.stats.PhoneCallPowerStatsProcessor;
@@ -502,6 +503,17 @@ public final class BatteryStatsService extends IBatteryStats.Stub
AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
.setProcessor(
new WifiPowerStatsProcessor(mPowerProfile));
+
+ config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_BLUETOOTH)
+ .trackDeviceStates(
+ AggregatedPowerStatsConfig.STATE_POWER,
+ AggregatedPowerStatsConfig.STATE_SCREEN)
+ .trackUidStates(
+ AggregatedPowerStatsConfig.STATE_POWER,
+ AggregatedPowerStatsConfig.STATE_SCREEN,
+ AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
+ .setProcessor(
+ new BluetoothPowerStatsProcessor(mPowerProfile));
return config;
}
@@ -565,6 +577,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub
mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_BLUETOOTH,
Flags.streamlinedConnectivityBatteryStats());
+ mBatteryUsageStatsProvider.setPowerStatsExporterEnabled(
+ BatteryConsumer.POWER_COMPONENT_BLUETOOTH,
+ Flags.streamlinedConnectivityBatteryStats());
mWorker.systemServicesReady();
mStats.systemServicesReady(mContext);
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 0d5eabc5ed47..b25239574071 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -90,7 +90,9 @@ public class BatteryUsageStatsProvider {
if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_WIFI)) {
mPowerCalculators.add(new WifiPowerCalculator(mPowerProfile));
}
- mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile));
+ if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_BLUETOOTH)) {
+ mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile));
+ }
mPowerCalculators.add(new SensorPowerCalculator(
mContext.getSystemService(SensorManager.class)));
mPowerCalculators.add(new GnssPowerCalculator(mPowerProfile));
diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java
new file mode 100644
index 000000000000..4d6db9703ce7
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import com.android.internal.os.PowerProfile;
+import com.android.internal.os.PowerStats;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BluetoothPowerStatsProcessor extends PowerStatsProcessor {
+ private static final String TAG = "BluetoothPowerStatsProcessor";
+
+ private final UsageBasedPowerEstimator mRxPowerEstimator;
+ private final UsageBasedPowerEstimator mTxPowerEstimator;
+ private final UsageBasedPowerEstimator mIdlePowerEstimator;
+
+ private PowerStats.Descriptor mLastUsedDescriptor;
+ private BluetoothPowerStatsLayout mStatsLayout;
+ // Sequence of steps for power estimation and intermediate results.
+ private PowerEstimationPlan mPlan;
+
+ private long[] mTmpDeviceStatsArray;
+ private long[] mTmpUidStatsArray;
+
+ public BluetoothPowerStatsProcessor(PowerProfile powerProfile) {
+ mRxPowerEstimator = new UsageBasedPowerEstimator(
+ powerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX));
+ mTxPowerEstimator = new UsageBasedPowerEstimator(
+ powerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_TX));
+ mIdlePowerEstimator = new UsageBasedPowerEstimator(
+ powerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE));
+ }
+
+ private static class Intermediates {
+ /**
+ * Number of received bytes
+ */
+ public long rxBytes;
+ /**
+ * Duration of receiving
+ */
+ public long rxTime;
+ /**
+ * Estimated power for the RX state.
+ */
+ public double rxPower;
+ /**
+ * Number of transmitted bytes
+ */
+ public long txBytes;
+ /**
+ * Duration of transmitting
+ */
+ public long txTime;
+ /**
+ * Estimated power for the TX state.
+ */
+ public double txPower;
+ /**
+ * Estimated power for IDLE, SCAN states.
+ */
+ public double idlePower;
+ /**
+ * Total scan time.
+ */
+ public long scanTime;
+ /**
+ * Measured consumed energy from power monitoring hardware (micro-coulombs)
+ */
+ public long consumedEnergy;
+ }
+
+ @Override
+ void finish(PowerComponentAggregatedPowerStats stats) {
+ if (stats.getPowerStatsDescriptor() == null) {
+ return;
+ }
+
+ unpackPowerStatsDescriptor(stats.getPowerStatsDescriptor());
+
+ if (mPlan == null) {
+ mPlan = new PowerEstimationPlan(stats.getConfig());
+ }
+
+ for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) {
+ DeviceStateEstimation estimation = mPlan.deviceStateEstimations.get(i);
+ Intermediates intermediates = new Intermediates();
+ estimation.intermediates = intermediates;
+ computeDevicePowerEstimates(stats, estimation.stateValues, intermediates);
+ }
+
+ double ratio = 1.0;
+ if (mStatsLayout.getEnergyConsumerCount() != 0) {
+ ratio = computeEstimateAdjustmentRatioUsingConsumedEnergy();
+ if (ratio != 1) {
+ for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) {
+ DeviceStateEstimation estimation = mPlan.deviceStateEstimations.get(i);
+ adjustDevicePowerEstimates(stats, estimation.stateValues,
+ (Intermediates) estimation.intermediates, ratio);
+ }
+ }
+ }
+
+ combineDeviceStateEstimates();
+
+ ArrayList<Integer> uids = new ArrayList<>();
+ stats.collectUids(uids);
+ if (!uids.isEmpty()) {
+ for (int uid : uids) {
+ for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) {
+ computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(i));
+ }
+ }
+
+ for (int uid : uids) {
+ for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) {
+ computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(i));
+ }
+ }
+ }
+ mPlan.resetIntermediates();
+ }
+
+ private void unpackPowerStatsDescriptor(PowerStats.Descriptor descriptor) {
+ if (descriptor.equals(mLastUsedDescriptor)) {
+ return;
+ }
+
+ mLastUsedDescriptor = descriptor;
+ mStatsLayout = new BluetoothPowerStatsLayout(descriptor);
+ mTmpDeviceStatsArray = new long[descriptor.statsArrayLength];
+ mTmpUidStatsArray = new long[descriptor.uidStatsArrayLength];
+ }
+
+ /**
+ * Compute power estimates using the power profile.
+ */
+ private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats,
+ int[] deviceStates, Intermediates intermediates) {
+ if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStates)) {
+ return;
+ }
+
+ for (int i = mStatsLayout.getEnergyConsumerCount() - 1; i >= 0; i--) {
+ intermediates.consumedEnergy += mStatsLayout.getConsumedEnergy(mTmpDeviceStatsArray, i);
+ }
+
+ intermediates.rxTime = mStatsLayout.getDeviceRxTime(mTmpDeviceStatsArray);
+ intermediates.txTime = mStatsLayout.getDeviceTxTime(mTmpDeviceStatsArray);
+ intermediates.scanTime = mStatsLayout.getDeviceScanTime(mTmpDeviceStatsArray);
+ long idleTime = mStatsLayout.getDeviceIdleTime(mTmpDeviceStatsArray);
+
+ intermediates.rxPower = mRxPowerEstimator.calculatePower(intermediates.rxTime);
+ intermediates.txPower = mTxPowerEstimator.calculatePower(intermediates.txTime);
+ intermediates.idlePower = mIdlePowerEstimator.calculatePower(idleTime);
+ mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray,
+ intermediates.rxPower + intermediates.txPower + intermediates.idlePower);
+ stats.setDeviceStats(deviceStates, mTmpDeviceStatsArray);
+ }
+
+ /**
+ * Compute an adjustment ratio using the total power estimated using the power profile
+ * and the total power measured by hardware.
+ */
+ private double computeEstimateAdjustmentRatioUsingConsumedEnergy() {
+ long totalConsumedEnergy = 0;
+ double totalPower = 0;
+
+ for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) {
+ Intermediates intermediates =
+ (Intermediates) mPlan.deviceStateEstimations.get(i).intermediates;
+ totalPower += intermediates.rxPower + intermediates.txPower + intermediates.idlePower;
+ totalConsumedEnergy += intermediates.consumedEnergy;
+ }
+
+ if (totalPower == 0) {
+ return 1;
+ }
+
+ return uCtoMah(totalConsumedEnergy) / totalPower;
+ }
+
+ /**
+ * Uniformly apply the same adjustment to all power estimates in order to ensure that the total
+ * estimated power matches the measured consumed power. We are not claiming that all
+ * averages captured in the power profile have to be off by the same percentage in reality.
+ */
+ private void adjustDevicePowerEstimates(PowerComponentAggregatedPowerStats stats,
+ int[] deviceStates, Intermediates intermediates, double ratio) {
+ double adjutedPower;
+ intermediates.rxPower *= ratio;
+ intermediates.txPower *= ratio;
+ intermediates.idlePower *= ratio;
+ adjutedPower = intermediates.rxPower + intermediates.txPower + intermediates.idlePower;
+
+ if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStates)) {
+ return;
+ }
+
+ mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, adjutedPower);
+ stats.setDeviceStats(deviceStates, mTmpDeviceStatsArray);
+ }
+
+ /**
+ * Combine power estimates before distributing them proportionally to UIDs.
+ */
+ private void combineDeviceStateEstimates() {
+ for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) {
+ CombinedDeviceStateEstimate cdse = mPlan.combinedDeviceStateEstimations.get(i);
+ Intermediates cdseIntermediates = new Intermediates();
+ cdse.intermediates = cdseIntermediates;
+ List<DeviceStateEstimation> deviceStateEstimations = cdse.deviceStateEstimations;
+ for (int j = deviceStateEstimations.size() - 1; j >= 0; j--) {
+ DeviceStateEstimation dse = deviceStateEstimations.get(j);
+ Intermediates intermediates = (Intermediates) dse.intermediates;
+ cdseIntermediates.rxTime += intermediates.rxTime;
+ cdseIntermediates.rxBytes += intermediates.rxBytes;
+ cdseIntermediates.rxPower += intermediates.rxPower;
+ cdseIntermediates.txTime += intermediates.txTime;
+ cdseIntermediates.txBytes += intermediates.txBytes;
+ cdseIntermediates.txPower += intermediates.txPower;
+ cdseIntermediates.idlePower += intermediates.idlePower;
+ cdseIntermediates.scanTime += intermediates.scanTime;
+ cdseIntermediates.consumedEnergy += intermediates.consumedEnergy;
+ }
+ }
+ }
+
+ private void computeUidActivityTotals(PowerComponentAggregatedPowerStats stats, int uid,
+ UidStateEstimate uidStateEstimate) {
+ Intermediates intermediates =
+ (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates;
+ for (UidStateProportionalEstimate proportionalEstimate :
+ uidStateEstimate.proportionalEstimates) {
+ if (!stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) {
+ continue;
+ }
+
+ intermediates.rxBytes += mStatsLayout.getUidRxBytes(mTmpUidStatsArray);
+ intermediates.txBytes += mStatsLayout.getUidTxBytes(mTmpUidStatsArray);
+ }
+ }
+
+ private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, int uid,
+ UidStateEstimate uidStateEstimate) {
+ Intermediates intermediates =
+ (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates;
+
+ // Scan is more expensive than data transfer, so in the presence of large
+ // of scanning duration, blame apps according to the time they spent scanning.
+ // This may disproportionately blame apps that do a lot of scanning, which is
+ // the tread-off we are making in the absence of more detailed metrics.
+ boolean normalizeRxByScanTime = intermediates.scanTime > intermediates.rxTime;
+ boolean normalizeTxByScanTime = intermediates.scanTime > intermediates.txTime;
+
+ for (UidStateProportionalEstimate proportionalEstimate :
+ uidStateEstimate.proportionalEstimates) {
+ if (!stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) {
+ continue;
+ }
+
+ double power = 0;
+ if (normalizeRxByScanTime) {
+ if (intermediates.scanTime != 0) {
+ power += intermediates.rxPower * mStatsLayout.getUidScanTime(mTmpUidStatsArray)
+ / intermediates.scanTime;
+ }
+ } else {
+ if (intermediates.rxBytes != 0) {
+ power += intermediates.rxPower * mStatsLayout.getUidRxBytes(mTmpUidStatsArray)
+ / intermediates.rxBytes;
+ }
+ }
+ if (normalizeTxByScanTime) {
+ if (intermediates.scanTime != 0) {
+ power += intermediates.txPower * mStatsLayout.getUidScanTime(mTmpUidStatsArray)
+ / intermediates.scanTime;
+ }
+ } else {
+ if (intermediates.txBytes != 0) {
+ power += intermediates.txPower * mStatsLayout.getUidTxBytes(mTmpUidStatsArray)
+ / intermediates.txBytes;
+ }
+ }
+ mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power);
+ stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray);
+ }
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java
new file mode 100644
index 000000000000..752bc2712fe2
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_CACHED;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE;
+
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothActivityEnergyInfo;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.UidTraffic;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.power.stats.EnergyConsumerType;
+import android.os.BatteryConsumer;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Process;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.SparseLongArray;
+
+import com.android.internal.os.Clock;
+import com.android.internal.os.PowerProfile;
+import com.android.server.power.stats.BluetoothPowerStatsCollector.BluetoothStatsRetriever;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.IntSupplier;
+
+public class BluetoothPowerStatsProcessorTest {
+
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
+ private static final double PRECISION = 0.00001;
+ private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101;
+ private static final int BLUETOOTH_ENERGY_CONSUMER_ID = 1;
+ private static final int VOLTAGE_MV = 3500;
+
+ @Rule(order = 1)
+ public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
+ .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX, 50.0)
+ .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_TX, 100.0)
+ .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE, 10.0)
+ .initMeasuredEnergyStatsLocked();
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private PowerStatsCollector.ConsumedEnergyRetriever mConsumedEnergyRetriever;
+ private final PowerStatsUidResolver mPowerStatsUidResolver = new PowerStatsUidResolver();
+
+ private BluetoothActivityEnergyInfo mBluetoothActivityEnergyInfo;
+ private final SparseLongArray mUidScanTimes = new SparseLongArray();
+
+ private final BluetoothPowerStatsCollector.BluetoothStatsRetriever mBluetoothStatsRetriever =
+ new BluetoothPowerStatsCollector.BluetoothStatsRetriever() {
+ @Override
+ public void retrieveBluetoothScanTimes(Callback callback) {
+ for (int i = 0; i < mUidScanTimes.size(); i++) {
+ callback.onBluetoothScanTime(mUidScanTimes.keyAt(i),
+ mUidScanTimes.valueAt(i));
+ }
+ }
+
+ @Override
+ public boolean requestControllerActivityEnergyInfo(Executor executor,
+ BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback callback) {
+ callback.onBluetoothActivityEnergyInfoAvailable(mBluetoothActivityEnergyInfo);
+ return true;
+ }
+ };
+
+ private final BluetoothPowerStatsCollector.Injector mInjector =
+ new BluetoothPowerStatsCollector.Injector() {
+ @Override
+ public Handler getHandler() {
+ return mStatsRule.getHandler();
+ }
+
+ @Override
+ public Clock getClock() {
+ return mStatsRule.getMockClock();
+ }
+
+ @Override
+ public PowerStatsUidResolver getUidResolver() {
+ return mPowerStatsUidResolver;
+ }
+
+ @Override
+ public long getPowerStatsCollectionThrottlePeriod(String powerComponentName) {
+ return 0;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public PowerStatsCollector.ConsumedEnergyRetriever getConsumedEnergyRetriever() {
+ return mConsumedEnergyRetriever;
+ }
+
+ @Override
+ public IntSupplier getVoltageSupplier() {
+ return () -> VOLTAGE_MV;
+ }
+
+ @Override
+ public BluetoothStatsRetriever getBluetoothStatsRetriever() {
+ return mBluetoothStatsRetriever;
+ }
+ };
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)).thenReturn(true);
+ }
+
+ @Test
+ public void powerProfileModel_mostlyDataTransfer() {
+ // No power monitoring hardware
+ when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.BLUETOOTH))
+ .thenReturn(new int[0]);
+
+ BluetoothPowerStatsProcessor processor =
+ new BluetoothPowerStatsProcessor(mStatsRule.getPowerProfile());
+
+ PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
+
+ BluetoothPowerStatsCollector collector = new BluetoothPowerStatsCollector(mInjector);
+ collector.setEnabled(true);
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1000, 600, 100, 200,
+ mockUidTraffic(APP_UID1, 100, 200),
+ mockUidTraffic(APP_UID2, 300, 400));
+
+ mUidScanTimes.put(APP_UID1, 100);
+
+ // Establish a baseline
+ aggregatedStats.addPowerStats(collector.collectStats(), 0);
+
+ // Turn the screen off after 2.5 seconds
+ aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE,
+ 5000);
+
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1100, 6600, 1100, 2200,
+ mockUidTraffic(APP_UID1, 1100, 2200),
+ mockUidTraffic(APP_UID2, 3300, 4400));
+
+ mUidScanTimes.clear();
+ mUidScanTimes.put(APP_UID1, 200);
+ mUidScanTimes.put(APP_UID2, 300);
+
+ mStatsRule.setTime(10_000, 10_000);
+
+ aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
+
+ processor.finish(aggregatedStats);
+
+ BluetoothPowerStatsLayout statsLayout =
+ new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
+
+ // RX power = 'rx-duration * PowerProfile[bluetooth.controller.rx]`
+ // RX power = 6000 * 50 = 300000 mA-ms = 0.083333 mAh
+ // TX power = 'tx-duration * PowerProfile[bluetooth.controller.tx]`
+ // TX power = 1000 * 100 = 100000 mA-ms = 0.02777 mAh
+ // Idle power = 'idle-duration * PowerProfile[bluetooth.controller.idle]`
+ // Idle power = 2000 * 10 = 20000 mA-ms = 0.00555 mAh
+ // Total power = RX + TX + Idle = 0.116666
+ // Screen-on - 25%
+ // Screen-off - 75%
+ double expectedPower = 0.116666;
+ long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength];
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.25);
+
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.75);
+
+ // UID1 =
+ // (1000 / 4000) * 0.083333 // rx
+ // + (2000 / 6000) * 0.027777 // tx
+ // = 0.030092 mAh
+ double expectedPower1 = 0.030092;
+ long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength];
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.5);
+
+ // UID2 =
+ // (3000 / 4000) * 0.083333 // rx
+ // + (4000 / 6000) * 0.027777 // tx
+ // = 0.08102 mAh
+ double expectedPower2 = 0.08102;
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.75);
+ }
+
+ @Test
+ public void powerProfileModel_mostlyScan() {
+ // No power monitoring hardware
+ when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.BLUETOOTH))
+ .thenReturn(new int[0]);
+
+ BluetoothPowerStatsProcessor processor =
+ new BluetoothPowerStatsProcessor(mStatsRule.getPowerProfile());
+
+ PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
+
+ BluetoothPowerStatsCollector collector = new BluetoothPowerStatsCollector(mInjector);
+ collector.setEnabled(true);
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1000, 600, 100, 200,
+ mockUidTraffic(APP_UID1, 100, 200),
+ mockUidTraffic(APP_UID2, 300, 400));
+
+ mUidScanTimes.put(APP_UID1, 100);
+
+ // Establish a baseline
+ aggregatedStats.addPowerStats(collector.collectStats(), 0);
+
+ aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE,
+ 5000);
+
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1100, 6600, 1100, 2200,
+ mockUidTraffic(APP_UID1, 1100, 2200),
+ mockUidTraffic(APP_UID2, 3300, 4400));
+
+ // Total scan time exceeding data transfer times
+ mUidScanTimes.clear();
+ mUidScanTimes.put(APP_UID1, 3100);
+ mUidScanTimes.put(APP_UID2, 5000);
+
+ mStatsRule.setTime(10_000, 10_000);
+
+ aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
+
+ processor.finish(aggregatedStats);
+
+ BluetoothPowerStatsLayout statsLayout =
+ new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
+
+ // RX power = 'rx-duration * PowerProfile[bluetooth.controller.rx]`
+ // RX power = 6000 * 50 = 300000 mA-ms = 0.083333 mAh
+ // TX power = 'tx-duration * PowerProfile[bluetooth.controller.tx]`
+ // TX power = 1000 * 100 = 100000 mA-ms = 0.02777 mAh
+ // Idle power = 'idle-duration * PowerProfile[bluetooth.controller.idle]`
+ // Idle power = 2000 * 10 = 20000 mA-ms = 0.00555 mAh
+ // Total power = RX + TX + Idle = 0.116666
+ double expectedPower = 0.116666;
+ long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength];
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.25);
+
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.75);
+
+ // UID1 =
+ // (3000 / 8000) * 0.083333 // rx
+ // + (3000 / 8000) * 0.027777 // tx
+ // = 0.041666 mAh
+ double expectedPower1 = 0.041666;
+ long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength];
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.5);
+
+ // UID2 =
+ // (5000 / 8000) * 0.083333 // rx
+ // + (5000 / 8000) * 0.027777 // tx
+ // = 0.069443 mAh
+ double expectedPower2 = 0.069443;
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.75);
+ }
+
+ @Test
+ public void consumedEnergyModel() {
+ // No power monitoring hardware
+ when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.BLUETOOTH))
+ .thenReturn(new int[]{BLUETOOTH_ENERGY_CONSUMER_ID});
+
+ BluetoothPowerStatsProcessor processor =
+ new BluetoothPowerStatsProcessor(mStatsRule.getPowerProfile());
+
+ PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
+
+ BluetoothPowerStatsCollector collector = new BluetoothPowerStatsCollector(mInjector);
+ collector.setEnabled(true);
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1000, 600, 100, 200,
+ mockUidTraffic(APP_UID1, 100, 200),
+ mockUidTraffic(APP_UID2, 300, 400));
+
+ mUidScanTimes.put(APP_UID1, 100);
+
+ when(mConsumedEnergyRetriever.getConsumedEnergyUws(
+ new int[]{BLUETOOTH_ENERGY_CONSUMER_ID})).thenReturn(new long[]{0});
+
+ // Establish a baseline
+ aggregatedStats.addPowerStats(collector.collectStats(), 0);
+
+ // Turn the screen off after 2.5 seconds
+ aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE,
+ 5000);
+
+ mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1100, 6600, 1100, 2200,
+ mockUidTraffic(APP_UID1, 1100, 2200),
+ mockUidTraffic(APP_UID2, 3300, 4400));
+
+ mUidScanTimes.clear();
+ mUidScanTimes.put(APP_UID1, 200);
+ mUidScanTimes.put(APP_UID2, 300);
+
+ mStatsRule.setTime(10_000, 10_000);
+
+ // 10 mAh represented as microWattSeconds
+ long energyUws = 10 * 3600 * VOLTAGE_MV;
+ when(mConsumedEnergyRetriever.getConsumedEnergyUws(
+ new int[]{BLUETOOTH_ENERGY_CONSUMER_ID})).thenReturn(new long[]{energyUws});
+
+ aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
+
+ processor.finish(aggregatedStats);
+
+ BluetoothPowerStatsLayout statsLayout =
+ new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
+
+ // All estimates are computed as in the #powerProfileModel_mostlyDataTransfer test,
+ // except they are all scaled by the same ratio to ensure that the total estimated
+ // energy is equal to the measured energy
+ double expectedPower = 10;
+ long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength];
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.25);
+
+ aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+ assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+ .isWithin(PRECISION).of(expectedPower * 0.75);
+
+ // UID1
+ // 0.030092 // power profile model estimate
+ // 0.116666 // power profile model estimate for total power
+ // 10 // total consumed energy
+ // = 0.030092 * (10 / 0.116666) = 2.579365
+ double expectedPower1 = 2.579365;
+ long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength];
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID1,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower1 * 0.5);
+
+ // UID2 =
+ // 0.08102 // power profile model estimate
+ // 0.116666 // power profile model estimate for total power
+ // 10 // total consumed energy
+ // = 0.08102 * (10 / 0.116666) = 6.944444
+ double expectedPower2 = 6.944444;
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.25);
+
+ aggregatedStats.getUidStats(uidStats, APP_UID2,
+ states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+ assertThat(statsLayout.getUidPowerEstimate(uidStats))
+ .isWithin(PRECISION).of(expectedPower2 * 0.75);
+ }
+
+ private static PowerComponentAggregatedPowerStats createAggregatedPowerStats(
+ BluetoothPowerStatsProcessor processor) {
+ AggregatedPowerStatsConfig.PowerComponent config =
+ new AggregatedPowerStatsConfig.PowerComponent(
+ BatteryConsumer.POWER_COMPONENT_BLUETOOTH)
+ .trackDeviceStates(STATE_POWER, STATE_SCREEN)
+ .trackUidStates(STATE_POWER, STATE_SCREEN, STATE_PROCESS_STATE)
+ .setProcessor(processor);
+
+ PowerComponentAggregatedPowerStats aggregatedStats =
+ new PowerComponentAggregatedPowerStats(
+ new AggregatedPowerStats(mock(AggregatedPowerStatsConfig.class)), config);
+
+ aggregatedStats.setState(STATE_POWER, POWER_STATE_OTHER, 0);
+ aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0);
+ aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0);
+ aggregatedStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0);
+
+ return aggregatedStats;
+ }
+
+ private int[] states(int... states) {
+ return states;
+ }
+
+ private BluetoothActivityEnergyInfo mockBluetoothActivityEnergyInfo(long timestamp,
+ long rxTimeMs, long txTimeMs, long idleTimeMs, UidTraffic... uidTraffic) {
+ if (RavenwoodRule.isOnRavenwood()) {
+ BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class);
+ when(info.getControllerRxTimeMillis()).thenReturn(rxTimeMs);
+ when(info.getControllerTxTimeMillis()).thenReturn(txTimeMs);
+ when(info.getControllerIdleTimeMillis()).thenReturn(idleTimeMs);
+ when(info.getUidTraffic()).thenReturn(List.of(uidTraffic));
+ return info;
+ } else {
+ final Parcel btActivityEnergyInfoParcel = Parcel.obtain();
+ btActivityEnergyInfoParcel.writeLong(timestamp);
+ btActivityEnergyInfoParcel.writeInt(
+ BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE);
+ btActivityEnergyInfoParcel.writeLong(txTimeMs);
+ btActivityEnergyInfoParcel.writeLong(rxTimeMs);
+ btActivityEnergyInfoParcel.writeLong(idleTimeMs);
+ btActivityEnergyInfoParcel.writeLong(0L);
+ btActivityEnergyInfoParcel.writeTypedList(List.of(uidTraffic));
+ btActivityEnergyInfoParcel.setDataPosition(0);
+
+ BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR
+ .createFromParcel(btActivityEnergyInfoParcel);
+ btActivityEnergyInfoParcel.recycle();
+ return info;
+ }
+ }
+
+ private UidTraffic mockUidTraffic(int uid, long rxBytes, long txBytes) {
+ if (RavenwoodRule.isOnRavenwood()) {
+ UidTraffic traffic = mock(UidTraffic.class);
+ when(traffic.getUid()).thenReturn(uid);
+ when(traffic.getRxBytes()).thenReturn(rxBytes);
+ when(traffic.getTxBytes()).thenReturn(txBytes);
+ return traffic;
+ } else {
+ final Parcel uidTrafficParcel = Parcel.obtain();
+ uidTrafficParcel.writeInt(uid);
+ uidTrafficParcel.writeLong(rxBytes);
+ uidTrafficParcel.writeLong(txBytes);
+ uidTrafficParcel.setDataPosition(0);
+
+ UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel);
+ uidTrafficParcel.recycle();
+ return traffic;
+ }
+ }
+}