diff options
| author | 2024-04-18 17:01:14 -0700 | |
|---|---|---|
| committer | 2024-05-20 15:58:51 -0700 | |
| commit | bbe32aed42b7ba2bd7e1373cb7bf4cdfa34c1710 (patch) | |
| tree | 3729cd77e46caa2d31e318d262b3d6d4705e64ab | |
| parent | 40349d86bed476cd478bb81d20ac2d4ff63fbb93 (diff) | |
Introduce PowerStatsCollector for Bluetooth
Bug: 323970018
Test: atest PowerStatsTestsRavenwood && atest PowerStatsTests
Flag: com.android.server.power.optimization.streamlined_connectivity_battery_stats
Change-Id: I5dcef0c5ce78859456d456df503f6b4a56ee2b9d
6 files changed, 907 insertions, 28 deletions
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 58732fd200d2..2fc63a664467 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -563,6 +563,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub BatteryConsumer.POWER_COMPONENT_WIFI, Flags.streamlinedConnectivityBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_BLUETOOTH, + Flags.streamlinedConnectivityBatteryStats()); + mWorker.systemServicesReady(); mStats.systemServicesReady(mContext); mCpuWakeupStats.systemServicesReady(); diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java index cb10da9787df..2f1641980784 100644 --- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java +++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java @@ -572,34 +572,41 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat } if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_BT) != 0) { - // We were asked to fetch Bluetooth data. - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - SynchronousResultReceiver resultReceiver = - new SynchronousResultReceiver("bluetooth"); - adapter.requestControllerActivityEnergyInfo( - Runnable::run, - new BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback() { - @Override - public void onBluetoothActivityEnergyInfoAvailable( - BluetoothActivityEnergyInfo info) { - Bundle bundle = new Bundle(); - bundle.putParcelable( - BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, info); - resultReceiver.send(0, bundle); - } + @SuppressWarnings("GuardedBy") + PowerStatsCollector collector = mStats.getPowerStatsCollector( + BatteryConsumer.POWER_COMPONENT_BLUETOOTH); + if (collector.isEnabled()) { + collector.schedule(); + } else { + // We were asked to fetch Bluetooth data. + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter != null) { + SynchronousResultReceiver resultReceiver = + new SynchronousResultReceiver("bluetooth"); + adapter.requestControllerActivityEnergyInfo( + Runnable::run, + new BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback() { + @Override + public void onBluetoothActivityEnergyInfoAvailable( + BluetoothActivityEnergyInfo info) { + Bundle bundle = new Bundle(); + bundle.putParcelable( + BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, info); + resultReceiver.send(0, bundle); + } - @Override - public void onBluetoothActivityEnergyInfoError(int errorCode) { - Slog.w(TAG, "error reading Bluetooth stats: " + errorCode); - Bundle bundle = new Bundle(); - bundle.putParcelable( - BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, null); - resultReceiver.send(0, bundle); + @Override + public void onBluetoothActivityEnergyInfoError(int errorCode) { + Slog.w(TAG, "error reading Bluetooth stats: " + errorCode); + Bundle bundle = new Bundle(); + bundle.putParcelable( + BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, null); + resultReceiver.send(0, bundle); + } } - } - ); - bluetoothReceiver = resultReceiver; + ); + bluetoothReceiver = resultReceiver; + } } } diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 9a4155122402..1b6af7170756 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -32,6 +32,8 @@ import android.app.ActivityManager; import android.app.AlarmManager; import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothActivityEnergyInfo; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; import android.bluetooth.UidTraffic; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -172,6 +174,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -294,6 +297,7 @@ public class BatteryStatsImpl extends BatteryStats { private final CpuPowerStatsCollector mCpuPowerStatsCollector; private final MobileRadioPowerStatsCollector mMobileRadioPowerStatsCollector; private final WifiPowerStatsCollector mWifiPowerStatsCollector; + private final BluetoothPowerStatsCollector mBluetoothPowerStatsCollector; private final SparseBooleanArray mPowerStatsCollectorEnabled = new SparseBooleanArray(); private final WifiPowerStatsCollector.WifiStatsRetriever mWifiStatsRetriever = new WifiPowerStatsCollector.WifiStatsRetriever() { @@ -313,6 +317,38 @@ public class BatteryStatsImpl extends BatteryStats { } }; + private class BluetoothStatsRetrieverImpl implements + BluetoothPowerStatsCollector.BluetoothStatsRetriever { + private final BluetoothManager mBluetoothManager; + + BluetoothStatsRetrieverImpl(BluetoothManager bluetoothManager) { + mBluetoothManager = bluetoothManager; + } + + @Override + public void retrieveBluetoothScanTimes(Callback callback) { + synchronized (BatteryStatsImpl.this) { + retrieveBluetoothScanTimesLocked(callback); + } + } + + @Override + public boolean requestControllerActivityEnergyInfo(Executor executor, + BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback callback) { + if (mBluetoothManager == null) { + return false; + } + + BluetoothAdapter adapter = mBluetoothManager.getAdapter(); + if (adapter == null) { + return false; + } + + adapter.requestControllerActivityEnergyInfo(executor, callback); + return true; + } + } + public LongSparseArray<SamplingTimer> getKernelMemoryStats() { return mKernelMemoryStats; } @@ -1926,12 +1962,14 @@ public class BatteryStatsImpl extends BatteryStats { } private class PowerStatsCollectorInjector implements CpuPowerStatsCollector.Injector, - MobileRadioPowerStatsCollector.Injector, WifiPowerStatsCollector.Injector { + MobileRadioPowerStatsCollector.Injector, WifiPowerStatsCollector.Injector, + BluetoothPowerStatsCollector.Injector { private PackageManager mPackageManager; private PowerStatsCollector.ConsumedEnergyRetriever mConsumedEnergyRetriever; private NetworkStatsManager mNetworkStatsManager; private TelephonyManager mTelephonyManager; private WifiManager mWifiManager; + private BluetoothPowerStatsCollector.BluetoothStatsRetriever mBluetoothStatsRetriever; void setContext(Context context) { mPackageManager = context.getPackageManager(); @@ -1940,6 +1978,8 @@ public class BatteryStatsImpl extends BatteryStats { mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class); mTelephonyManager = context.getSystemService(TelephonyManager.class); mWifiManager = context.getSystemService(WifiManager.class); + mBluetoothStatsRetriever = new BluetoothStatsRetrieverImpl( + context.getSystemService(BluetoothManager.class)); } @Override @@ -2018,6 +2058,11 @@ public class BatteryStatsImpl extends BatteryStats { } @Override + public BluetoothPowerStatsCollector.BluetoothStatsRetriever getBluetoothStatsRetriever() { + return mBluetoothStatsRetriever; + } + + @Override public LongSupplier getCallDurationSupplier() { return () -> mPhoneOnTimer.getTotalTimeLocked(mClock.elapsedRealtime() * 1000, STATS_SINCE_CHARGED); @@ -6774,6 +6819,24 @@ public class BatteryStatsImpl extends BatteryStats { } } + private void retrieveBluetoothScanTimesLocked( + BluetoothPowerStatsCollector.BluetoothStatsRetriever.Callback callback) { + long elapsedTimeUs = mClock.elapsedRealtime() * 1000; + for (int i = mUidStats.size() - 1; i >= 0; i--) { + Uid uidStats = mUidStats.valueAt(i); + if (uidStats.mBluetoothScanTimer == null) { + continue; + } + + long scanTimeUs = mBluetoothScanTimer.getTotalTimeLocked(elapsedTimeUs, + STATS_SINCE_CHARGED); + if (scanTimeUs != 0) { + int uid = mUidStats.keyAt(i); + callback.onBluetoothScanTime(uid, (scanTimeUs + 500) / 1000); + } + } + } + @GuardedBy("this") private void noteWifiRadioApWakeupLocked(final long elapsedRealtimeMillis, final long uptimeMillis, int uid) { @@ -11202,6 +11265,10 @@ public class BatteryStatsImpl extends BatteryStats { mWifiPowerStatsCollector = new WifiPowerStatsCollector(mPowerStatsCollectorInjector); mWifiPowerStatsCollector.addConsumer(this::recordPowerStats); + mBluetoothPowerStatsCollector = new BluetoothPowerStatsCollector( + mPowerStatsCollectorInjector); + mBluetoothPowerStatsCollector.addConsumer(this::recordPowerStats); + mStartCount++; initTimersAndCounters(); mOnBattery = mOnBatteryInternal = false; @@ -13146,6 +13213,10 @@ public class BatteryStatsImpl extends BatteryStats { @GuardedBy("this") public void updateBluetoothStateLocked(@Nullable final BluetoothActivityEnergyInfo info, final long consumedChargeUC, long elapsedRealtimeMs, long uptimeMs) { + if (mBluetoothPowerStatsCollector.isEnabled()) { + return; + } + if (DEBUG_ENERGY) { Slog.d(TAG, "Updating bluetooth stats: " + info); } @@ -13153,6 +13224,7 @@ public class BatteryStatsImpl extends BatteryStats { if (info == null) { return; } + if (!mOnBatteryInternal || mIgnoreNextExternalStats) { mLastBluetoothActivityInfo.set(info); return; @@ -13187,7 +13259,6 @@ public class BatteryStatsImpl extends BatteryStats { (mGlobalEnergyConsumerStats != null && mBluetoothPowerCalculator != null && consumedChargeUC > 0) ? new SparseDoubleArray() : null; - long totalScanTimeMs = 0; final int uidCount = mUidStats.size(); @@ -14616,6 +14687,10 @@ public class BatteryStatsImpl extends BatteryStats { mPowerStatsCollectorEnabled.get(BatteryConsumer.POWER_COMPONENT_WIFI)); mWifiPowerStatsCollector.schedule(); + mBluetoothPowerStatsCollector.setEnabled( + mPowerStatsCollectorEnabled.get(BatteryConsumer.POWER_COMPONENT_BLUETOOTH)); + mBluetoothPowerStatsCollector.schedule(); + mSystemReady = true; } @@ -14632,6 +14707,8 @@ public class BatteryStatsImpl extends BatteryStats { return mMobileRadioPowerStatsCollector; case BatteryConsumer.POWER_COMPONENT_WIFI: return mWifiPowerStatsCollector; + case BatteryConsumer.POWER_COMPONENT_BLUETOOTH: + return mBluetoothPowerStatsCollector; } return null; } @@ -16168,6 +16245,7 @@ public class BatteryStatsImpl extends BatteryStats { mCpuPowerStatsCollector.forceSchedule(); mMobileRadioPowerStatsCollector.forceSchedule(); mWifiPowerStatsCollector.forceSchedule(); + mBluetoothPowerStatsCollector.forceSchedule(); } /** @@ -16187,6 +16265,7 @@ public class BatteryStatsImpl extends BatteryStats { mCpuPowerStatsCollector.collectAndDump(pw); mMobileRadioPowerStatsCollector.collectAndDump(pw); mWifiPowerStatsCollector.collectAndDump(pw); + mBluetoothPowerStatsCollector.collectAndDump(pw); } private final Runnable mWriteAsyncRunnable = () -> { diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java new file mode 100644 index 000000000000..8a5085b0b34b --- /dev/null +++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java @@ -0,0 +1,332 @@ +/* + * 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 android.bluetooth.BluetoothActivityEnergyInfo; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.UidTraffic; +import android.content.pm.PackageManager; +import android.hardware.power.stats.EnergyConsumerType; +import android.os.BatteryConsumer; +import android.os.Handler; +import android.os.PersistableBundle; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.os.Clock; +import com.android.internal.os.PowerStats; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.IntSupplier; + +public class BluetoothPowerStatsCollector extends PowerStatsCollector { + private static final String TAG = "BluetoothPowerStatsCollector"; + + private static final long BLUETOOTH_ACTIVITY_REQUEST_TIMEOUT = 20000; + + private static final long ENERGY_UNSPECIFIED = -1; + + interface BluetoothStatsRetriever { + interface Callback { + void onBluetoothScanTime(int uid, long scanTimeMs); + } + + void retrieveBluetoothScanTimes(Callback callback); + + boolean requestControllerActivityEnergyInfo(Executor executor, + BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback callback); + } + + interface Injector { + Handler getHandler(); + Clock getClock(); + PowerStatsUidResolver getUidResolver(); + long getPowerStatsCollectionThrottlePeriod(String powerComponentName); + PackageManager getPackageManager(); + ConsumedEnergyRetriever getConsumedEnergyRetriever(); + IntSupplier getVoltageSupplier(); + BluetoothStatsRetriever getBluetoothStatsRetriever(); + } + + private final Injector mInjector; + + private BluetoothPowerStatsLayout mLayout; + private boolean mIsInitialized; + private PowerStats mPowerStats; + private long[] mDeviceStats; + private BluetoothStatsRetriever mBluetoothStatsRetriever; + private ConsumedEnergyRetriever mConsumedEnergyRetriever; + private IntSupplier mVoltageSupplier; + private int[] mEnergyConsumerIds = new int[0]; + private long[] mLastConsumedEnergyUws; + private int mLastVoltageMv; + + private long mLastRxTime; + private long mLastTxTime; + private long mLastIdleTime; + + private static class UidStats { + public long rxCount; + public long lastRxCount; + public long txCount; + public long lastTxCount; + public long scanTime; + public long lastScanTime; + } + + private final SparseArray<UidStats> mUidStats = new SparseArray<>(); + + BluetoothPowerStatsCollector(Injector injector) { + super(injector.getHandler(), injector.getPowerStatsCollectionThrottlePeriod( + BatteryConsumer.powerComponentIdToString( + BatteryConsumer.POWER_COMPONENT_BLUETOOTH)), + injector.getUidResolver(), + injector.getClock()); + mInjector = injector; + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled) { + PackageManager packageManager = mInjector.getPackageManager(); + super.setEnabled(packageManager != null + && packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)); + } else { + super.setEnabled(false); + } + } + + private boolean ensureInitialized() { + if (mIsInitialized) { + return true; + } + + if (!isEnabled()) { + return false; + } + + mConsumedEnergyRetriever = mInjector.getConsumedEnergyRetriever(); + mVoltageSupplier = mInjector.getVoltageSupplier(); + mBluetoothStatsRetriever = mInjector.getBluetoothStatsRetriever(); + mEnergyConsumerIds = + mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.BLUETOOTH); + mLastConsumedEnergyUws = new long[mEnergyConsumerIds.length]; + Arrays.fill(mLastConsumedEnergyUws, ENERGY_UNSPECIFIED); + + mLayout = new BluetoothPowerStatsLayout(); + mLayout.addDeviceBluetoothControllerActivity(); + mLayout.addDeviceSectionEnergyConsumers(mEnergyConsumerIds.length); + mLayout.addDeviceSectionUsageDuration(); + mLayout.addDeviceSectionPowerEstimate(); + mLayout.addUidTrafficStats(); + mLayout.addUidSectionPowerEstimate(); + + PersistableBundle extras = new PersistableBundle(); + mLayout.toExtras(extras); + PowerStats.Descriptor powerStatsDescriptor = new PowerStats.Descriptor( + BatteryConsumer.POWER_COMPONENT_BLUETOOTH, mLayout.getDeviceStatsArrayLength(), + null, 0, mLayout.getUidStatsArrayLength(), + extras); + mPowerStats = new PowerStats(powerStatsDescriptor); + mDeviceStats = mPowerStats.stats; + + mIsInitialized = true; + return true; + } + + @Override + protected PowerStats collectStats() { + if (!ensureInitialized()) { + return null; + } + + mPowerStats.uidStats.clear(); + + collectBluetoothActivityInfo(); + collectBluetoothScanStats(); + + if (mEnergyConsumerIds.length != 0) { + collectEnergyConsumers(); + } + + return mPowerStats; + } + + private void collectBluetoothActivityInfo() { + CompletableFuture<BluetoothActivityEnergyInfo> immediateFuture = new CompletableFuture<>(); + boolean success = mBluetoothStatsRetriever.requestControllerActivityEnergyInfo( + Runnable::run, + new BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback() { + @Override + public void onBluetoothActivityEnergyInfoAvailable( + BluetoothActivityEnergyInfo info) { + immediateFuture.complete(info); + } + + @Override + public void onBluetoothActivityEnergyInfoError(int error) { + immediateFuture.completeExceptionally( + new RuntimeException("error: " + error)); + } + }); + + if (!success) { + return; + } + + BluetoothActivityEnergyInfo activityInfo; + try { + activityInfo = immediateFuture.get(BLUETOOTH_ACTIVITY_REQUEST_TIMEOUT, + TimeUnit.MILLISECONDS); + } catch (Exception e) { + Slog.e(TAG, "Cannot acquire BluetoothActivityEnergyInfo", e); + activityInfo = null; + } + + if (activityInfo == null) { + return; + } + + long rxTime = activityInfo.getControllerRxTimeMillis(); + long rxTimeDelta = Math.max(0, rxTime - mLastRxTime); + mLayout.setDeviceRxTime(mDeviceStats, rxTimeDelta); + mLastRxTime = rxTime; + + long txTime = activityInfo.getControllerTxTimeMillis(); + long txTimeDelta = Math.max(0, txTime - mLastTxTime); + mLayout.setDeviceTxTime(mDeviceStats, txTimeDelta); + mLastTxTime = txTime; + + long idleTime = activityInfo.getControllerIdleTimeMillis(); + long idleTimeDelta = Math.max(0, idleTime - mLastIdleTime); + mLayout.setDeviceIdleTime(mDeviceStats, idleTimeDelta); + mLastIdleTime = idleTime; + + mPowerStats.durationMs = rxTimeDelta + txTimeDelta + idleTimeDelta; + + List<UidTraffic> uidTraffic = activityInfo.getUidTraffic(); + for (int i = uidTraffic.size() - 1; i >= 0; i--) { + UidTraffic ut = uidTraffic.get(i); + int uid = mUidResolver.mapUid(ut.getUid()); + UidStats counts = mUidStats.get(uid); + if (counts == null) { + counts = new UidStats(); + mUidStats.put(uid, counts); + } + counts.rxCount += ut.getRxBytes(); + counts.txCount += ut.getTxBytes(); + } + + for (int i = mUidStats.size() - 1; i >= 0; i--) { + UidStats counts = mUidStats.valueAt(i); + long rxDelta = Math.max(0, counts.rxCount - counts.lastRxCount); + counts.lastRxCount = counts.rxCount; + counts.rxCount = 0; + + long txDelta = Math.max(0, counts.txCount - counts.lastTxCount); + counts.lastTxCount = counts.txCount; + counts.txCount = 0; + + if (rxDelta != 0 || txDelta != 0) { + int uid = mUidStats.keyAt(i); + long[] stats = mPowerStats.uidStats.get(uid); + if (stats == null) { + stats = new long[mLayout.getUidStatsArrayLength()]; + mPowerStats.uidStats.put(uid, stats); + } + + mLayout.setUidRxBytes(stats, rxDelta); + mLayout.setUidTxBytes(stats, txDelta); + } + } + } + + private void collectBluetoothScanStats() { + mBluetoothStatsRetriever.retrieveBluetoothScanTimes((uid, scanTimeMs) -> { + uid = mUidResolver.mapUid(uid); + UidStats uidStats = mUidStats.get(uid); + if (uidStats == null) { + uidStats = new UidStats(); + mUidStats.put(uid, uidStats); + } + uidStats.scanTime += scanTimeMs; + }); + + long totalScanTime = 0; + for (int i = mUidStats.size() - 1; i >= 0; i--) { + UidStats counts = mUidStats.valueAt(i); + if (counts.scanTime == 0) { + continue; + } + + long delta = Math.max(0, counts.scanTime - counts.lastScanTime); + counts.lastScanTime = counts.scanTime; + counts.scanTime = 0; + + if (delta != 0) { + int uid = mUidStats.keyAt(i); + long[] stats = mPowerStats.uidStats.get(uid); + if (stats == null) { + stats = new long[mLayout.getUidStatsArrayLength()]; + mPowerStats.uidStats.put(uid, stats); + } + + mLayout.setUidScanTime(stats, delta); + totalScanTime += delta; + } + } + + mLayout.setDeviceScanTime(mDeviceStats, totalScanTime); + } + + private void collectEnergyConsumers() { + int voltageMv = mVoltageSupplier.getAsInt(); + if (voltageMv <= 0) { + Slog.wtf(TAG, "Unexpected battery voltage (" + voltageMv + + " mV) when querying energy consumers"); + return; + } + + int averageVoltage = mLastVoltageMv != 0 ? (mLastVoltageMv + voltageMv) / 2 : voltageMv; + mLastVoltageMv = voltageMv; + + long[] energyUws = mConsumedEnergyRetriever.getConsumedEnergyUws(mEnergyConsumerIds); + if (energyUws == null) { + return; + } + + for (int i = energyUws.length - 1; i >= 0; i--) { + long energyDelta = mLastConsumedEnergyUws[i] != ENERGY_UNSPECIFIED + ? energyUws[i] - mLastConsumedEnergyUws[i] : 0; + if (energyDelta < 0) { + // Likely, restart of powerstats HAL + energyDelta = 0; + } + mLayout.setConsumedEnergy(mPowerStats.stats, i, uJtoUc(energyDelta, averageVoltage)); + mLastConsumedEnergyUws[i] = energyUws[i]; + } + } + + @Override + protected void onUidRemoved(int uid) { + super.onUidRemoved(uid); + mUidStats.remove(uid); + } +} diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsLayout.java new file mode 100644 index 000000000000..9358b5ef20a8 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsLayout.java @@ -0,0 +1,143 @@ +/* + * 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 android.annotation.NonNull; +import android.os.PersistableBundle; + +import com.android.internal.os.PowerStats; + +public class BluetoothPowerStatsLayout extends PowerStatsLayout { + private static final String EXTRA_DEVICE_RX_TIME_POSITION = "dt-rx"; + private static final String EXTRA_DEVICE_TX_TIME_POSITION = "dt-tx"; + private static final String EXTRA_DEVICE_IDLE_TIME_POSITION = "dt-idle"; + private static final String EXTRA_DEVICE_SCAN_TIME_POSITION = "dt-scan"; + private static final String EXTRA_UID_RX_BYTES_POSITION = "ub-rx"; + private static final String EXTRA_UID_TX_BYTES_POSITION = "ub-tx"; + private static final String EXTRA_UID_SCAN_TIME_POSITION = "ut-scan"; + + private int mDeviceRxTimePosition; + private int mDeviceTxTimePosition; + private int mDeviceIdleTimePosition; + private int mDeviceScanTimePosition; + private int mUidRxBytesPosition; + private int mUidTxBytesPosition; + private int mUidScanTimePosition; + + BluetoothPowerStatsLayout() { + } + + BluetoothPowerStatsLayout(@NonNull PowerStats.Descriptor descriptor) { + super(descriptor); + } + + void addDeviceBluetoothControllerActivity() { + mDeviceRxTimePosition = addDeviceSection(1, "rx"); + mDeviceTxTimePosition = addDeviceSection(1, "tx"); + mDeviceIdleTimePosition = addDeviceSection(1, "idle"); + mDeviceScanTimePosition = addDeviceSection(1, "scan", FLAG_OPTIONAL); + } + + void addUidTrafficStats() { + mUidRxBytesPosition = addUidSection(1, "rx-B"); + mUidTxBytesPosition = addUidSection(1, "tx-B"); + mUidScanTimePosition = addUidSection(1, "scan", FLAG_OPTIONAL); + } + + public void setDeviceRxTime(long[] stats, long durationMillis) { + stats[mDeviceRxTimePosition] = durationMillis; + } + + public long getDeviceRxTime(long[] stats) { + return stats[mDeviceRxTimePosition]; + } + + public void setDeviceTxTime(long[] stats, long durationMillis) { + stats[mDeviceTxTimePosition] = durationMillis; + } + + public long getDeviceTxTime(long[] stats) { + return stats[mDeviceTxTimePosition]; + } + + public void setDeviceIdleTime(long[] stats, long durationMillis) { + stats[mDeviceIdleTimePosition] = durationMillis; + } + + public long getDeviceIdleTime(long[] stats) { + return stats[mDeviceIdleTimePosition]; + } + + public void setDeviceScanTime(long[] stats, long durationMillis) { + stats[mDeviceScanTimePosition] = durationMillis; + } + + public long getDeviceScanTime(long[] stats) { + return stats[mDeviceScanTimePosition]; + } + + public void setUidRxBytes(long[] stats, long count) { + stats[mUidRxBytesPosition] = count; + } + + public long getUidRxBytes(long[] stats) { + return stats[mUidRxBytesPosition]; + } + + public void setUidTxBytes(long[] stats, long count) { + stats[mUidTxBytesPosition] = count; + } + + public long getUidTxBytes(long[] stats) { + return stats[mUidTxBytesPosition]; + } + + public void setUidScanTime(long[] stats, long count) { + stats[mUidScanTimePosition] = count; + } + + public long getUidScanTime(long[] stats) { + return stats[mUidScanTimePosition]; + } + + /** + * Copies the elements of the stats array layout into <code>extras</code> + */ + public void toExtras(PersistableBundle extras) { + super.toExtras(extras); + extras.putInt(EXTRA_DEVICE_RX_TIME_POSITION, mDeviceRxTimePosition); + extras.putInt(EXTRA_DEVICE_TX_TIME_POSITION, mDeviceTxTimePosition); + extras.putInt(EXTRA_DEVICE_IDLE_TIME_POSITION, mDeviceIdleTimePosition); + extras.putInt(EXTRA_DEVICE_SCAN_TIME_POSITION, mDeviceScanTimePosition); + extras.putInt(EXTRA_UID_RX_BYTES_POSITION, mUidRxBytesPosition); + extras.putInt(EXTRA_UID_TX_BYTES_POSITION, mUidTxBytesPosition); + extras.putInt(EXTRA_UID_SCAN_TIME_POSITION, mUidScanTimePosition); + } + + /** + * Retrieves elements of the stats array layout from <code>extras</code> + */ + public void fromExtras(PersistableBundle extras) { + super.fromExtras(extras); + mDeviceRxTimePosition = extras.getInt(EXTRA_DEVICE_RX_TIME_POSITION); + mDeviceTxTimePosition = extras.getInt(EXTRA_DEVICE_TX_TIME_POSITION); + mDeviceIdleTimePosition = extras.getInt(EXTRA_DEVICE_IDLE_TIME_POSITION); + mDeviceScanTimePosition = extras.getInt(EXTRA_DEVICE_SCAN_TIME_POSITION); + mUidRxBytesPosition = extras.getInt(EXTRA_UID_RX_BYTES_POSITION); + mUidTxBytesPosition = extras.getInt(EXTRA_UID_TX_BYTES_POSITION); + mUidScanTimePosition = extras.getInt(EXTRA_UID_SCAN_TIME_POSITION); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsCollectorTest.java new file mode 100644 index 000000000000..02c7b745b24c --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsCollectorTest.java @@ -0,0 +1,315 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +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.platform.test.ravenwood.RavenwoodRule; +import android.util.IndentingPrintWriter; +import android.util.SparseLongArray; + +import com.android.internal.os.Clock; +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.IntSupplier; + +public class BluetoothPowerStatsCollectorTest { + private static final int APP_UID1 = 42; + private static final int APP_UID2 = 24; + private static final int ISOLATED_UID = 99123; + + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Rule(order = 1) + public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() + .setPowerStatsThrottlePeriodMillis(BatteryConsumer.POWER_COMPONENT_BLUETOOTH, 1000); + + private MockBatteryStatsImpl mBatteryStats; + + private final MockClock mClock = mStatsRule.getMockClock(); + + @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 List<PowerStats> mRecordedPowerStats = new ArrayList<>(); + + private 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 () -> 3500; + } + + @Override + public BluetoothPowerStatsCollector.BluetoothStatsRetriever + getBluetoothStatsRetriever() { + return mBluetoothStatsRetriever; + } + }; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)).thenReturn(true); + mPowerStatsUidResolver.noteIsolatedUidAdded(ISOLATED_UID, APP_UID2); + mBatteryStats = mStatsRule.getBatteryStats(); + } + + @SuppressWarnings("GuardedBy") + @Test + public void triggering() throws Throwable { + PowerStatsCollector collector = mBatteryStats.getPowerStatsCollector( + BatteryConsumer.POWER_COMPONENT_BLUETOOTH); + collector.addConsumer(mRecordedPowerStats::add); + + mBatteryStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_BLUETOOTH, + true); + + mBatteryStats.setDummyExternalStatsSync(new MockBatteryStatsImpl.DummyExternalStatsSync(){ + @Override + public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) { + collector.schedule(); + } + }); + + mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1000, 2000, 3000, 600); + + // This should trigger a sample collection to establish a baseline + mBatteryStats.onSystemReady(mContext); + + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(70000, 70000); + mBatteryStats.noteUidProcessStateLocked(APP_UID1, ActivityManager.PROCESS_STATE_TOP, + mClock.realtime, mClock.uptime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + } + + @Test + public void collectStats() { + PowerStats powerStats = collectPowerStats(); + assertThat(powerStats.durationMs).isEqualTo(7200); + + BluetoothPowerStatsLayout layout = new BluetoothPowerStatsLayout(powerStats.descriptor); + assertThat(layout.getDeviceRxTime(powerStats.stats)).isEqualTo(6000); + assertThat(layout.getDeviceTxTime(powerStats.stats)).isEqualTo(1000); + assertThat(layout.getDeviceIdleTime(powerStats.stats)).isEqualTo(200); + assertThat(layout.getDeviceScanTime(powerStats.stats)).isEqualTo(800); + assertThat(layout.getConsumedEnergy(powerStats.stats, 0)) + .isEqualTo((64321 - 10000) * 1000 / 3500); + + assertThat(powerStats.uidStats.size()).isEqualTo(2); + long[] actual1 = powerStats.uidStats.get(APP_UID1); + assertThat(layout.getUidRxBytes(actual1)).isEqualTo(1000); + assertThat(layout.getUidTxBytes(actual1)).isEqualTo(2000); + assertThat(layout.getUidScanTime(actual1)).isEqualTo(100); + + // Combines APP_UID2 and ISOLATED_UID + long[] actual2 = powerStats.uidStats.get(APP_UID2); + assertThat(layout.getUidRxBytes(actual2)).isEqualTo(8000); + assertThat(layout.getUidTxBytes(actual2)).isEqualTo(10000); + assertThat(layout.getUidScanTime(actual2)).isEqualTo(700); + + assertThat(powerStats.uidStats.get(ISOLATED_UID)).isNull(); + } + + @Test + public void dump() throws Throwable { + PowerStats powerStats = collectPowerStats(); + StringWriter sw = new StringWriter(); + IndentingPrintWriter pw = new IndentingPrintWriter(sw); + powerStats.dump(pw); + pw.flush(); + String dump = sw.toString(); + assertThat(dump).contains("duration=7200"); + assertThat(dump).contains( + "rx: 6000 tx: 1000 idle: 200 scan: 800 energy: " + ((64321 - 10000) * 1000 / 3500)); + assertThat(dump).contains("UID 24: rx-B: 8000 tx-B: 10000 scan: 700"); + assertThat(dump).contains("UID 42: rx-B: 1000 tx-B: 2000 scan: 100"); + } + + private PowerStats collectPowerStats() { + BluetoothPowerStatsCollector collector = new BluetoothPowerStatsCollector(mInjector); + collector.setEnabled(true); + + when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.BLUETOOTH)) + .thenReturn(new int[]{777}); + + mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1000, 600, 100, 2000, + mockUidTraffic(APP_UID1, 100, 200), + mockUidTraffic(APP_UID2, 300, 400), + mockUidTraffic(ISOLATED_UID, 500, 600)); + + mUidScanTimes.put(APP_UID1, 100); + + when(mConsumedEnergyRetriever.getConsumedEnergyUws(eq(new int[]{777}))) + .thenReturn(new long[]{10000}); + + // Establish a baseline + collector.collectStats(); + + mBluetoothActivityEnergyInfo = mockBluetoothActivityEnergyInfo(1100, 6600, 1100, 2200, + mockUidTraffic(APP_UID1, 1100, 2200), + mockUidTraffic(APP_UID2, 3300, 4400), + mockUidTraffic(ISOLATED_UID, 5500, 6600)); + + mUidScanTimes.clear(); + mUidScanTimes.put(APP_UID1, 200); + mUidScanTimes.put(APP_UID2, 300); + mUidScanTimes.put(ISOLATED_UID, 400); + + when(mConsumedEnergyRetriever.getConsumedEnergyUws(eq(new int[]{777}))) + .thenReturn(new long[]{64321}); + + mStatsRule.setTime(20000, 20000); + return collector.collectStats(); + } + + 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; + } + } +} |