summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java3
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java59
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java83
-rw-r--r--services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java332
-rw-r--r--services/core/java/com/android/server/power/stats/BluetoothPowerStatsLayout.java143
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsCollectorTest.java315
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;
+ }
+ }
+}