diff options
| author | 2024-04-22 14:05:14 -0700 | |
|---|---|---|
| committer | 2024-05-20 15:59:28 -0700 | |
| commit | c4dfcb6c35a0c8b21a9446875b0e467f68a994ab (patch) | |
| tree | 2fe75a31904b5c8abe1dd925637b0794d0d68b48 | |
| parent | bbe32aed42b7ba2bd7e1373cb7bf4cdfa34c1710 (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
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; + } + } +} |