Merge "BatteryStats receives custom measured energies" into sc-dev

This commit is contained in:
Adam Bookatz
2021-02-11 08:22:25 +00:00
committed by Android (Google) Code Review
9 changed files with 685 additions and 428 deletions

View File

@@ -7173,6 +7173,20 @@ public class BatteryStatsImpl extends BatteryStats {
.getAccumulatedStandardBucketEnergy(MeasuredEnergyStats.ENERGY_BUCKET_SCREEN_DOZE);
}
/**
* Returns the energy in microjoules that the given custom energy bucket consumed.
* Will return {@link #ENERGY_DATA_UNAVAILABLE} if data is unavailable
*
* @param customEnergyBucket custom energy bucket of interest
* @return energy (in microjoules) used by this uid for this energy bucket
*/
public long getCustomMeasuredEnergyMicroJoules(int customEnergyBucket) {
if (mGlobalMeasuredEnergyStats == null) {
return ENERGY_DATA_UNAVAILABLE;
}
return mGlobalMeasuredEnergyStats.getAccumulatedCustomBucketEnergy(customEnergyBucket);
}
@Override public long getStartClockTime() {
final long currentTimeMs = System.currentTimeMillis();
if ((currentTimeMs > MILLISECONDS_IN_YEAR
@@ -7941,6 +7955,13 @@ public class BatteryStatsImpl extends BatteryStats {
.updateStandardBucket(energyBucket, energyDeltaUJ, accumulate);
}
/** Adds the given energy to the given custom energy bucket for this uid. */
private void addEnergyToCustomBucketLocked(long energyDeltaUJ, int energyBucket,
boolean accumulate) {
getOrCreateMeasuredEnergyStatsLocked()
.updateCustomBucket(energyBucket, energyDeltaUJ, accumulate);
}
/**
* Returns the energy used by this uid for a standard energy bucket of interest.
* @param bucket standard energy bucket of interest
@@ -7957,6 +7978,22 @@ public class BatteryStatsImpl extends BatteryStats {
return mUidMeasuredEnergyStats.getAccumulatedStandardBucketEnergy(bucket);
}
/**
* Returns the energy used by this uid for a custom energy bucket of interest.
* @param customEnergyBucket custom energy bucket of interest
* @return energy (in microjoules) used by this uid for this energy bucket
*/
public long getCustomMeasuredEnergyMicroJoules(int customEnergyBucket) {
if (mBsi.mGlobalMeasuredEnergyStats == null
|| !mBsi.mGlobalMeasuredEnergyStats.isValidCustomBucket(customEnergyBucket)) {
return ENERGY_DATA_UNAVAILABLE;
}
if (mUidMeasuredEnergyStats == null) {
return 0L; // It is supported, but was never filled, so it must be 0
}
return mUidMeasuredEnergyStats.getAccumulatedCustomBucketEnergy(customEnergyBucket);
}
/**
* Gets the minimum of the uid's foreground activity time and its PROCESS_STATE_TOP time
* since last marked. Also sets the mark time for both these timers.
@@ -12464,6 +12501,42 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
/**
* Accumulate Custom energy bucket energy, globally and for each app.
*
* @param totalEnergyUJ energy (microjoules) used for this bucket since this was last called.
* @param uidEnergies map of uid->energy (microjoules) for this bucket since last called.
* Data inside uidEnergies will not be modified (treated immutable).
*/
public void updateCustomMeasuredEnergyDataLocked(int customEnergyBucket,
long totalEnergyUJ, @Nullable SparseLongArray uidEnergies) {
if (DEBUG_ENERGY) {
Slog.d(TAG, "Updating attributed measured energy stats for custom bucket "
+ customEnergyBucket
+ " with total energy " + totalEnergyUJ
+ " and uid energies " + String.valueOf(uidEnergies));
}
if (mGlobalMeasuredEnergyStats == null) return;
if (!mOnBatteryInternal || mIgnoreNextExternalStats || totalEnergyUJ <= 0) return;
mGlobalMeasuredEnergyStats.updateCustomBucket(customEnergyBucket, totalEnergyUJ, true);
if (uidEnergies == null) return;
final int numUids = uidEnergies.size();
for (int i = 0; i < numUids; i++) {
final int uidInt = mapUid(uidEnergies.keyAt(i));
final long uidEnergyUJ = uidEnergies.valueAt(i);
if (uidEnergyUJ == 0) continue;
// TODO: Worry about uids not in BSI currently, including uninstalled uids 'coming back'
// Specifically: What if the uid had been removed? We'll re-create it now.
// And if we instead use getAvailableUidStatsLocked() and chec for null, then we might
// not create a Uid even when we should be (say, the app's first event, somehow, was to
// use GPU). I guess that CPU/kernel data might already have this problem?
final Uid uidObj = getUidStatsLocked(uidInt);
uidObj.addEnergyToCustomBucketLocked(uidEnergyUJ, customEnergyBucket, true);
}
}
/**
* Read and record Rail Energy data.
*/

View File

@@ -1,66 +0,0 @@
/*
* Copyright (C) 2020 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.internal.power;
import android.annotation.IntDef;
import com.android.internal.os.RailStats;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Interface to provide subsystem energy data.
* TODO: replace this and {@link RailStats} once b/173077356 is done
*/
public interface MeasuredEnergyArray {
int SUBSYSTEM_UNKNOWN = -1;
int SUBSYSTEM_DISPLAY = 0;
int NUMBER_SUBSYSTEMS = 1;
String[] SUBSYSTEM_NAMES = {"display"};
@IntDef(prefix = { "SUBSYSTEM_" }, value = {
SUBSYSTEM_UNKNOWN,
SUBSYSTEM_DISPLAY,
})
@Retention(RetentionPolicy.SOURCE)
@interface MeasuredEnergySubsystem {}
/**
* Get the subsystem at an index in array.
*
* @param index into the array.
* @return subsystem.
*/
@MeasuredEnergySubsystem
int getSubsystem(int index);
/**
* Get the energy (in microjoules) consumed since boot of the subsystem at an index.
*
* @param index into the array.
* @return energy (in microjoules) consumed since boot.
*/
long getEnergy(int index);
/**
* Return number of subsystems in the array.
*/
int size();
}

View File

@@ -28,7 +28,6 @@ import android.util.Slog;
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.power.MeasuredEnergyArray.MeasuredEnergySubsystem;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -247,7 +246,7 @@ public class MeasuredEnergyStats {
}
/**
* Map {@link MeasuredEnergySubsystem} and device state to Display {@link StandardEnergyBucket}.
* Map {@link android.view.Display} STATE_ to corresponding {@link StandardEnergyBucket}.
*/
public static @StandardEnergyBucket int getDisplayEnergyBucket(int screenState) {
if (Display.isOnState(screenState)) {
@@ -450,7 +449,8 @@ public class MeasuredEnergyStats {
return bucket >= 0 && bucket < NUMBER_STANDARD_ENERGY_BUCKETS;
}
private boolean isValidCustomBucket(int customBucket) {
/** Returns whether the given custom bucket is valid (exists) on this device. */
public boolean isValidCustomBucket(int customBucket) {
return customBucket >= 0
&& customBucketToIndex(customBucket) < mAccumulatedEnergiesMicroJoules.length;
}

View File

@@ -24,6 +24,7 @@ import android.os.BatteryStats;
import android.os.BatteryStats.HistoryItem;
import android.os.BatteryStats.Uid.Sensor;
import android.os.WorkSource;
import android.util.SparseLongArray;
import android.view.Display;
import androidx.test.filters.SmallTest;
@@ -583,6 +584,95 @@ public class BatteryStatsNoteTest extends TestCase {
checkMeasuredEnergy("H", uid1, blame1, uid2, blame2, globalDoze, bi);
}
@SmallTest
public void testUpdateCustomMeasuredEnergyDataLocked_neverCalled() {
final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
bi.setOnBatteryInternal(true);
final int uid1 = 11500;
final int uid2 = 11501;
// Initially, all custom buckets report energy of 0.
checkCustomMeasuredEnergy("0", 0, 0, uid1, 0, 0, uid2, 0, 0, bi);
}
@SmallTest
public void testUpdateCustomMeasuredEnergyDataLocked() {
final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
final int bucketA = 0; // Custom bucket 0
final int bucketB = 1; // Custom bucket 1
long totalBlameA = 0; // Total energy consumption for bucketA (may exceed sum of uids)
long totalBlameB = 0; // Total energy consumption for bucketB (may exceed sum of uids)
final int uid1 = 10500;
long blame1A = 0; // Blame for uid1 in bucketA
long blame1B = 0; // Blame for uid1 in bucketB
final int uid2 = 10501;
long blame2A = 0; // Blame for uid2 in bucketA
long blame2B = 0; // Blame for uid2 in bucketB
final SparseLongArray newEnergiesA = new SparseLongArray(2);
final SparseLongArray newEnergiesB = new SparseLongArray(2);
// ----- Case A: battery off (so blame does not increase)
bi.setOnBatteryInternal(false);
newEnergiesA.put(uid1, 20_000);
// Implicit newEnergiesA.put(uid2, 0);
bi.updateCustomMeasuredEnergyDataLocked(bucketA, 500_000, newEnergiesA);
newEnergiesB.put(uid1, 60_000);
// Implicit newEnergiesB.put(uid2, 0);
bi.updateCustomMeasuredEnergyDataLocked(bucketB, 700_000, newEnergiesB);
checkCustomMeasuredEnergy(
"A", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
// ----- Case B: battery on
bi.setOnBatteryInternal(true);
newEnergiesA.put(uid1, 7_000); blame1A += 7_000;
// Implicit newEnergiesA.put(uid2, 0); blame2A += 0;
bi.updateCustomMeasuredEnergyDataLocked(bucketA, 310_000, newEnergiesA);
totalBlameA += 310_000;
newEnergiesB.put(uid1, 63_000); blame1B += 63_000;
newEnergiesB.put(uid2, 15_000); blame2B += 15_000;
bi.updateCustomMeasuredEnergyDataLocked(bucketB, 790_000, newEnergiesB);
totalBlameB += 790_000;
checkCustomMeasuredEnergy(
"B", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
// ----- Case C: battery still on
newEnergiesA.delete(uid1); blame1A += 0;
newEnergiesA.put(uid2, 16_000); blame2A += 16_000;
bi.updateCustomMeasuredEnergyDataLocked(bucketA, 560_000, newEnergiesA);
totalBlameA += 560_000;
bi.updateCustomMeasuredEnergyDataLocked(bucketB, 10_000, null);
totalBlameB += 10_000;
checkCustomMeasuredEnergy(
"C", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
// ----- Case D: battery still on
bi.updateCustomMeasuredEnergyDataLocked(bucketA, 0, newEnergiesA);
bi.updateCustomMeasuredEnergyDataLocked(bucketB, 15_000, new SparseLongArray(1));
totalBlameB += 15_000;
checkCustomMeasuredEnergy(
"D", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
}
private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) {
// Note that noteUidProcessStateLocked uses ActivityManager process states.
if (fgOn) {
@@ -610,4 +700,29 @@ public class BatteryStatsNoteTest extends TestCase {
assertEquals("Wrong doze for Case " + caseName, globalDoze,
bi.getScreenDozeEnergy());
}
private void checkCustomMeasuredEnergy(String caseName,
long totalBlameA, long totalBlameB,
int uid1, long blame1A, long blame1B,
int uid2, long blame2A, long blame2B,
MockBatteryStatsImpl bi) {
assertEquals("Wrong total blame in bucket 0 for Case " + caseName, totalBlameA,
bi.getCustomMeasuredEnergyMicroJoules(0));
assertEquals("Wrong total blame in bucket 1 for Case " + caseName, totalBlameB,
bi.getCustomMeasuredEnergyMicroJoules(1));
assertEquals("Wrong uid1 blame in bucket 0 for Case " + caseName, blame1A,
bi.getUidStatsLocked(uid1).getCustomMeasuredEnergyMicroJoules(0));
assertEquals("Wrong uid1 blame in bucket 1 for Case " + caseName, blame1B,
bi.getUidStatsLocked(uid1).getCustomMeasuredEnergyMicroJoules(1));
assertEquals("Wrong uid2 blame in bucket 0 for Case " + caseName, blame2A,
bi.getUidStatsLocked(uid2).getCustomMeasuredEnergyMicroJoules(0));
assertEquals("Wrong uid2 blame in bucket 1 for Case " + caseName, blame2B,
bi.getUidStatsLocked(uid2).getCustomMeasuredEnergyMicroJoules(1));
}
}

View File

@@ -386,6 +386,24 @@ public class MeasuredEnergyStatsTest {
assertEquals(60, stats.getAccumulatedCustomBucketEnergy(1));
}
@Test
public void testIsValidCustomBucket() {
final MeasuredEnergyStats stats
= new MeasuredEnergyStats(new boolean[NUMBER_STANDARD_ENERGY_BUCKETS], 3);
assertFalse(stats.isValidCustomBucket(-1));
assertTrue(stats.isValidCustomBucket(0));
assertTrue(stats.isValidCustomBucket(1));
assertTrue(stats.isValidCustomBucket(2));
assertFalse(stats.isValidCustomBucket(3));
assertFalse(stats.isValidCustomBucket(4));
final MeasuredEnergyStats boringStats
= new MeasuredEnergyStats(new boolean[NUMBER_STANDARD_ENERGY_BUCKETS], 0);
assertFalse(boringStats.isValidCustomBucket(-1));
assertFalse(boringStats.isValidCustomBucket(0));
assertFalse(boringStats.isValidCustomBucket(1));
}
@Test
public void testReset() {
final boolean[] supportedStandardBuckets = new boolean[NUMBER_STANDARD_ENERGY_BUCKETS];

View File

@@ -15,8 +15,6 @@
*/
package com.android.server.am;
import static com.android.internal.power.MeasuredEnergyArray.SUBSYSTEM_DISPLAY;
import android.annotation.Nullable;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.BluetoothAdapter;
@@ -39,13 +37,12 @@ import android.telephony.ModemActivityInfo;
import android.telephony.TelephonyManager;
import android.util.IntArray;
import android.util.Slog;
import android.util.SparseIntArray;
import android.util.SparseArray;
import android.util.SparseLongArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BatteryStatsImpl;
import com.android.internal.power.MeasuredEnergyArray;
import com.android.internal.power.MeasuredEnergyStats;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -148,13 +145,13 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
private WifiActivityEnergyInfo mLastWifiInfo =
new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0);
/** Maps the EnergyConsumer id to it's corresponding {@link MeasuredEnergySubsystem} */
/**
* Maps an {@link EnergyConsumerType} to it's corresponding {@link EnergyConsumer#id}s,
* unless it is of {@link EnergyConsumer#type}=={@link EnergyConsumerType#OTHER}
*/
// TODO: Hook this up (it isn't used yet)
@GuardedBy("mWorkerLock")
private @Nullable SparseIntArray mEnergyConsumerToSubsystemMap = null;
/** Maps a {@link MeasuredEnergySubsystem} to it's corresponding EnergyConsumer id */
@GuardedBy("mWorkerLock")
private @Nullable SparseIntArray mSubsystemToEnergyConsumerMap = null;
private @Nullable SparseArray<int[]> mEnergyConsumerTypeToIdMap = null;
/** Snapshot of measured energies, or null if no measured energies are supported. */
@GuardedBy("mWorkerLock")
@@ -204,18 +201,26 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
mWifiManager = wm;
mTelephony = tm;
mPowerStatsInternal = psi;
boolean[] supportedStdBuckets = null;
int numCustomBuckets = 0;
if (mPowerStatsInternal != null) {
populateEnergyConsumerSubsystemMapsLocked();
final MeasuredEnergyArray initialMeasuredEnergies = getEnergyConsumptionData();
mMeasuredEnergySnapshot = initialMeasuredEnergies == null
? null : new MeasuredEnergySnapshot(initialMeasuredEnergies);
final boolean[] supportedStdBuckets
= getSupportedEnergyBuckets(initialMeasuredEnergies);
final int numCustomBuckets = 0; // TODO: Get this from initialMeasuredEnergies
synchronized (mStats) {
mStats.initMeasuredEnergyStatsLocked(supportedStdBuckets, numCustomBuckets);
final SparseArray<EnergyConsumer> idToConsumer
= populateEnergyConsumerSubsystemMapsLocked();
if (idToConsumer != null) {
mMeasuredEnergySnapshot = new MeasuredEnergySnapshot(idToConsumer);
final EnergyConsumerResult[] initialEcrs = getEnergyConsumptionData();
// According to spec, initialEcrs will include 0s for consumers that haven't
// used any energy yet, as long as they are supported; however, attributed uid
// energies will be absent if their energy is 0.
mMeasuredEnergySnapshot.updateAndGetDelta(initialEcrs);
numCustomBuckets = mMeasuredEnergySnapshot.getNumOtherOrdinals();
supportedStdBuckets = getSupportedEnergyBuckets(idToConsumer);
}
}
synchronized (mStats) {
mStats.initMeasuredEnergyStatsLocked(supportedStdBuckets, numCustomBuckets);
}
}
}
@@ -568,7 +573,9 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
} catch (ExecutionException e) {
Slog.w(TAG, "exception reading modem stats: " + e.getCause());
}
final SparseLongArray energyDeltas = mMeasuredEnergySnapshot == null ? null :
final MeasuredEnergySnapshot.MeasuredEnergyDeltaData measuredEnergyDeltas =
mMeasuredEnergySnapshot == null ? null :
mMeasuredEnergySnapshot.updateAndGetDelta(getMeasuredEnergyLocked(updateFlags));
final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -576,6 +583,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
final long elapsedRealtimeUs = elapsedRealtime * 1000;
final long uptimeUs = uptime * 1000;
// Now that we have finally received all the data, we can tell mStats about it.
synchronized (mStats) {
mStats.addHistoryEventLocked(
elapsedRealtime,
@@ -601,10 +609,21 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
}
// Inform mStats about each applicable measured energy.
if (energyDeltas != null) {
final long displayEnergy = energyDeltas.get(SUBSYSTEM_DISPLAY, 0L);
// Always pass in what BatteryExternalStatsWorker thinks screenState is.
mStats.updateDisplayEnergyLocked(displayEnergy, screenState, elapsedRealtime);
if (measuredEnergyDeltas != null) {
final long displayEnergy = measuredEnergyDeltas.displayEnergyUJ;
if (displayEnergy != MeasuredEnergySnapshot.UNAVAILABLE) {
// If updating, pass in what BatteryExternalStatsWorker thinks screenState is.
mStats.updateDisplayEnergyLocked(displayEnergy, screenState, elapsedRealtime);
}
}
// Inform mStats about each applicable custom energy bucket.
if (measuredEnergyDeltas != null && measuredEnergyDeltas.otherTotalEnergyUJ != null) {
// Iterate over the custom (EnergyConsumerType.OTHER) ordinals.
for (int ord = 0; ord < measuredEnergyDeltas.otherTotalEnergyUJ.length; ord++) {
long totalEnergy = measuredEnergyDeltas.otherTotalEnergyUJ[ord];
SparseLongArray uidEnergies = measuredEnergyDeltas.otherUidEnergiesUJ[ord];
mStats.updateCustomMeasuredEnergyDataLocked(ord, totalEnergy, uidEnergies);
}
}
if (bluetoothInfo != null) {
@@ -621,7 +640,8 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
if (wifiInfo != null) {
if (wifiInfo.isValid()) {
// TODO: wifiEnergyDelta = energyDeltas.get(MeasuredEnergyArray.SUBSYSTEM_WIFI, 0L);
// TODO: wifiEnergyDelta = measuredEnergyDeltas.consumerTypeEnergyUJ
// .get(EnergyConsumerType.WIFI, MeasuredEnergySnapshot.UNAVAILABLE)
mStats.updateWifiState(extractDeltaLocked(wifiInfo)
/*, TODO: wifiEnergyDelta */, elapsedRealtime, uptime);
} else {
@@ -740,21 +760,23 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
}
/**
* Map the {@link MeasuredEnergyArray.MeasuredEnergySubsystem}s in the given energyArray to
* Map the {@link EnergyConsumerType}s in the given energyArray to
* their corresponding {@link MeasuredEnergyStats.StandardEnergyBucket}s.
* Does not include custom energy buckets (which are always, by definition, supported).
*
* @return array with true for index i if standard energy bucket i is supported.
*/
private static @Nullable boolean[] getSupportedEnergyBuckets(MeasuredEnergyArray energyArray) {
if (energyArray == null) {
private static @Nullable boolean[] getSupportedEnergyBuckets(
SparseArray<EnergyConsumer> idToConsumer) {
if (idToConsumer == null) {
return null;
}
final boolean[] buckets = new boolean[MeasuredEnergyStats.NUMBER_STANDARD_ENERGY_BUCKETS];
final int size = energyArray.size();
for (int energyIdx = 0; energyIdx < size; energyIdx++) {
switch (energyArray.getSubsystem(energyIdx)) {
case MeasuredEnergyArray.SUBSYSTEM_DISPLAY:
final int size = idToConsumer.size();
for (int idx = 0; idx < size; idx++) {
final EnergyConsumer consumer = idToConsumer.valueAt(idx);
switch (consumer.type) {
case EnergyConsumerType.DISPLAY:
buckets[MeasuredEnergyStats.ENERGY_BUCKET_SCREEN_ON] = true;
buckets[MeasuredEnergyStats.ENERGY_BUCKET_SCREEN_DOZE] = true;
buckets[MeasuredEnergyStats.ENERGY_BUCKET_SCREEN_OTHER] = true;
@@ -764,71 +786,22 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
return buckets;
}
/**
* Get a {@link MeasuredEnergyArray} with the latest
* {@link MeasuredEnergyArray.MeasuredEnergySubsystem} energy usage since boot.
*
* TODO(b/176988041): Replace {@link MeasuredEnergyArray} usage with {@link
* EnergyConsumerResult}[]
*/
/** Get {@link EnergyConsumerResult}s with the latest energy usage since boot. */
@GuardedBy("mWorkerLock")
@VisibleForTesting
public @Nullable MeasuredEnergyArray getEnergyConsumptionData() {
final EnergyConsumerResult[] results;
private @Nullable EnergyConsumerResult[] getEnergyConsumptionData() {
try {
results = mPowerStatsInternal.getEnergyConsumedAsync(new int[0])
return mPowerStatsInternal.getEnergyConsumedAsync(new int[0])
.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (Exception e) {
Slog.e(TAG, "Failed to getEnergyConsumedAsync", e);
return null;
}
if (results == null) return null;
final int size = results.length;
final int[] subsystems = new int[size];
final long[] energyUJ = new long[size];
int count = 0;
for (int i = 0; i < size; i++) {
final EnergyConsumerResult consumer = results[i];
final int subsystem = mEnergyConsumerToSubsystemMap.get(consumer.id,
MeasuredEnergyArray.SUBSYSTEM_UNKNOWN);
if (subsystem == MeasuredEnergyArray.SUBSYSTEM_UNKNOWN) continue;
subsystems[count] = subsystem;
energyUJ[count] = consumer.energyUWs;
count++;
}
final int arraySize = count;
return new MeasuredEnergyArray() {
@Override
public int getSubsystem(int index) {
if (index >= size()) {
throw new IllegalArgumentException(
"Out of bounds subsystem index! index : " + index + ", size : "
+ size());
}
return subsystems[index];
}
@Override
public long getEnergy(int index) {
if (index >= size()) {
throw new IllegalArgumentException(
"Out of bounds subsystem index! index : " + index + ", size : "
+ size());
}
return energyUJ[index];
}
@Override
public int size() {
return arraySize;
}
};
}
/** Fetch MeasuredEnergyArray for supported subsystems based on the given updateFlags. */
/** Fetch EnergyConsumerResult[] for supported subsystems based on the given updateFlags. */
@GuardedBy("mWorkerLock")
private @Nullable MeasuredEnergyArray getMeasuredEnergyLocked(@ExternalUpdateFlag int flags) {
private @Nullable EnergyConsumerResult[] getMeasuredEnergyLocked(@ExternalUpdateFlag int flags)
{
if (mMeasuredEnergySnapshot == null || mPowerStatsInternal == null) return null;
if (flags == UPDATE_ALL) {
@@ -836,12 +809,13 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
return getEnergyConsumptionData();
}
final List<Integer> energySubsystems = new ArrayList<>();
final List<Integer> energyConsumerIds = new ArrayList<>();
if ((flags & UPDATE_DISPLAY) != 0) {
addEnergyConsumerIdLocked(energySubsystems, SUBSYSTEM_DISPLAY);
addEnergyConsumerIdLocked(energyConsumerIds, EnergyConsumerType.DISPLAY);
}
// TODO: Wifi, Bluetooth, etc., go here
if (energySubsystems.isEmpty()) {
if (energyConsumerIds.isEmpty()) {
return null;
}
// TODO: Query *specific* subsystems from HAL based on energyConsumerIds.toArray()
@@ -849,59 +823,48 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
}
@GuardedBy("mWorkerLock")
private void addEnergyConsumerIdLocked(List<Integer> energyConsumerIds,
@MeasuredEnergyArray.MeasuredEnergySubsystem int consumerId) {
if (mMeasuredEnergySnapshot.hasSubsystem(consumerId)) {
energyConsumerIds.add(consumerId);
}
private void addEnergyConsumerIdLocked(
List<Integer> energyConsumerIds, @EnergyConsumerType int type) {
final int consumerId = 0; // TODO: Use mEnergyConsumerTypeToIdMap to get this
energyConsumerIds.add(consumerId);
}
/** Populates the cached type->ids map, and returns the (inverse) id->EnergyConsumer map. */
@GuardedBy("mWorkerLock")
private void populateEnergyConsumerSubsystemMapsLocked() {
private @Nullable SparseArray<EnergyConsumer> populateEnergyConsumerSubsystemMapsLocked() {
if (mPowerStatsInternal == null) {
// PowerStatsInternal unavailable, don't bother populating maps.
mEnergyConsumerToSubsystemMap = null;
mSubsystemToEnergyConsumerMap = null;
return;
return null;
}
final EnergyConsumer[] energyConsumers = mPowerStatsInternal.getEnergyConsumerInfo();
if (energyConsumers == null) {
// EnergyConsumer data unavailable, don't bother populating maps.
mEnergyConsumerToSubsystemMap = null;
mSubsystemToEnergyConsumerMap = null;
return;
if (energyConsumers == null || energyConsumers.length == 0) {
return null;
}
final int length = energyConsumers.length;
if (length == 0) {
// EnergyConsumer array empty, don't bother populating maps.
mEnergyConsumerToSubsystemMap = null;
mSubsystemToEnergyConsumerMap = null;
return;
} else {
mEnergyConsumerToSubsystemMap = new SparseIntArray(length);
mSubsystemToEnergyConsumerMap = new SparseIntArray(length);
}
// TODO: Initialize typeToIds
// Maps type -> {ids} (1:n map, since multiple ids might have the same type)
// final SparseArray<SparseIntArray> typeToIds = new SparseArray<>();
// Maps id -> EnergyConsumer (1:1 map)
final SparseArray<EnergyConsumer> idToConsumer = new SparseArray<>(energyConsumers.length);
// Add all expected EnergyConsumers to the maps
for (int i = 0; i < length; i++) {
final EnergyConsumer consumer = energyConsumers[i];
switch (consumer.type) {
case EnergyConsumerType.DISPLAY:
if (consumer.ordinal == 0) {
mEnergyConsumerToSubsystemMap.put(consumer.id,
MeasuredEnergyArray.SUBSYSTEM_DISPLAY);
mSubsystemToEnergyConsumerMap.put(MeasuredEnergyArray.SUBSYSTEM_DISPLAY,
consumer.id);
} else {
Slog.w(TAG, "Unexpected ordinal (" + consumer.ordinal
+ ") for EnergyConsumerType.DISPLAY");
}
break;
default:
Slog.w(TAG, "Unexpected EnergyConsumerType (" + consumer.type + ")");
for (final EnergyConsumer consumer : energyConsumers) {
// Check for inappropriate ordinals
if (consumer.ordinal != 0) {
switch (consumer.type) {
case EnergyConsumerType.OTHER:
case EnergyConsumerType.CPU_CLUSTER:
break;
default:
Slog.w(TAG, "EnergyConsumer '" + consumer.name + "' has unexpected ordinal "
+ consumer.ordinal + " for type " + consumer.type);
continue; // Ignore this consumer
}
}
idToConsumer.put(consumer.id, consumer);
// TODO: Also populate typeToIds map
}
// TODO: Store typeToIds in mEnergyConsumerTypeToIdMap.
return idToConsumer;
}
}

View File

@@ -17,132 +17,260 @@
package com.android.server.am;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.power.stats.EnergyConsumer;
import android.hardware.power.stats.EnergyConsumerAttribution;
import android.hardware.power.stats.EnergyConsumerResult;
import android.hardware.power.stats.EnergyConsumerType;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseLongArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.power.MeasuredEnergyArray;
import com.android.internal.power.MeasuredEnergyArray.MeasuredEnergySubsystem;
import java.io.PrintWriter;
import java.util.Arrays;
/**
* Keeps snapshots of data from previously pulled MeasuredEnergyArrays.
* Keeps snapshots of data from previously pulled EnergyConsumerResults.
*/
@VisibleForTesting
public class MeasuredEnergySnapshot {
private static final String TAG = "MeasuredEnergySnapshot";
private static final long UNAVAILABLE = -1;
public static final long UNAVAILABLE = -1L;
/** Map of {@link EnergyConsumer#id} to its corresponding {@link EnergyConsumer}. */
private final SparseArray<EnergyConsumer> mEnergyConsumers;
/** Number of ordinals for {@link EnergyConsumerType#OTHER}. */
private final int mNumOtherOrdinals;
/**
* Energy snapshots from the last time each {@link MeasuredEnergySubsystem} was updated.
* Energy snapshots, mapping {@link EnergyConsumer#id} to energy (UJ) from the last time
* each {@link EnergyConsumer} was updated.
*
* Note that the snapshots for different subsystems may have been taken at different times.
* Note that the snapshots for different ids may have been taken at different times.
* Note that energies for all existing ids are stored here, including each ordinal of type
* {@link EnergyConsumerType#OTHER} (tracking their total energy usage).
*
* A snapshot is {@link #UNAVAILABLE} if the subsystem has never been updated (ie. unsupported).
* If an id is not present yet, it is treated as uninitialized (energy {@link #UNAVAILABLE}).
*/
private final long[] mMeasuredEnergySnapshots;
private final SparseLongArray mMeasuredEnergySnapshots;
/**
* Constructor that initializes to the given energyArray;
* all subsystems not mentioned in initialEnergyArray are set to UNAVAILABLE.
* Energy snapshots <b>per uid</b> from the last time each {@link EnergyConsumer} of type
* {@link EnergyConsumerType#OTHER} was updated.
* It maps each OTHER {@link EnergyConsumer#id} to a SparseLongArray, which itself maps each
* uid to an energy (UJ). That is,
* mAttributionSnapshots.get(consumerId).get(uid) = energy used by uid for this consumer.
*
* If an id is not present yet, it is treated as uninitialized (i.e. each uid is unavailable).
* If an id is present but a uid is not present, that uid's energy is 0.
*/
public MeasuredEnergySnapshot(MeasuredEnergyArray initialEnergyArray) {
this(MeasuredEnergyArray.NUMBER_SUBSYSTEMS, initialEnergyArray);
private final SparseArray<SparseLongArray> mAttributionSnapshots;
/**
* Constructor that initializes to the given id->EnergyConsumer map, indicating which consumers
* exist and what their details are.
*/
MeasuredEnergySnapshot(@NonNull SparseArray<EnergyConsumer> idToConsumerMap) {
mEnergyConsumers = idToConsumerMap;
mMeasuredEnergySnapshots = new SparseLongArray(mEnergyConsumers.size());
mNumOtherOrdinals = calculateNumOtherOrdinals(idToConsumerMap);
mAttributionSnapshots = new SparseArray<>(mNumOtherOrdinals);
}
/**
* Constructor (for testing) that initializes to the given energyArray and numSubsystems;
* all subsystems not mentioned in initialEnergyArray are set to UNAVAILABLE.
* Returns the number of ordinals for {@link EnergyConsumerType#OTHER}, i.e. the number of
* custom energy buckets supported by the device.
*/
@VisibleForTesting
MeasuredEnergySnapshot(int numSubsystems, MeasuredEnergyArray initialEnergyArray) {
if (initialEnergyArray.size() > numSubsystems) {
throw new IllegalArgumentException("Energy array contains " + initialEnergyArray.size()
+ " subsystems, which exceeds the maximum allowed of " + numSubsystems);
}
mMeasuredEnergySnapshots = new long[numSubsystems];
Arrays.fill(mMeasuredEnergySnapshots, UNAVAILABLE);
fillGivenSubsystems(initialEnergyArray);
public int getNumOtherOrdinals() {
return mNumOtherOrdinals;
}
/**
* For the subsystems present in energyArray, overwrites mMeasuredEnergySnapshots with their
* energy values from energyArray.
*/
private void fillGivenSubsystems(MeasuredEnergyArray energyArray) {
final int size = energyArray.size();
for (int i = 0; i < size; i++) {
final int subsystem = energyArray.getSubsystem(i);
mMeasuredEnergySnapshots[subsystem] = energyArray.getEnergy(i);
}
/** Class for returning measured energy delta data. */
static class MeasuredEnergyDeltaData {
/** The energyUJ for {@link EnergyConsumerType#DISPLAY}. */
public long displayEnergyUJ = UNAVAILABLE;
/** Map of {@link EnergyConsumerType#OTHER} ordinals to their total energyUJ. */
public @Nullable long[] otherTotalEnergyUJ = null;
/** Map of {@link EnergyConsumerType#OTHER} ordinals to their {uid->energyUJ} maps. */
public @Nullable SparseLongArray[] otherUidEnergiesUJ = null;
}
/**
* Update with the some freshly measured energies and return the difference (delta)
* between the previously stored values and the passed-in values.
*
* @param energyArray measured energy array for some (possibly not all) subsystems.
* @param ecrs EnergyConsumerResults for some (possibly not all) {@link EnergyConsumer}s.
* Consumers that are not present are ignored (they are *not* treated as 0).
*
* @return a map from the updated subsystems to their corresponding energy deltas.
* Subsystems not present in energyArray will not appear.
* Subsystems with no difference in energy will not appear.
* Returns null, if energyArray is null.
* @return a MeasuredEnergyDeltaData, containing maps from the updated consumers to
* their corresponding energy deltas.
* Fields with no interesting data (consumers not present in ecrs or with no energy
* difference) will generally be left as their default values.
* otherTotalEnergyUJ and otherUidEnergiesUJ are always either both null or both of
* length {@link #getNumOtherOrdinals()}.
* Returns null, if ecrs is null or empty.
*/
public @Nullable SparseLongArray updateAndGetDelta(MeasuredEnergyArray energyArray) {
if (energyArray == null) {
public @Nullable MeasuredEnergyDeltaData updateAndGetDelta(EnergyConsumerResult[] ecrs) {
if (ecrs == null || ecrs.length == 0) {
return null;
}
final SparseLongArray delta = new SparseLongArray();
final int size = energyArray.size();
for (int i = 0; i < size; i++) {
final int updatedSubsystem = energyArray.getSubsystem(i);
final long newEnergyUJ = energyArray.getEnergy(i);
final long oldEnergyUJ = mMeasuredEnergySnapshots[updatedSubsystem];
final MeasuredEnergyDeltaData output = new MeasuredEnergyDeltaData();
// If this is the first valid energy, there is no delta to take.
if (oldEnergyUJ < 0) continue;
final long deltaUJ = newEnergyUJ - oldEnergyUJ;
if (deltaUJ == 0) continue;
if (deltaUJ < 0) {
Slog.e(TAG, "For subsystem " + updatedSubsystem + ", new energy (" + newEnergyUJ
+ ") is less than old energy (" + oldEnergyUJ + "). Skipping. ");
for (final EnergyConsumerResult ecr : ecrs) {
// Extract the new energy data for the current consumer.
final int consumerId = ecr.id;
final long newEnergyUJ = ecr.energyUWs;
final EnergyConsumerAttribution[] newAttributions = ecr.attribution;
// Look up the static information about this consumer.
final EnergyConsumer consumer = mEnergyConsumers.get(consumerId, null);
if (consumer == null) {
Slog.e(TAG, "updateAndGetDelta given invalid consumerId " + consumerId);
continue;
}
delta.put(updatedSubsystem, deltaUJ);
final int type = consumer.type;
final int ordinal = consumer.ordinal;
// Look up, and update, the old energy information about this consumer.
final long oldEnergyUJ = mMeasuredEnergySnapshots.get(consumerId, UNAVAILABLE);
mMeasuredEnergySnapshots.put(consumerId, newEnergyUJ);
final SparseLongArray otherUidEnergies
= updateAndGetDeltaForTypeOther(consumer, newAttributions);
// Everything is fully done being updated. We now calculate the delta for returning.
// NB: Since sum(attribution.energyUWs)<=energyUWs we assume that if deltaEnergy==0
// there's no attribution either. Technically that isn't enforced at the HAL, but we
// can't really trust data like that anyway.
if (oldEnergyUJ < 0) continue; // Generally happens only on initialization.
if (newEnergyUJ == oldEnergyUJ) continue;
final long deltaUJ = newEnergyUJ - oldEnergyUJ;
if (deltaUJ < 0) {
Slog.e(TAG, "EnergyConsumer " + consumer.name + ": new energy (" + newEnergyUJ
+ ") < old energy (" + oldEnergyUJ + "). Skipping. ");
continue;
}
switch (type) {
case EnergyConsumerType.DISPLAY:
output.displayEnergyUJ = deltaUJ;
break;
case EnergyConsumerType.OTHER:
if (output.otherTotalEnergyUJ == null) {
output.otherTotalEnergyUJ = new long[getNumOtherOrdinals()];
output.otherUidEnergiesUJ = new SparseLongArray[getNumOtherOrdinals()];
}
output.otherTotalEnergyUJ[ordinal] = deltaUJ;
output.otherUidEnergiesUJ[ordinal] = otherUidEnergies;
break;
default:
Slog.w(TAG, "Ignoring consumer " + consumer.name + " of unknown type " + type);
}
}
fillGivenSubsystems(energyArray);
return delta;
return output;
}
/**
* Check if a subsystem's measured energy is available.
* @param subsystem which subsystem.
* @return true if subsystem is available.
* For a consumer of type {@link EnergyConsumerType#OTHER}, updates
* {@link #mAttributionSnapshots} with freshly measured energies (per uid) and returns the
* difference (delta) between the previously stored values and the passed-in values.
*
* @param consumerInfo a consumer of type {@link EnergyConsumerType#OTHER}.
* @param newAttributions Record of uids and their new energyUJ values.
* Any uid not present is treated as having energy 0.
* If null or empty, all uids are treated as having energy 0.
* @return A map (in the sense of {@link MeasuredEnergyDeltaData#otherUidEnergiesUJ} for this
* consumer) of uid -> energyDelta, with all uids that have a non-zero energyDelta.
* Returns null if no delta available to calculate.
*/
public boolean hasSubsystem(@MeasuredEnergySubsystem int subsystem) {
return mMeasuredEnergySnapshots[subsystem] != UNAVAILABLE;
private @Nullable SparseLongArray updateAndGetDeltaForTypeOther(
@NonNull EnergyConsumer consumerInfo,
@Nullable EnergyConsumerAttribution[] newAttributions) {
if (consumerInfo.type != EnergyConsumerType.OTHER) {
return null;
}
if (newAttributions == null) {
// Treat null as empty (i.e. all uids have 0 energy).
newAttributions = new EnergyConsumerAttribution[0];
}
// SparseLongArray mapping uid -> energyUJ (for this particular consumerId)
SparseLongArray uidOldEnergyMap = mAttributionSnapshots.get(consumerInfo.id, null);
// If uidOldEnergyMap wasn't present, each uid was UNAVAILABLE, so update data and return.
if (uidOldEnergyMap == null) {
uidOldEnergyMap = new SparseLongArray(newAttributions.length);
mAttributionSnapshots.put(consumerInfo.id, uidOldEnergyMap);
for (EnergyConsumerAttribution newAttribution : newAttributions) {
uidOldEnergyMap.put(newAttribution.uid, newAttribution.energyUWs);
}
return null;
}
// Map uid -> energyDelta. No initial capacity since many deltas might be 0.
final SparseLongArray uidEnergyDeltas = new SparseLongArray();
for (EnergyConsumerAttribution newAttribution : newAttributions) {
final int uid = newAttribution.uid;
final long newEnergyUJ = newAttribution.energyUWs;
// uidOldEnergyMap was present. So any particular uid that wasn't present, had 0 energy.
final long oldEnergyUJ = uidOldEnergyMap.get(uid, 0L);
uidOldEnergyMap.put(uid, newEnergyUJ);
// Everything is fully done being updated. We now calculate the delta for returning.
if (oldEnergyUJ < 0) continue;
if (newEnergyUJ == oldEnergyUJ) continue;
final long deltaUJ = newEnergyUJ - oldEnergyUJ;
if (deltaUJ < 0) {
Slog.e(TAG, "EnergyConsumer " + consumerInfo.name + ": new energy (" + newEnergyUJ
+ ") but old energy (" + oldEnergyUJ + "). Skipping. ");
continue;
}
uidEnergyDeltas.put(uid, deltaUJ);
}
return uidEnergyDeltas;
}
/** Dump debug data. */
public void dump(PrintWriter pw) {
pw.println("Measured energy snapshot (microjoules):");
pw.print(" ");
for (int i = 0; i < MeasuredEnergyArray.NUMBER_SUBSYSTEMS; i++) {
final long energyUJ = mMeasuredEnergySnapshots[i];
if (energyUJ == UNAVAILABLE) continue;
pw.print(MeasuredEnergyArray.SUBSYSTEM_NAMES[i]);
pw.print(" : ");
pw.print(energyUJ);
if (i != MeasuredEnergyArray.NUMBER_SUBSYSTEMS - 1) {
pw.print(", ");
}
pw.println("Measured energy snapshot");
pw.println("List of EnergyConsumers:");
for (int i = 0; i < mEnergyConsumers.size(); i++) {
final int id = mEnergyConsumers.keyAt(i);
final EnergyConsumer consumer = mEnergyConsumers.valueAt(i);
pw.println(String.format(" Consumer %d is {id=%d, ordinal=%d, type=%d, name=%s}", id,
consumer.id, consumer.ordinal, consumer.type, consumer.name));
}
pw.println("Map of consumerIds to energy (in microjoules):");
for (int i = 0; i < mMeasuredEnergySnapshots.size(); i++) {
final int id = mMeasuredEnergySnapshots.keyAt(i);
final long energyUJ = mMeasuredEnergySnapshots.valueAt(i);
pw.println(String.format(" Consumer %d has energy %d uJ}", id, energyUJ));
}
pw.println("List of the " + mNumOtherOrdinals + " OTHER EnergyConsumers:");
pw.println(" " + mAttributionSnapshots);
pw.println();
}
/** Determines the number of ordinals for {@link EnergyConsumerType#OTHER}. */
private static int calculateNumOtherOrdinals(SparseArray<EnergyConsumer> idToConsumer) {
if (idToConsumer == null) return 0;
int numOtherOrdinals = 0;
final int size = idToConsumer.size();
for (int idx = 0; idx < size; idx++) {
final EnergyConsumer consumer = idToConsumer.valueAt(idx);
if (consumer.type == EnergyConsumerType.OTHER) numOtherOrdinals++;
}
return numOtherOrdinals;
}
}

View File

@@ -16,8 +16,6 @@
package com.android.server.am;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Context;
@@ -30,15 +28,12 @@ import android.hardware.power.stats.PowerEntity;
import android.hardware.power.stats.StateResidencyResult;
import android.power.PowerStatsInternal;
import android.util.SparseArray;
import android.util.SparseLongArray;
import androidx.test.InstrumentationRegistry;
import com.android.internal.os.BatteryStatsImpl;
import com.android.internal.power.MeasuredEnergyArray;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
@@ -63,44 +58,6 @@ public class BatteryExternalStatsWorkerTest {
mBatteryStatsImpl);
}
@Test
public void getEnergyConsumptionData() {
SparseLongArray expectSubsystems = new SparseLongArray();
// Add some energy consumers used by BatteryExternalStatsWorker.
final int displayId = mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.DISPLAY, 0,
"display");
mPowerStatsInternal.incrementEnergyConsumption(displayId, 12345);
expectSubsystems.put(MeasuredEnergyArray.SUBSYSTEM_DISPLAY, 12345);
// Add an arbitrary energy consumer unused by BatteryExternalStatsWorker.
// Must be changed if '154' ever becomes an EnergyConsumerType used by BESW.
final int someId = mPowerStatsInternal.addEnergyConsumer((byte) 154, 0, "some_consumer");
mPowerStatsInternal.incrementEnergyConsumption(someId, 34567);
// Inform BESW that PowerStatsInternal is ready to query
mBatteryExternalStatsWorker.systemServicesReady();
MeasuredEnergyArray energies = mBatteryExternalStatsWorker.getEnergyConsumptionData();
assertEquals(expectSubsystems.size(), energies.size());
final int size = expectSubsystems.size();
for (int i = 0; i < size; i++) {
int subsystem = expectSubsystems.keyAt(i);
// find the subsystem in the returned MeasuredEnergyArray
int subsystemIndex = -1;
for (int j = 0; j < size; j++) {
if (subsystem == energies.getSubsystem(i)) {
subsystemIndex = i;
break;
}
}
assertNotEquals("Subsystem " + subsystem + " not found in MeasuredEnergyArray", -1,
subsystemIndex);
assertEquals(expectSubsystems.valueAt(i), energies.getEnergy(subsystemIndex));
}
}
public class TestInjector extends BatteryExternalStatsWorker.Injector {
public TestInjector(Context context) {
super(context);

View File

@@ -16,18 +16,23 @@
package com.android.server.am;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static com.android.server.am.MeasuredEnergySnapshot.UNAVAILABLE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.hardware.power.stats.EnergyConsumer;
import android.hardware.power.stats.EnergyConsumerAttribution;
import android.hardware.power.stats.EnergyConsumerResult;
import android.hardware.power.stats.EnergyConsumerType;
import android.util.SparseArray;
import android.util.SparseLongArray;
import androidx.test.filters.SmallTest;
import com.android.internal.power.MeasuredEnergyArray;
import com.android.server.am.MeasuredEnergySnapshot.MeasuredEnergyDeltaData;
import org.junit.Before;
import org.junit.Test;
/**
@@ -38,134 +43,198 @@ import org.junit.Test;
*/
@SmallTest
public final class MeasuredEnergySnapshotTest {
private static final int NUMBER_SUBSYSTEMS = 3;
private static final int SUBSYSTEM_DISPLAY = 0;
private static final int SUBSYSTEM_NEVER_USED = 1;
private static final int SUBSYSTEM_CATAPULT = 2;
private static final EnergyConsumer CONSUMER_DISPLAY = createEnergyConsumer(
0, 0, EnergyConsumerType.DISPLAY, "Display");
private static final EnergyConsumer CONSUMER_OTHER_0 = createEnergyConsumer(
47, 0, EnergyConsumerType.OTHER, "GPU");
private static final EnergyConsumer CONSUMER_OTHER_1 = createEnergyConsumer(
1, 1, EnergyConsumerType.OTHER, "HPU");
private static final EnergyConsumer CONSUMER_OTHER_2 = createEnergyConsumer(
436, 2, EnergyConsumerType.OTHER, "IPU");
private MeasuredEnergySnapshot mSnapshot;
private static final SparseArray<EnergyConsumer> ALL_ID_CONSUMER_MAP = createIdToConsumerMap(
CONSUMER_DISPLAY, CONSUMER_OTHER_0, CONSUMER_OTHER_1, CONSUMER_OTHER_2);
private static final SparseArray<EnergyConsumer> SOME_ID_CONSUMER_MAP = createIdToConsumerMap(
CONSUMER_DISPLAY);
// Basic MeasuredEnergyArray that supports all the subsystems. Out of order on purpose.
private final int[] mAllSubsystems =
{SUBSYSTEM_DISPLAY, SUBSYSTEM_CATAPULT, SUBSYSTEM_NEVER_USED};
// E.g. mAllSubsystems[mSubsystemIndices[SUBSYSTEM_CATAPULT]]=SUBSYSTEM_CATAPULT
private final int[] mSubsystemIndices = {0, 2, 1};
private final long[] mCurrentSubsystemEnergyUJ = {111, 0, 0};
private final MeasuredEnergyArray mOmniEnergyArray = new MeasuredEnergyArray() {
@Override
public int getSubsystem(int index) {
return mAllSubsystems[index];
}
@Override
public long getEnergy(int index) {
return mCurrentSubsystemEnergyUJ[index];
}
@Override
public int size() {
return mAllSubsystems.length;
}
// Elements in each results are purposefully out of order.
private static final EnergyConsumerResult[] RESULTS_0 = new EnergyConsumerResult[] {
createEnergyConsumerResult(CONSUMER_OTHER_0.id, 90, new int[] {47, 3}, new long[] {14, 13}),
createEnergyConsumerResult(CONSUMER_DISPLAY.id, 14, null, null),
createEnergyConsumerResult(CONSUMER_OTHER_1.id, 0, null, null),
// No CONSUMER_OTHER_2
};
private final MeasuredEnergyArray mJustDisplayEnergyArray = new MeasuredEnergyArray() {
@Override
public int getSubsystem(int index) {
return mAllSubsystems[0];
}
@Override
public long getEnergy(int index) {
return mCurrentSubsystemEnergyUJ[0];
}
@Override
public int size() {
return 1;
}
private static final EnergyConsumerResult[] RESULTS_1 = new EnergyConsumerResult[] {
createEnergyConsumerResult(CONSUMER_DISPLAY.id, 24, null, null),
createEnergyConsumerResult(CONSUMER_OTHER_0.id, 90, new int[] {47, 3}, new long[] {14, 13}),
createEnergyConsumerResult(CONSUMER_OTHER_2.id, 12, new int[] {6}, new long[] {10}),
createEnergyConsumerResult(CONSUMER_OTHER_1.id, 12_000, null, null),
};
private static final EnergyConsumerResult[] RESULTS_2 = new EnergyConsumerResult[] {
createEnergyConsumerResult(CONSUMER_DISPLAY.id, 36, null, null),
// No CONSUMER_OTHER_0
// No CONSUMER_OTHER_1
// No CONSUMER_OTHER_2
};
private static final EnergyConsumerResult[] RESULTS_3 = new EnergyConsumerResult[] {
// No CONSUMER_DISPLAY
createEnergyConsumerResult(CONSUMER_OTHER_2.id, 13, new int[] {6}, new long[] {10}),
createEnergyConsumerResult(
CONSUMER_OTHER_0.id, 190, new int[] {2, 3, 47, 7}, new long[] {9, 18, 14, 6}),
createEnergyConsumerResult(CONSUMER_OTHER_1.id, 12_000, null, null),
};
private static final EnergyConsumerResult[] RESULTS_4 = new EnergyConsumerResult[] {
createEnergyConsumerResult(CONSUMER_DISPLAY.id, 43, null, null),
createEnergyConsumerResult(
CONSUMER_OTHER_0.id, 290, new int[] {7, 47, 3, 2}, new long[] {6, 14, 18, 11}),
// No CONSUMER_OTHER_1
createEnergyConsumerResult(CONSUMER_OTHER_2.id, 165, new int[] {6, 47}, new long[] {10, 8}),
};
@Before
public void setUp() {
mSnapshot = new MeasuredEnergySnapshot(NUMBER_SUBSYSTEMS, mOmniEnergyArray);
@Test
public void testUpdateAndGetDelta_empty() {
final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(ALL_ID_CONSUMER_MAP);
assertNull(snapshot.updateAndGetDelta(null));
assertNull(snapshot.updateAndGetDelta(new EnergyConsumerResult[0]));
}
@Test
public void testUpdateAndGetDelta() {
SparseLongArray result;
final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(ALL_ID_CONSUMER_MAP);
// Increment DISPLAY by 15
incrementEnergyOfSubsystem(SUBSYSTEM_DISPLAY, 15);
result = mSnapshot.updateAndGetDelta(mOmniEnergyArray);
assertEquals(1, result.size());
assertEquals(15, result.get(SUBSYSTEM_DISPLAY));
// results0
MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0);
if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
assertEquals(UNAVAILABLE, delta.displayEnergyUJ);
assertNull(delta.otherTotalEnergyUJ);
assertNull(delta.otherUidEnergiesUJ);
}
// Increment DISPLAY by 7
// Increment CATAPULT by 5. But do NOT include (pull) it in the passed in energy array.
incrementEnergyOfSubsystem(SUBSYSTEM_DISPLAY, 7);
incrementEnergyOfSubsystem(SUBSYSTEM_CATAPULT, 5);
result = mSnapshot.updateAndGetDelta(mJustDisplayEnergyArray); // Just pull display.
assertEquals(1, result.size());
assertEquals(7, result.get(SUBSYSTEM_DISPLAY));
// results1
delta = snapshot.updateAndGetDelta(RESULTS_1);
assertNotNull(delta);
assertEquals(24 - 14, delta.displayEnergyUJ);
// Increment CATAPULT by 64 (in addition to the previous increase of 5)
incrementEnergyOfSubsystem(SUBSYSTEM_CATAPULT, 64);
result = mSnapshot.updateAndGetDelta(mOmniEnergyArray);
assertEquals(1, result.size());
assertEquals(5 + 64, result.get(SUBSYSTEM_CATAPULT));
assertNotNull(delta.otherTotalEnergyUJ);
assertEquals(90 - 90, delta.otherTotalEnergyUJ[0]);
assertEquals(12_000 - 0, delta.otherTotalEnergyUJ[1]);
assertEquals(0, delta.otherTotalEnergyUJ[2]); // First good pull. Treat delta as 0.
// Do nothing
result = mSnapshot.updateAndGetDelta(mOmniEnergyArray);
assertEquals("0 results should not appear at all", 0, result.size());
assertNotNull(delta.otherUidEnergiesUJ);
assertNullOrEmpty(delta.otherUidEnergiesUJ[0]); // No change in uid energies
assertNullOrEmpty(delta.otherUidEnergiesUJ[1]);
assertNullOrEmpty(delta.otherUidEnergiesUJ[2]);
// Increment DISPLAY by 42
incrementEnergyOfSubsystem(SUBSYSTEM_DISPLAY, 42);
result = mSnapshot.updateAndGetDelta(mOmniEnergyArray);
assertEquals(1, result.size());
assertEquals(42, result.get(SUBSYSTEM_DISPLAY));
// results2
delta = snapshot.updateAndGetDelta(RESULTS_2);
assertNotNull(delta);
assertEquals(36 - 24, delta.displayEnergyUJ);
assertNull(delta.otherUidEnergiesUJ);
assertNull(delta.otherTotalEnergyUJ);
// Increment DISPLAY by 106 and CATAPULT by 13
incrementEnergyOfSubsystem(SUBSYSTEM_DISPLAY, 106);
incrementEnergyOfSubsystem(SUBSYSTEM_CATAPULT, 13);
result = mSnapshot.updateAndGetDelta(mOmniEnergyArray);
assertEquals(2, result.size());
assertEquals(106, result.get(SUBSYSTEM_DISPLAY));
assertEquals(13, result.get(SUBSYSTEM_CATAPULT));
// results3
delta = snapshot.updateAndGetDelta(RESULTS_3);
assertNotNull(delta);
assertEquals(UNAVAILABLE, delta.displayEnergyUJ);
assertNotNull(delta.otherTotalEnergyUJ);
assertEquals(190 - 90, delta.otherTotalEnergyUJ[0]);
assertEquals(12_000 - 12_000, delta.otherTotalEnergyUJ[1]);
assertEquals(13 - 12, delta.otherTotalEnergyUJ[2]);
assertNotNull(delta.otherUidEnergiesUJ);
assertEquals(3, delta.otherUidEnergiesUJ[0].size());
assertEquals(9 - 0, delta.otherUidEnergiesUJ[0].get(2));
assertEquals(18 - 13, delta.otherUidEnergiesUJ[0].get(3));
assertEquals(6 - 0, delta.otherUidEnergiesUJ[0].get(7));
assertNullOrEmpty(delta.otherUidEnergiesUJ[1]);
assertNullOrEmpty(delta.otherUidEnergiesUJ[2]);
// results4
delta = snapshot.updateAndGetDelta(RESULTS_4);
assertNotNull(delta);
assertEquals(43 - 36, delta.displayEnergyUJ);
assertNotNull(delta.otherTotalEnergyUJ);
assertEquals(290 - 190, delta.otherTotalEnergyUJ[0]);
assertEquals(0, delta.otherTotalEnergyUJ[1]); // Not present (e.g. missing data)
assertEquals(165 - 13, delta.otherTotalEnergyUJ[2]);
assertNotNull(delta.otherUidEnergiesUJ);
assertEquals(1, delta.otherUidEnergiesUJ[0].size());
assertEquals(11 - 9, delta.otherUidEnergiesUJ[0].get(2));
assertNullOrEmpty(delta.otherUidEnergiesUJ[1]); // Not present
assertEquals(1, delta.otherUidEnergiesUJ[2].size());
assertEquals(8, delta.otherUidEnergiesUJ[2].get(47));
}
private void incrementEnergyOfSubsystem(int subsystem, long energy) {
mCurrentSubsystemEnergyUJ[mSubsystemIndices[subsystem]] += energy;
/** Test updateAndGetDelta() when the results have consumers absent from idToConsumerMap. */
@Test
public void testUpdateAndGetDelta_some() {
final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(SOME_ID_CONSUMER_MAP);
// results0
MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0);
if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
assertEquals(UNAVAILABLE, delta.displayEnergyUJ);
assertNull(delta.otherTotalEnergyUJ);
assertNull(delta.otherUidEnergiesUJ);
}
// results1
delta = snapshot.updateAndGetDelta(RESULTS_1);
assertNotNull(delta);
assertEquals(24 - 14, delta.displayEnergyUJ);
assertNull(delta.otherTotalEnergyUJ); // Although in the results, they're not in the idMap
assertNull(delta.otherUidEnergiesUJ);
}
@Test
public void testUpdateAndGetDelta_null() {
assertNull(mSnapshot.updateAndGetDelta(null));
public void testGetNumOtherOrdinals() {
final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(ALL_ID_CONSUMER_MAP);
assertEquals(3, snapshot.getNumOtherOrdinals());
}
@Test
public void testHasSubsystem() {
// Setup MeasuredEnergySnapshot which reported some of the subsystems.
final int[] subsystems = {SUBSYSTEM_DISPLAY, SUBSYSTEM_CATAPULT};
MeasuredEnergyArray measuredEnergyArray = new MeasuredEnergyArray() {
@Override
public int getSubsystem(int index) {
return subsystems[index];
}
public void testGetNumOtherOrdinals_none() {
final MeasuredEnergySnapshot snapshot = new MeasuredEnergySnapshot(SOME_ID_CONSUMER_MAP);
assertEquals(0, snapshot.getNumOtherOrdinals());
}
@Override
public long getEnergy(int index) {
return 0; // Irrelevant for this test.
}
private static EnergyConsumer createEnergyConsumer(int id, int ord, byte type, String name) {
final EnergyConsumer ec = new EnergyConsumer();
ec.id = id;
ec.ordinal = ord;
ec.type = type;
ec.name = name;
return ec;
}
@Override
public int size() {
return subsystems.length;
}
};
final MeasuredEnergySnapshot snapshot =
new MeasuredEnergySnapshot(NUMBER_SUBSYSTEMS, measuredEnergyArray);
private static SparseArray<EnergyConsumer> createIdToConsumerMap(EnergyConsumer ... ecs) {
final SparseArray<EnergyConsumer> map = new SparseArray<>();
for (EnergyConsumer ec : ecs) {
map.put(ec.id, ec);
}
return map;
}
assertTrue(snapshot.hasSubsystem(SUBSYSTEM_DISPLAY));
assertTrue(snapshot.hasSubsystem(SUBSYSTEM_CATAPULT));
assertFalse(snapshot.hasSubsystem(SUBSYSTEM_NEVER_USED));
private static EnergyConsumerResult createEnergyConsumerResult(
int id, long energyUWs, int[] uids, long[] uidEnergies) {
final EnergyConsumerResult ecr = new EnergyConsumerResult();
ecr.id = id;
ecr.energyUWs = energyUWs;
if (uids != null) {
ecr.attribution = new EnergyConsumerAttribution[uids.length];
for (int i = 0; i < uids.length; i++) {
ecr.attribution[i] = new EnergyConsumerAttribution();
ecr.attribution[i].uid = uids[i];
ecr.attribution[i].energyUWs = uidEnergies[i];
}
}
return ecr;
}
private void assertNullOrEmpty(SparseLongArray a) {
if (a != null) assertEquals("Array should be null or empty", 0, a.size());
}
}