1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.power; 18 19 20 import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE; 21 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.os.Parcel; 26 import android.text.TextUtils; 27 import android.util.DebugUtils; 28 import android.util.Slog; 29 import android.view.Display; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import java.io.PrintWriter; 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import java.util.Arrays; 37 38 /** 39 * Tracks the measured charge consumption of various subsystems according to their 40 * {@link StandardPowerBucket} or custom power bucket (which is tied to 41 * {@link android.hardware.power.stats.EnergyConsumer.ordinal}). 42 * 43 * This class doesn't use a TimeBase, and instead requires manually decisions about when to 44 * accumulate since it is trivial. However, in the future, a TimeBase could be used instead. 45 */ 46 @VisibleForTesting 47 public class MeasuredEnergyStats { 48 private static final String TAG = "MeasuredEnergyStats"; 49 50 // Note: {@link com.android.internal.os.BatteryStatsImpl#VERSION} MUST be updated if standard 51 // power bucket integers are modified/added/removed. 52 public static final int POWER_BUCKET_UNKNOWN = -1; 53 public static final int POWER_BUCKET_SCREEN_ON = 0; 54 public static final int POWER_BUCKET_SCREEN_DOZE = 1; 55 public static final int POWER_BUCKET_SCREEN_OTHER = 2; 56 public static final int POWER_BUCKET_CPU = 3; 57 public static final int POWER_BUCKET_WIFI = 4; 58 public static final int POWER_BUCKET_BLUETOOTH = 5; 59 public static final int POWER_BUCKET_GNSS = 6; 60 public static final int POWER_BUCKET_MOBILE_RADIO = 7; 61 public static final int NUMBER_STANDARD_POWER_BUCKETS = 8; // Buckets above this are custom. 62 63 @IntDef(prefix = {"POWER_BUCKET_"}, value = { 64 POWER_BUCKET_UNKNOWN, 65 POWER_BUCKET_SCREEN_ON, 66 POWER_BUCKET_SCREEN_DOZE, 67 POWER_BUCKET_SCREEN_OTHER, 68 POWER_BUCKET_CPU, 69 POWER_BUCKET_WIFI, 70 POWER_BUCKET_BLUETOOTH, 71 POWER_BUCKET_GNSS, 72 POWER_BUCKET_MOBILE_RADIO, 73 }) 74 @Retention(RetentionPolicy.SOURCE) 75 public @interface StandardPowerBucket { 76 } 77 78 /** 79 * Total charge (in microcoulombs) that a power bucket (including both 80 * {@link StandardPowerBucket} and custom buckets) has accumulated since the last reset. 81 * Values MUST be non-zero or POWER_DATA_UNAVAILABLE. Accumulation only occurs 82 * while the necessary conditions are satisfied (e.g. on battery). 83 * 84 * Charge for both {@link StandardPowerBucket}s and custom power buckets are stored in this 85 * array, and may internally both referred to as 'buckets'. This is an implementation detail; 86 * externally, we differentiate between these two data sources. 87 * 88 * Warning: Long array is used for access speed. If the number of supported subsystems 89 * becomes large, consider using an alternate data structure such as a SparseLongArray. 90 */ 91 private final long[] mAccumulatedChargeMicroCoulomb; 92 93 private final String[] mCustomBucketNames; 94 95 /** 96 * Creates a MeasuredEnergyStats set to support the provided power buckets. 97 * supportedStandardBuckets must be of size {@link #NUMBER_STANDARD_POWER_BUCKETS}. 98 * numCustomBuckets >= 0 is the number of (non-standard) custom power buckets on the device. 99 */ MeasuredEnergyStats(@onNull boolean[] supportedStandardBuckets, @Nullable String[] customBucketNames)100 public MeasuredEnergyStats(@NonNull boolean[] supportedStandardBuckets, 101 @Nullable String[] customBucketNames) { 102 mCustomBucketNames = customBucketNames == null ? new String[0] : customBucketNames; 103 final int numTotalBuckets = NUMBER_STANDARD_POWER_BUCKETS + mCustomBucketNames.length; 104 mAccumulatedChargeMicroCoulomb = new long[numTotalBuckets]; 105 // Initialize to all zeros where supported, otherwise POWER_DATA_UNAVAILABLE. 106 // All custom buckets are, by definition, supported, so their values stay at 0. 107 for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) { 108 if (!supportedStandardBuckets[stdBucket]) { 109 mAccumulatedChargeMicroCoulomb[stdBucket] = POWER_DATA_UNAVAILABLE; 110 } 111 } 112 } 113 114 /** 115 * Creates a new zero'd MeasuredEnergyStats, using the template to determine which buckets are 116 * supported. This certainly does NOT produce an exact clone of the template. 117 */ MeasuredEnergyStats(MeasuredEnergyStats template)118 private MeasuredEnergyStats(MeasuredEnergyStats template) { 119 final int numIndices = template.getNumberOfIndices(); 120 mAccumulatedChargeMicroCoulomb = new long[numIndices]; 121 // Initialize to all zeros where supported, otherwise POWER_DATA_UNAVAILABLE. 122 // All custom buckets are, by definition, supported, so their values stay at 0. 123 for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) { 124 if (!template.isIndexSupported(stdBucket)) { 125 mAccumulatedChargeMicroCoulomb[stdBucket] = POWER_DATA_UNAVAILABLE; 126 } 127 } 128 mCustomBucketNames = template.getCustomBucketNames(); 129 } 130 131 /** 132 * Creates a new zero'd MeasuredEnergyStats, using the template to determine which buckets are 133 * supported. 134 */ createFromTemplate(MeasuredEnergyStats template)135 public static MeasuredEnergyStats createFromTemplate(MeasuredEnergyStats template) { 136 return new MeasuredEnergyStats(template); 137 } 138 139 /** 140 * Constructor for creating a temp MeasuredEnergyStats. 141 * See {@link #createAndReadSummaryFromParcel(Parcel, MeasuredEnergyStats)}. 142 */ MeasuredEnergyStats(int numIndices)143 private MeasuredEnergyStats(int numIndices) { 144 mAccumulatedChargeMicroCoulomb = new long[numIndices]; 145 mCustomBucketNames = new String[numIndices - NUMBER_STANDARD_POWER_BUCKETS]; 146 } 147 148 /** Construct from parcel. */ MeasuredEnergyStats(Parcel in)149 public MeasuredEnergyStats(Parcel in) { 150 final int size = in.readInt(); 151 mAccumulatedChargeMicroCoulomb = new long[size]; 152 in.readLongArray(mAccumulatedChargeMicroCoulomb); 153 mCustomBucketNames = in.readStringArray(); 154 } 155 156 /** Write to parcel */ writeToParcel(Parcel out)157 public void writeToParcel(Parcel out) { 158 out.writeInt(mAccumulatedChargeMicroCoulomb.length); 159 out.writeLongArray(mAccumulatedChargeMicroCoulomb); 160 out.writeStringArray(mCustomBucketNames); 161 } 162 163 /** 164 * Read from summary parcel. 165 * Note: Measured subsystem (and therefore bucket) availability may be different from when the 166 * summary parcel was written. Availability has already been correctly set in the constructor. 167 * Note: {@link com.android.internal.os.BatteryStatsImpl#VERSION} must be updated if summary 168 * parceling changes. 169 * 170 * Corresponding write performed by {@link #writeSummaryToParcel(Parcel, boolean)}. 171 */ readSummaryFromParcel(Parcel in, boolean overwriteAvailability)172 private void readSummaryFromParcel(Parcel in, boolean overwriteAvailability) { 173 final int numWrittenEntries = in.readInt(); 174 for (int entry = 0; entry < numWrittenEntries; entry++) { 175 final int index = in.readInt(); 176 final long chargeUC = in.readLong(); 177 if (overwriteAvailability) { 178 mAccumulatedChargeMicroCoulomb[index] = chargeUC; 179 } else { 180 setValueIfSupported(index, chargeUC); 181 } 182 } 183 } 184 185 /** 186 * Write to summary parcel. 187 * Note: Measured subsystem availability may be different when the summary parcel is read. 188 * 189 * Corresponding read performed by {@link #readSummaryFromParcel(Parcel, boolean)}. 190 */ writeSummaryToParcel(Parcel out, boolean skipZero)191 private void writeSummaryToParcel(Parcel out, boolean skipZero) { 192 final int posOfNumWrittenEntries = out.dataPosition(); 193 out.writeInt(0); 194 int numWrittenEntries = 0; 195 // Write only the supported buckets (with non-zero charge, if applicable). 196 for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { 197 final long charge = mAccumulatedChargeMicroCoulomb[index]; 198 if (charge < 0) continue; 199 if (charge == 0 && skipZero) continue; 200 201 out.writeInt(index); 202 out.writeLong(charge); 203 numWrittenEntries++; 204 } 205 final int currPos = out.dataPosition(); 206 out.setDataPosition(posOfNumWrittenEntries); 207 out.writeInt(numWrittenEntries); 208 out.setDataPosition(currPos); 209 } 210 211 /** Get number of possible buckets, including both standard and custom ones. */ getNumberOfIndices()212 private int getNumberOfIndices() { 213 return mAccumulatedChargeMicroCoulomb.length; 214 } 215 216 217 /** Updates the given standard power bucket with the given charge if accumulate is true. */ updateStandardBucket(@tandardPowerBucket int bucket, long chargeDeltaUC)218 public void updateStandardBucket(@StandardPowerBucket int bucket, long chargeDeltaUC) { 219 checkValidStandardBucket(bucket); 220 updateEntry(bucket, chargeDeltaUC); 221 } 222 223 /** Updates the given custom power bucket with the given charge if accumulate is true. */ updateCustomBucket(int customBucket, long chargeDeltaUC)224 public void updateCustomBucket(int customBucket, long chargeDeltaUC) { 225 if (!isValidCustomBucket(customBucket)) { 226 Slog.e(TAG, "Attempted to update invalid custom bucket " + customBucket); 227 return; 228 } 229 final int index = customBucketToIndex(customBucket); 230 updateEntry(index, chargeDeltaUC); 231 } 232 233 /** Updates the given index with the given charge if accumulate is true. */ updateEntry(int index, long chargeDeltaUC)234 private void updateEntry(int index, long chargeDeltaUC) { 235 if (mAccumulatedChargeMicroCoulomb[index] >= 0L) { 236 mAccumulatedChargeMicroCoulomb[index] += chargeDeltaUC; 237 } else { 238 Slog.wtf(TAG, "Attempting to add " + chargeDeltaUC + " to unavailable bucket " 239 + getBucketName(index) + " whose value was " 240 + mAccumulatedChargeMicroCoulomb[index]); 241 } 242 } 243 244 /** 245 * Return accumulated charge (in microcouloumb) for a standard power bucket since last reset. 246 * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable. 247 * @throws IllegalArgumentException if no such {@link StandardPowerBucket}. 248 */ getAccumulatedStandardBucketCharge(@tandardPowerBucket int bucket)249 public long getAccumulatedStandardBucketCharge(@StandardPowerBucket int bucket) { 250 checkValidStandardBucket(bucket); 251 return mAccumulatedChargeMicroCoulomb[bucket]; 252 } 253 254 /** 255 * Return accumulated charge (in microcoulomb) for the a custom power bucket since last 256 * reset. 257 * Returns {@link android.os.BatteryStats#POWER_DATA_UNAVAILABLE} if this data is unavailable. 258 */ 259 @VisibleForTesting getAccumulatedCustomBucketCharge(int customBucket)260 public long getAccumulatedCustomBucketCharge(int customBucket) { 261 if (!isValidCustomBucket(customBucket)) { 262 return POWER_DATA_UNAVAILABLE; 263 } 264 return mAccumulatedChargeMicroCoulomb[customBucketToIndex(customBucket)]; 265 } 266 267 /** 268 * Return accumulated charge (in microcoulomb) for all custom power buckets since last reset. 269 */ getAccumulatedCustomBucketCharges()270 public @NonNull long[] getAccumulatedCustomBucketCharges() { 271 final long[] charges = new long[getNumberCustomPowerBuckets()]; 272 for (int bucket = 0; bucket < charges.length; bucket++) { 273 charges[bucket] = mAccumulatedChargeMicroCoulomb[customBucketToIndex(bucket)]; 274 } 275 return charges; 276 } 277 278 /** 279 * Map {@link android.view.Display} STATE_ to corresponding {@link StandardPowerBucket}. 280 */ getDisplayPowerBucket(int screenState)281 public static @StandardPowerBucket int getDisplayPowerBucket(int screenState) { 282 if (Display.isOnState(screenState)) { 283 return POWER_BUCKET_SCREEN_ON; 284 } 285 if (Display.isDozeState(screenState)) { 286 return POWER_BUCKET_SCREEN_DOZE; 287 } 288 return POWER_BUCKET_SCREEN_OTHER; 289 } 290 291 /** 292 * Create a MeasuredEnergyStats object from a summary parcel. 293 * 294 * Corresponding write performed by 295 * {@link #writeSummaryToParcel(MeasuredEnergyStats, Parcel, boolean, boolean)}. 296 * 297 * @return a new MeasuredEnergyStats object as described. 298 * Returns null if the parcel indicates there is no data to populate. 299 */ createAndReadSummaryFromParcel(Parcel in)300 public static @Nullable MeasuredEnergyStats createAndReadSummaryFromParcel(Parcel in) { 301 final int arraySize = in.readInt(); 302 // Check if any MeasuredEnergyStats exists on the parcel 303 if (arraySize == 0) return null; 304 305 final String[] customBucketNames; 306 if (in.readBoolean()) { 307 customBucketNames = in.readStringArray(); 308 } else { 309 customBucketNames = new String[0]; 310 } 311 final MeasuredEnergyStats stats = new MeasuredEnergyStats( 312 new boolean[NUMBER_STANDARD_POWER_BUCKETS], customBucketNames); 313 stats.readSummaryFromParcel(in, true); 314 return stats; 315 } 316 317 /** 318 * Create a MeasuredEnergyStats using the template to determine which buckets are supported, 319 * and populate this new object from the given parcel. 320 * 321 * The parcel must be consistent with the template in terms of the number of 322 * possible (not necessarily supported) standard and custom buckets. 323 * 324 * Corresponding write performed by 325 * {@link #writeSummaryToParcel(MeasuredEnergyStats, Parcel, boolean, boolean)}. 326 * 327 * @return a new MeasuredEnergyStats object as described. 328 * Returns null if the stats contain no non-0 information (such as if template is null 329 * or if the parcel indicates there is no data to populate). 330 * 331 * @see #createFromTemplate 332 */ createAndReadSummaryFromParcel(Parcel in, @Nullable MeasuredEnergyStats template)333 public static @Nullable MeasuredEnergyStats createAndReadSummaryFromParcel(Parcel in, 334 @Nullable MeasuredEnergyStats template) { 335 final int arraySize = in.readInt(); 336 // Check if any MeasuredEnergyStats exists on the parcel 337 if (arraySize == 0) return null; 338 339 boolean includesCustomBucketNames = in.readBoolean(); 340 if (includesCustomBucketNames) { 341 // Consume the array of custom bucket names. They are already included in the 342 // template. 343 in.readStringArray(); 344 } 345 if (template == null) { 346 // Nothing supported anymore. Create placeholder object just to consume the parcel data. 347 final MeasuredEnergyStats mes = new MeasuredEnergyStats(arraySize); 348 mes.readSummaryFromParcel(in, false); 349 return null; 350 } 351 352 if (arraySize != template.getNumberOfIndices()) { 353 Slog.wtf(TAG, "Size of MeasuredEnergyStats parcel (" + arraySize 354 + ") does not match template (" + template.getNumberOfIndices() + ")."); 355 // Something is horribly wrong. Just consume the parcel and return null. 356 final MeasuredEnergyStats mes = new MeasuredEnergyStats(arraySize); 357 mes.readSummaryFromParcel(in, false); 358 return null; 359 } 360 361 final MeasuredEnergyStats stats = createFromTemplate(template); 362 stats.readSummaryFromParcel(in, false); 363 if (stats.containsInterestingData()) { 364 return stats; 365 } else { 366 // Don't waste RAM on it (and make sure not to persist it in the next writeSummary) 367 return null; 368 } 369 } 370 371 /** Returns true iff any of the buckets are supported and non-zero. */ containsInterestingData()372 private boolean containsInterestingData() { 373 for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { 374 if (mAccumulatedChargeMicroCoulomb[index] > 0) return true; 375 } 376 return false; 377 } 378 379 /** 380 * Write a MeasuredEnergyStats to a parcel. If the stats is null, just write a 0. 381 * 382 * Corresponding read performed by {@link #createAndReadSummaryFromParcel(Parcel)} 383 * and {@link #createAndReadSummaryFromParcel(Parcel, MeasuredEnergyStats)}. 384 */ writeSummaryToParcel(@ullable MeasuredEnergyStats stats, Parcel dest, boolean skipZero, boolean skipCustomBucketNames)385 public static void writeSummaryToParcel(@Nullable MeasuredEnergyStats stats, 386 Parcel dest, boolean skipZero, boolean skipCustomBucketNames) { 387 if (stats == null) { 388 dest.writeInt(0); 389 return; 390 } 391 dest.writeInt(stats.getNumberOfIndices()); 392 if (!skipCustomBucketNames) { 393 dest.writeBoolean(true); 394 dest.writeStringArray(stats.getCustomBucketNames()); 395 } else { 396 dest.writeBoolean(false); 397 } 398 stats.writeSummaryToParcel(dest, skipZero); 399 } 400 401 /** Reset accumulated charges. */ reset()402 private void reset() { 403 final int numIndices = getNumberOfIndices(); 404 for (int index = 0; index < numIndices; index++) { 405 setValueIfSupported(index, 0L); 406 } 407 } 408 409 /** Reset accumulated charges of the given stats. */ resetIfNotNull(@ullable MeasuredEnergyStats stats)410 public static void resetIfNotNull(@Nullable MeasuredEnergyStats stats) { 411 if (stats != null) stats.reset(); 412 } 413 414 /** If the index is AVAILABLE, overwrite its value; otherwise leave it as UNAVAILABLE. */ setValueIfSupported(int index, long value)415 private void setValueIfSupported(int index, long value) { 416 if (mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE) { 417 mAccumulatedChargeMicroCoulomb[index] = value; 418 } 419 } 420 421 /** 422 * Check if measuring the charge consumption of the given bucket is supported by this device. 423 * @throws IllegalArgumentException if not a valid {@link StandardPowerBucket}. 424 */ isStandardBucketSupported(@tandardPowerBucket int bucket)425 public boolean isStandardBucketSupported(@StandardPowerBucket int bucket) { 426 checkValidStandardBucket(bucket); 427 return isIndexSupported(bucket); 428 } 429 isIndexSupported(int index)430 private boolean isIndexSupported(int index) { 431 return mAccumulatedChargeMicroCoulomb[index] != POWER_DATA_UNAVAILABLE; 432 } 433 434 /** Check if the supported power buckets are precisely those given. */ isSupportEqualTo( @onNull boolean[] queriedStandardBuckets, @Nullable String[] customBucketNames)435 public boolean isSupportEqualTo( 436 @NonNull boolean[] queriedStandardBuckets, @Nullable String[] customBucketNames) { 437 if (customBucketNames == null) { 438 //In practice customBucketNames should never be null, but sanitize it just to be sure. 439 customBucketNames = new String[0]; 440 } 441 442 final int numBuckets = getNumberOfIndices(); 443 final int numCustomBuckets = customBucketNames == null ? 0 : customBucketNames.length; 444 if (numBuckets != NUMBER_STANDARD_POWER_BUCKETS + numCustomBuckets) { 445 return false; 446 } 447 448 if (!Arrays.equals(mCustomBucketNames, customBucketNames)) { 449 return false; 450 } 451 452 for (int stdBucket = 0; stdBucket < NUMBER_STANDARD_POWER_BUCKETS; stdBucket++) { 453 if (isStandardBucketSupported(stdBucket) != queriedStandardBuckets[stdBucket]) { 454 return false; 455 } 456 } 457 return true; 458 } 459 getCustomBucketNames()460 public String[] getCustomBucketNames() { 461 return mCustomBucketNames; 462 } 463 464 /** Dump debug data. */ dump(PrintWriter pw)465 public void dump(PrintWriter pw) { 466 pw.print(" "); 467 for (int index = 0; index < mAccumulatedChargeMicroCoulomb.length; index++) { 468 pw.print(getBucketName(index)); 469 pw.print(" : "); 470 pw.print(mAccumulatedChargeMicroCoulomb[index]); 471 if (!isIndexSupported(index)) { 472 pw.print(" (unsupported)"); 473 } 474 if (index != mAccumulatedChargeMicroCoulomb.length - 1) { 475 pw.print(", "); 476 } 477 } 478 pw.println(); 479 } 480 481 /** 482 * If the index is a standard bucket, returns its name; otherwise returns its prefixed custom 483 * bucket number. 484 */ getBucketName(int index)485 private String getBucketName(int index) { 486 if (isValidStandardBucket(index)) { 487 return DebugUtils.valueToString(MeasuredEnergyStats.class, "POWER_BUCKET_", index); 488 } 489 final int customBucket = indexToCustomBucket(index); 490 StringBuilder name = new StringBuilder().append("CUSTOM_").append(customBucket); 491 if (mCustomBucketNames != null && !TextUtils.isEmpty(mCustomBucketNames[customBucket])) { 492 name.append('(').append(mCustomBucketNames[customBucket]).append(')'); 493 } 494 return name.toString(); 495 } 496 497 /** Get the number of custom power buckets on this device. */ getNumberCustomPowerBuckets()498 public int getNumberCustomPowerBuckets() { 499 return mAccumulatedChargeMicroCoulomb.length - NUMBER_STANDARD_POWER_BUCKETS; 500 } 501 customBucketToIndex(int customBucket)502 private static int customBucketToIndex(int customBucket) { 503 return customBucket + NUMBER_STANDARD_POWER_BUCKETS; 504 } 505 indexToCustomBucket(int index)506 private static int indexToCustomBucket(int index) { 507 return index - NUMBER_STANDARD_POWER_BUCKETS; 508 } 509 checkValidStandardBucket(@tandardPowerBucket int bucket)510 private static void checkValidStandardBucket(@StandardPowerBucket int bucket) { 511 if (!isValidStandardBucket(bucket)) { 512 throw new IllegalArgumentException("Illegal StandardPowerBucket " + bucket); 513 } 514 } 515 isValidStandardBucket(@tandardPowerBucket int bucket)516 private static boolean isValidStandardBucket(@StandardPowerBucket int bucket) { 517 return bucket >= 0 && bucket < NUMBER_STANDARD_POWER_BUCKETS; 518 } 519 520 /** Returns whether the given custom bucket is valid (exists) on this device. */ 521 @VisibleForTesting isValidCustomBucket(int customBucket)522 public boolean isValidCustomBucket(int customBucket) { 523 return customBucket >= 0 524 && customBucketToIndex(customBucket) < mAccumulatedChargeMicroCoulomb.length; 525 } 526 } 527