/* * 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 static android.os.BatteryStats.POWER_DATA_UNAVAILABLE; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.text.TextUtils; import android.util.DebugUtils; import android.util.Slog; import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.LongMultiStateCounter; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** * Tracks the charge consumption of various subsystems according to their * {@link StandardPowerBucket} or custom power bucket (which is tied to * {@link android.hardware.power.stats.EnergyConsumer.ordinal}). * * This class doesn't use a TimeBase, and instead requires manual decisions about when to * accumulate since it is trivial. However, in the future, a TimeBase could be used instead. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class EnergyConsumerStats { private static final String TAG = "MeasuredEnergyStats"; // Note: {@link BatteryStats#VERSION} MUST be updated if standard // power bucket integers are modified/added/removed. public static final int POWER_BUCKET_UNKNOWN = -1; public static final int POWER_BUCKET_SCREEN_ON = 0; public static final int POWER_BUCKET_SCREEN_DOZE = 1; public static final int POWER_BUCKET_SCREEN_OTHER = 2; public static final int POWER_BUCKET_CPU = 3; public static final int POWER_BUCKET_WIFI = 4; public static final int POWER_BUCKET_BLUETOOTH = 5; public static final int POWER_BUCKET_GNSS = 6; public static final int POWER_BUCKET_MOBILE_RADIO = 7; public static final int POWER_BUCKET_CAMERA = 8; public static final int POWER_BUCKET_PHONE = 9; public static final int NUMBER_STANDARD_POWER_BUCKETS = 10; // Buckets above this are custom. @IntDef(prefix = {"POWER_BUCKET_"}, value = { POWER_BUCKET_UNKNOWN, POWER_BUCKET_SCREEN_ON, POWER_BUCKET_SCREEN_DOZE, POWER_BUCKET_SCREEN_OTHER, POWER_BUCKET_CPU, POWER_BUCKET_WIFI, POWER_BUCKET_BLUETOOTH, POWER_BUCKET_GNSS, POWER_BUCKET_MOBILE_RADIO, POWER_BUCKET_CAMERA, POWER_BUCKET_PHONE, }) @Retention(RetentionPolicy.SOURCE) public @interface StandardPowerBucket { } private static final int INVALID_STATE = -1; /** * Configuration of measured energy stats: which power rails (buckets) are supported on * this device, what custom power drains are supported etc. */ public static class Config { private final boolean[] mSupportedStandardBuckets; @NonNull private final String[] mCustomBucketNames; private final boolean[] mSupportedMultiStateBuckets; @NonNull private final String[] mStateNames; public Config(@NonNull boolean[] supportedStandardBuckets, @Nullable String[] customBucketNames, @NonNull int[] supportedMultiStateBuckets, @Nullable String[] stateNames) { mSupportedStandardBuckets = supportedStandardBuckets; mCustomBucketNames = customBucketNames != null ? customBucketNames : new String[0]; mSupportedMultiStateBuckets = new boolean[supportedStandardBuckets.length + mCustomBucketNames.length]; for (int bucket : supportedMultiStateBuckets) { if (mSupportedStandardBuckets[bucket]) { mSupportedMultiStateBuckets[bucket] = true; } } mStateNames = stateNames != null ? stateNames : new String[] {""}; } /** * Returns true if the supplied Config is compatible with this one and therefore * data collected with one of them will work with the other. */ public boolean isCompatible(Config other) { return Arrays.equals(mSupportedStandardBuckets, other.mSupportedStandardBuckets) && Arrays.equals(mCustomBucketNames, other.mCustomBucketNames) && Arrays.equals(mSupportedMultiStateBuckets, other.mSupportedMultiStateBuckets) && Arrays.equals(mStateNames, other.mStateNames); } /** * Writes the Config object into the supplied Parcel. */ public static void writeToParcel(@Nullable Config config, Parcel out) { if (config == null) { out.writeBoolean(false); return; } out.writeBoolean(true); out.writeInt(config.mSupportedStandardBuckets.length); out.writeBooleanArray(config.mSupportedStandardBuckets); out.writeStringArray(config.mCustomBucketNames); int multiStateBucketCount = 0; for (boolean supported : config.mSupportedMultiStateBuckets) { if (supported) { multiStateBucketCount++; } } final int[] supportedMultiStateBuckets = new int[multiStateBucketCount]; int index = 0; for (int bucket = 0; bucket < config.mSupportedMultiStateBuckets.length; bucket++) { if (config.mSupportedMultiStateBuckets[bucket]) { supportedMultiStateBuckets[index++] = bucket; } } out.writeInt(multiStateBucketCount); out.writeIntArray(supportedMultiStateBuckets); out.writeStringArray(config.mStateNames); } /** * Reads a Config object from the supplied Parcel. */ @Nullable public static Config createFromParcel(Parcel in) { if (!in.readBoolean()) { return null; } final int supportedStandardBucketCount = in.readInt(); final boolean[] supportedStandardBuckets = new boolean[supportedStandardBucketCount]; in.readBooleanArray(supportedStandardBuckets); final String[] customBucketNames = in.readStringArray(); final int supportedMultiStateBucketCount = in.readInt(); final int[] supportedMultiStateBuckets = new int[supportedMultiStateBucketCount]; in.readIntArray(supportedMultiStateBuckets); final String[] stateNames = in.readStringArray(); return new Config(supportedStandardBuckets, customBucketNames, supportedMultiStateBuckets, stateNames); } /** Get number of possible buckets, including both standard and custom ones. */ private int getNumberOfBuckets() { return mSupportedStandardBuckets.length + mCustomBucketNames.length; } /** * Returns true if the specified charge bucket is tracked. */ public boolean isSupportedBucket(int index) { return mSupportedStandardBuckets[index]; } @NonNull public String[] getCustomBucketNames() { return mCustomBucketNames; } /** * Returns true if the specified charge bucket is tracked on a per-state basis. */ public boolean isSupportedMultiStateBucket(int index) { return mSupportedMultiStateBuckets[index]; } @NonNull public String[] getStateNames() { return mStateNames; } /** * If the index is a standard bucket, returns its name; otherwise returns its prefixed * custom bucket number. */ private String getBucketName(int index) { if (isValidStandardBucket(index)) { return DebugUtils.valueToString(EnergyConsumerStats.class, "POWER_BUCKET_", index); } final int customBucket = indexToCustomBucket(index); StringBuilder name = new StringBuilder().append("CUSTOM_").append(customBucket); if (!TextUtils.isEmpty(mCustomBucketNames[customBucket])) { name.append('(').append(mCustomBucketNames[customBucket]).append(')'); } return name.toString(); } } private final Config mConfig; /** * Total charge (in microcoulombs) that a power bucket (including both * {@link StandardPowerBucket} and custom buckets) has accumulated since the last reset. * Values MUST be non-zero or POWER_DATA_UNAVAILABLE. Accumulation only occurs * while the necessary conditions are satisfied (e.g. on battery). * * Charge for both {@link StandardPowerBucket}s and custom power buckets are stored in this * array, and may internally both referred to as 'buckets'. This is an implementation detail; * externally, we differentiate between these two data sources. * * Warning: Long array is used for access speed. If the number of supported subsystems * becomes large, consider using an alternate data structure such as a SparseLongArray. */ private final long[] mAccumulatedChargeMicroCoulomb; private LongMultiStateCounter[] mAccumulatedMultiStateChargeMicroCoulomb; private int mState = INVALID_STATE; private long mStateChangeTimestampMs; /** * Creates a MeasuredEnergyStats set to support the provided power buckets. * supportedStandardBuckets must be of size {@link #NUMBER_STANDARD_POWER_BUCKETS}. * numCustomBuckets >= 0 is the number of (non-standard) custom power buckets on the device. */ public EnergyConsumerStats(EnergyConsumerStats.Config config) { mConfig = config; final int numTotalBuckets = config.getNumberOfBuckets(); mAccumulatedChargeMicroCoulomb = new long[numTotalBuckets]; // Initialize to all zeros where supported, otherwise POWER_DATA_UNAVAILABLE. // All custom buckets are, by definition, supported, so their values stay at 0. for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) { if (!mConfig.mSupportedStandardBuckets[stdBucket]) { mAccumulatedChargeMicroCoulomb[stdBucket] = POWER_DATA_UNAVAILABLE; } } } /** * Reads a MeasuredEnergyStats from the supplied Parcel. */ @Nullable public static EnergyConsumerStats createFromParcel(Config config, Parcel in) { if (!in.readBoolean()) { return null; } return new EnergyConsumerStats(config, in); } /** Construct from parcel. */ public EnergyConsumerStats(EnergyConsumerStats.Config config, Parcel in) { mConfig = config; final int size = in.readInt(); mAccumulatedChargeMicroCoulomb = new long[size]; in.readLongArray(mAccumulatedChargeMicroCoulomb); if (in.readBoolean()) { mAccumulatedMultiStateChargeMicroCoulomb = new LongMultiStateCounter[size]; for (int i = 0; i < size; i++) { if (in.readBoolean()) { mAccumulatedMultiStateChargeMicroCoulomb[i] = LongMultiStateCounter.CREATOR.createFromParcel(in); } } } else { mAccumulatedMultiStateChargeMicroCoulomb = null; } } /** Write to parcel */ public void writeToParcel(Parcel out) { out.writeInt(mAccumulatedChargeMicroCoulomb.length); out.writeLongArray(mAccumulatedChargeMicroCoulomb); if (mAccumulatedMultiStateChargeMicroCoulomb != null) { out.writeBoolean(true); for (LongMultiStateCounter counter : mAccumulatedMultiStateChargeMicroCoulomb) { if (counter != null) { out.writeBoolean(true); counter.writeToParcel(out, 0); } else { out.writeBoolean(false); } } } else { out.writeBoolean(false); } } /** * Read from summary parcel. * Note: Measured subsystem (and therefore bucket) availability may be different from when the * summary parcel was written. Availability has already been correctly set in the constructor. * Note: {@link android.os.BatteryStats#VERSION} must be updated if summary parceling changes. * * Corresponding write performed by {@link #writeSummaryToParcel(Parcel)}. */ private void readSummaryFromParcel(Parcel in) { final int numWrittenEntries = in.readInt(); for (int entry = 0; entry < numWrittenEntries; entry++) { final int index = in.readInt(); final long chargeUC = in.readLong(); LongMultiStateCounter multiStateCounter = null; if (in.readBoolean()) { multiStateCounter = LongMultiStateCounter.CREATOR.createFromParcel(in); if (mConfig == null || multiStateCounter.getStateCount() != mConfig.getStateNames().length) { multiStateCounter = null; } } if (index < mAccumulatedChargeMicroCoulomb.length) { setValueIfSupported(index, chargeUC); if (multiStateCounter != null) { if (mAccumulatedMultiStateChargeMicroCoulomb == null) { mAccumulatedMultiStateChargeMicroCoulomb = new LongMultiStateCounter[mAccumulatedChargeMicroCoulomb.length]; } mAccumulatedMultiStateChargeMicroCoulomb[index] = multiStateCounter; } } } } /** * Write to summary parcel. * Note: Measured subsystem availability may be different when the summary parcel is read. * * Corresponding read performed by {@link #readSummaryFromParcel(Parcel)}. */ private void writeSummaryToParcel(Parcel out) { final int posOfNumWrittenEntries = out.dataPosition(); out.writeInt(0); int numWrittenEntries = 0; // Write only the supported buckets (with non-zero charge, if applicable). for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { final long charge = mAccumulatedChargeMicroCoulomb[index]; if (charge <= 0) continue; out.writeInt(index); out.writeLong(charge); if (mAccumulatedMultiStateChargeMicroCoulomb != null && mAccumulatedMultiStateChargeMicroCoulomb[index] != null) { out.writeBoolean(true); mAccumulatedMultiStateChargeMicroCoulomb[index].writeToParcel(out, 0); } else { out.writeBoolean(false); } numWrittenEntries++; } final int currPos = out.dataPosition(); out.setDataPosition(posOfNumWrittenEntries); out.writeInt(numWrittenEntries); out.setDataPosition(currPos); } /** Updates the given standard power bucket with the given charge if accumulate is true. */ public void updateStandardBucket(@StandardPowerBucket int bucket, long chargeDeltaUC) { updateStandardBucket(bucket, chargeDeltaUC, 0); } /** * Updates the given standard power bucket with the given charge if supported. * @param timestampMs elapsed realtime in milliseconds */ public void updateStandardBucket(@StandardPowerBucket int bucket, long chargeDeltaUC, long timestampMs) { checkValidStandardBucket(bucket); updateEntry(bucket, chargeDeltaUC, timestampMs); } /** Updates the given custom power bucket with the given charge if accumulate is true. */ public void updateCustomBucket(int customBucket, long chargeDeltaUC) { updateCustomBucket(customBucket, chargeDeltaUC, 0); } /** * Updates the given custom power bucket with the given charge if supported. * @param timestampMs elapsed realtime in milliseconds */ public void updateCustomBucket(int customBucket, long chargeDeltaUC, long timestampMs) { if (!isValidCustomBucket(customBucket)) { Slog.e(TAG, "Attempted to update invalid custom bucket " + customBucket); return; } final int index = customBucketToIndex(customBucket); updateEntry(index, chargeDeltaUC, timestampMs); } /** Updates the given bucket with the given charge delta. */ private void updateEntry(int index, long chargeDeltaUC, long timestampMs) { if (mAccumulatedChargeMicroCoulomb[index] >= 0L) { mAccumulatedChargeMicroCoulomb[index] += chargeDeltaUC; if (mState != INVALID_STATE && mConfig.isSupportedMultiStateBucket(index)) { if (mAccumulatedMultiStateChargeMicroCoulomb == null) { mAccumulatedMultiStateChargeMicroCoulomb = new LongMultiStateCounter[mAccumulatedChargeMicroCoulomb.length]; } LongMultiStateCounter counter = mAccumulatedMultiStateChargeMicroCoulomb[index]; if (counter == null) { counter = new LongMultiStateCounter(mConfig.mStateNames.length); mAccumulatedMultiStateChargeMicroCoulomb[index] = counter; counter.setState(mState, mStateChangeTimestampMs); counter.updateValue(0, mStateChangeTimestampMs); } counter.updateValue(mAccumulatedChargeMicroCoulomb[index], timestampMs); } } else { Slog.wtf(TAG, "Attempting to add " + chargeDeltaUC + " to unavailable bucket " + mConfig.getBucketName(index) + " whose value was " + mAccumulatedChargeMicroCoulomb[index]); } } /** * Updates the "state" on all multi-state counters used by this MeasuredEnergyStats. Further * accumulated charge updates will assign the deltas to this state, until the state changes. * * If setState is never called on a MeasuredEnergyStats object, then it does not track * per-state usage. */ public void setState(int state, long timestampMs) { mState = state; mStateChangeTimestampMs = timestampMs; if (mAccumulatedMultiStateChargeMicroCoulomb == null) { mAccumulatedMultiStateChargeMicroCoulomb = new LongMultiStateCounter[mAccumulatedChargeMicroCoulomb.length]; } for (int i = 0; i < mAccumulatedMultiStateChargeMicroCoulomb.length; i++) { LongMultiStateCounter counter = mAccumulatedMultiStateChargeMicroCoulomb[i]; if (counter == null && mConfig.isSupportedMultiStateBucket(i)) { counter = new LongMultiStateCounter(mConfig.mStateNames.length); counter.updateValue(0, timestampMs); mAccumulatedMultiStateChargeMicroCoulomb[i] = counter; } if (counter != null) { counter.setState(state, timestampMs); } } } /** * Return accumulated charge (in microcouloumb) for a standard power bucket since last reset. * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable. * @throws IllegalArgumentException if no such {@link StandardPowerBucket}. */ public long getAccumulatedStandardBucketCharge(@StandardPowerBucket int bucket) { checkValidStandardBucket(bucket); return mAccumulatedChargeMicroCoulomb[bucket]; } /** * Returns the accumulated charge (in microcouloumb) for the standard power bucket and * the specified state since last reset. * * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable. */ public long getAccumulatedStandardBucketCharge(@StandardPowerBucket int bucket, int state) { if (!mConfig.isSupportedMultiStateBucket(bucket)) { return POWER_DATA_UNAVAILABLE; } if (mAccumulatedMultiStateChargeMicroCoulomb == null) { return 0; } final LongMultiStateCounter counter = mAccumulatedMultiStateChargeMicroCoulomb[bucket]; if (counter == null) { return 0; } return counter.getCount(state); } /** * Return accumulated charge (in microcoulomb) for the a custom power bucket since last * reset. * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable. */ @VisibleForTesting public long getAccumulatedCustomBucketCharge(int customBucket) { if (!isValidCustomBucket(customBucket)) { return POWER_DATA_UNAVAILABLE; } return mAccumulatedChargeMicroCoulomb[customBucketToIndex(customBucket)]; } /** * Return accumulated charge (in microcoulomb) for all custom power buckets since last reset. */ public @NonNull long[] getAccumulatedCustomBucketCharges() { final long[] charges = new long[getNumberCustomPowerBuckets()]; for (int bucket = 0; bucket < charges.length; bucket++) { charges[bucket] = mAccumulatedChargeMicroCoulomb[customBucketToIndex(bucket)]; } return charges; } /** * Map {@link android.view.Display} STATE_ to corresponding {@link StandardPowerBucket}. */ public static @StandardPowerBucket int getDisplayPowerBucket(int screenState) { if (Display.isOnState(screenState)) { return POWER_BUCKET_SCREEN_ON; } if (Display.isDozeState(screenState)) { return POWER_BUCKET_SCREEN_DOZE; } return POWER_BUCKET_SCREEN_OTHER; } /** * Create a MeasuredEnergyStats using the template to determine which buckets are supported, * and populate this new object from the given parcel. * * The parcel must be consistent with the template in terms of the number of * possible (not necessarily supported) standard and custom buckets. * * Corresponding write performed by * {@link #writeSummaryToParcel(EnergyConsumerStats, Parcel)}. * * @return a new MeasuredEnergyStats object as described. * Returns null if the stats contain no non-0 information (such as if template is null * or if the parcel indicates there is no data to populate). */ @Nullable public static EnergyConsumerStats createAndReadSummaryFromParcel(@Nullable Config config, Parcel in) { final int arraySize = in.readInt(); // Check if any MeasuredEnergyStats exists on the parcel if (arraySize == 0) return null; if (config == null) { // Nothing supported anymore. Create placeholder object just to consume the parcel data. final EnergyConsumerStats mes = new EnergyConsumerStats( new Config(new boolean[arraySize], null, new int[0], new String[]{""})); mes.readSummaryFromParcel(in); return null; } if (arraySize != config.getNumberOfBuckets()) { Slog.wtf(TAG, "Size of MeasuredEnergyStats parcel (" + arraySize + ") does not match config (" + config.getNumberOfBuckets() + ")."); // Something is horribly wrong. Just consume the parcel and return null. final EnergyConsumerStats mes = new EnergyConsumerStats(config); mes.readSummaryFromParcel(in); return null; } final EnergyConsumerStats stats = new EnergyConsumerStats(config); stats.readSummaryFromParcel(in); if (stats.containsInterestingData()) { return stats; } else { // Don't waste RAM on it (and make sure not to persist it in the next writeSummary) return null; } } /** Returns true iff any of the buckets are supported and non-zero. */ private boolean containsInterestingData() { for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { if (mAccumulatedChargeMicroCoulomb[index] > 0) return true; } return false; } /** * Write a MeasuredEnergyStats to a parcel. If the stats is null, just write a 0. * * Corresponding read performed by {@link #createAndReadSummaryFromParcel}. */ public static void writeSummaryToParcel(@Nullable EnergyConsumerStats stats, Parcel dest) { if (stats == null) { dest.writeInt(0); return; } dest.writeInt(stats.mConfig.getNumberOfBuckets()); stats.writeSummaryToParcel(dest); } /** Reset accumulated charges. */ private void reset() { final int numIndices = mConfig.getNumberOfBuckets(); for (int index = 0; index < numIndices; index++) { setValueIfSupported(index, 0L); if (mAccumulatedMultiStateChargeMicroCoulomb != null && mAccumulatedMultiStateChargeMicroCoulomb[index] != null) { mAccumulatedMultiStateChargeMicroCoulomb[index].reset(); } } } /** Reset accumulated charges of the given stats. */ public static void resetIfNotNull(@Nullable EnergyConsumerStats stats) { if (stats != null) stats.reset(); } /** If the index is AVAILABLE, overwrite its value; otherwise leave it as UNAVAILABLE. */ private void setValueIfSupported(int index, long value) { if (mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE) { mAccumulatedChargeMicroCoulomb[index] = value; } } /** * Check if measuring the charge consumption of the given bucket is supported by this device. * @throws IllegalArgumentException if not a valid {@link StandardPowerBucket}. */ public boolean isStandardBucketSupported(@StandardPowerBucket int bucket) { checkValidStandardBucket(bucket); return isIndexSupported(bucket); } private boolean isIndexSupported(int index) { return mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE; } /** Dump debug data. */ public void dump(PrintWriter pw) { pw.print(" "); for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { pw.print(mConfig.getBucketName(index)); pw.print(" : "); pw.print(mAccumulatedChargeMicroCoulomb[index]); if (!isIndexSupported(index)) { pw.print(" (unsupported)"); } if (mAccumulatedMultiStateChargeMicroCoulomb != null) { final LongMultiStateCounter counter = mAccumulatedMultiStateChargeMicroCoulomb[index]; if (counter != null) { pw.print(" ["); for (int i = 0; i < mConfig.mStateNames.length; i++) { if (i != 0) { pw.print(" "); } pw.print(mConfig.mStateNames[i]); pw.print(": "); pw.print(counter.getCount(i)); } pw.print("]"); } } if (index != mAccumulatedChargeMicroCoulomb.length - 1) { pw.print(", "); } } pw.println(); } /** Get the number of custom power buckets on this device. */ public int getNumberCustomPowerBuckets() { return mAccumulatedChargeMicroCoulomb.length - NUMBER_STANDARD_POWER_BUCKETS; } private static int customBucketToIndex(int customBucket) { return customBucket + NUMBER_STANDARD_POWER_BUCKETS; } private static int indexToCustomBucket(int index) { return index - NUMBER_STANDARD_POWER_BUCKETS; } private static void checkValidStandardBucket(@StandardPowerBucket int bucket) { if (!isValidStandardBucket(bucket)) { throw new IllegalArgumentException("Illegal StandardPowerBucket " + bucket); } } private static boolean isValidStandardBucket(@StandardPowerBucket int bucket) { return bucket >= 0 && bucket < NUMBER_STANDARD_POWER_BUCKETS; } /** Returns whether the given custom bucket is valid (exists) on this device. */ @VisibleForTesting public boolean isValidCustomBucket(int customBucket) { return customBucket >= 0 && customBucketToIndex(customBucket) < mAccumulatedChargeMicroCoulomb.length; } }