/* * Copyright (C) 2023 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.os; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.BatteryConsumer; import android.os.BatteryStats; import android.os.Bundle; import android.os.Parcel; import android.os.PersistableBundle; import android.os.UserHandle; import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Container for power stats, acquired by various PowerStatsCollector classes. See subclasses for * details. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public final class PowerStats { private static final String TAG = "PowerStats"; private static final BatteryStatsHistory.VarintParceler VARINT_PARCELER = new BatteryStatsHistory.VarintParceler(); private static final byte PARCEL_FORMAT_VERSION = 2; private static final int PARCEL_FORMAT_VERSION_MASK = 0x000000FF; private static final int PARCEL_FORMAT_VERSION_SHIFT = Integer.numberOfTrailingZeros(PARCEL_FORMAT_VERSION_MASK); private static final int STATS_ARRAY_LENGTH_MASK = 0x0000FF00; private static final int STATS_ARRAY_LENGTH_SHIFT = Integer.numberOfTrailingZeros(STATS_ARRAY_LENGTH_MASK); public static final int MAX_STATS_ARRAY_LENGTH = (1 << Integer.bitCount(STATS_ARRAY_LENGTH_MASK)) - 1; private static final int STATE_STATS_ARRAY_LENGTH_MASK = 0x00FF0000; private static final int STATE_STATS_ARRAY_LENGTH_SHIFT = Integer.numberOfTrailingZeros(STATE_STATS_ARRAY_LENGTH_MASK); public static final int MAX_STATE_STATS_ARRAY_LENGTH = (1 << Integer.bitCount(STATE_STATS_ARRAY_LENGTH_MASK)) - 1; private static final int UID_STATS_ARRAY_LENGTH_MASK = 0xFF000000; private static final int UID_STATS_ARRAY_LENGTH_SHIFT = Integer.numberOfTrailingZeros(UID_STATS_ARRAY_LENGTH_MASK); public static final int MAX_UID_STATS_ARRAY_LENGTH = (1 << Integer.bitCount(UID_STATS_ARRAY_LENGTH_MASK)) - 1; /** * Descriptor of the stats collected for a given power component (e.g. CPU, WiFi etc). * This descriptor is used for storing PowerStats and can also be used by power models * to adjust the algorithm in accordance with the stats available on the device. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public static class Descriptor { public static final String EXTRA_DEVICE_STATS_FORMAT = "format-device"; public static final String EXTRA_STATE_STATS_FORMAT = "format-state"; public static final String EXTRA_UID_STATS_FORMAT = "format-uid"; public static final String XML_TAG_DESCRIPTOR = "descriptor"; private static final String XML_ATTR_ID = "id"; private static final String XML_ATTR_NAME = "name"; private static final String XML_ATTR_STATS_ARRAY_LENGTH = "stats-array-length"; private static final String XML_TAG_STATE = "state"; private static final String XML_ATTR_STATE_KEY = "key"; private static final String XML_ATTR_STATE_LABEL = "label"; private static final String XML_ATTR_STATE_STATS_ARRAY_LENGTH = "state-stats-array-length"; private static final String XML_ATTR_UID_STATS_ARRAY_LENGTH = "uid-stats-array-length"; private static final String XML_TAG_EXTRAS = "extras"; /** * {@link BatteryConsumer.PowerComponent} (e.g. CPU, WIFI etc) that this snapshot relates * to; or a custom power component ID (if the value * is >= {@link BatteryConsumer#FIRST_CUSTOM_POWER_COMPONENT_ID}). */ public final int powerComponentId; public final String name; /** * Stats for the power component, such as the total usage time. */ public final int statsArrayLength; /** * Map of device state codes to their corresponding human-readable labels. */ public final SparseArray stateLabels; /** * Stats for a specific state of the power component, e.g. "mobile radio in the 5G mode" */ public final int stateStatsArrayLength; /** * Stats for the usage of this power component by a specific UID (app) */ public final int uidStatsArrayLength; /** * Extra parameters specific to the power component, e.g. the availability of power * monitors. */ public final PersistableBundle extras; private PowerStatsFormatter mDeviceStatsFormatter; private PowerStatsFormatter mStateStatsFormatter; private PowerStatsFormatter mUidStatsFormatter; public Descriptor(@BatteryConsumer.PowerComponent int powerComponentId, int statsArrayLength, @Nullable SparseArray stateLabels, int stateStatsArrayLength, int uidStatsArrayLength, @NonNull PersistableBundle extras) { this(powerComponentId, BatteryConsumer.powerComponentIdToString(powerComponentId), statsArrayLength, stateLabels, stateStatsArrayLength, uidStatsArrayLength, extras); } public Descriptor(int customPowerComponentId, String name, int statsArrayLength, @Nullable SparseArray stateLabels, int stateStatsArrayLength, int uidStatsArrayLength, @NonNull PersistableBundle extras) { if (statsArrayLength > MAX_STATS_ARRAY_LENGTH) { throw new IllegalArgumentException( "statsArrayLength is too high. Max = " + MAX_STATS_ARRAY_LENGTH); } if (stateStatsArrayLength > MAX_STATE_STATS_ARRAY_LENGTH) { throw new IllegalArgumentException( "stateStatsArrayLength is too high. Max = " + MAX_STATE_STATS_ARRAY_LENGTH); } if (uidStatsArrayLength > MAX_UID_STATS_ARRAY_LENGTH) { throw new IllegalArgumentException( "uidStatsArrayLength is too high. Max = " + MAX_UID_STATS_ARRAY_LENGTH); } this.powerComponentId = customPowerComponentId; this.name = name; this.statsArrayLength = statsArrayLength; this.stateLabels = stateLabels != null ? stateLabels : new SparseArray<>(); this.stateStatsArrayLength = stateStatsArrayLength; this.uidStatsArrayLength = uidStatsArrayLength; this.extras = extras; } /** * Returns a custom formatter for this type of power stats. */ public PowerStatsFormatter getDeviceStatsFormatter() { if (mDeviceStatsFormatter == null) { mDeviceStatsFormatter = new PowerStatsFormatter( extras.getString(EXTRA_DEVICE_STATS_FORMAT)); } return mDeviceStatsFormatter; } /** * Returns a custom formatter for this type of power stats, specifically per-state stats. */ public PowerStatsFormatter getStateStatsFormatter() { if (mStateStatsFormatter == null) { mStateStatsFormatter = new PowerStatsFormatter( extras.getString(EXTRA_STATE_STATS_FORMAT)); } return mStateStatsFormatter; } /** * Returns a custom formatter for this type of power stats, specifically per-UID stats. */ public PowerStatsFormatter getUidStatsFormatter() { if (mUidStatsFormatter == null) { mUidStatsFormatter = new PowerStatsFormatter( extras.getString(EXTRA_UID_STATS_FORMAT)); } return mUidStatsFormatter; } /** * Returns the label associated with the give state key, e.g. "5G-high" for the * state of Mobile Radio representing the 5G mode and high signal power. */ public String getStateLabel(int key) { String label = stateLabels.get(key); if (label != null) { return label; } return name + "-" + Integer.toHexString(key); } /** * Writes the Descriptor into the parcel. */ public void writeSummaryToParcel(Parcel parcel) { int firstWord = ((PARCEL_FORMAT_VERSION << PARCEL_FORMAT_VERSION_SHIFT) & PARCEL_FORMAT_VERSION_MASK) | ((statsArrayLength << STATS_ARRAY_LENGTH_SHIFT) & STATS_ARRAY_LENGTH_MASK) | ((stateStatsArrayLength << STATE_STATS_ARRAY_LENGTH_SHIFT) & STATE_STATS_ARRAY_LENGTH_MASK) | ((uidStatsArrayLength << UID_STATS_ARRAY_LENGTH_SHIFT) & UID_STATS_ARRAY_LENGTH_MASK); parcel.writeInt(firstWord); parcel.writeInt(powerComponentId); parcel.writeString(name); parcel.writeInt(stateLabels.size()); for (int i = 0, size = stateLabels.size(); i < size; i++) { parcel.writeInt(stateLabels.keyAt(i)); parcel.writeString(stateLabels.valueAt(i)); } extras.writeToParcel(parcel, 0); } /** * Reads a Descriptor from the parcel. If the parcel has an incompatible format, * returns null. */ @Nullable public static Descriptor readSummaryFromParcel(Parcel parcel) { int firstWord = parcel.readInt(); int version = (firstWord & PARCEL_FORMAT_VERSION_MASK) >>> PARCEL_FORMAT_VERSION_SHIFT; if (version != PARCEL_FORMAT_VERSION) { Slog.w(TAG, "Cannot read PowerStats from Parcel - the parcel format version has " + "changed from " + version + " to " + PARCEL_FORMAT_VERSION); return null; } int statsArrayLength = (firstWord & STATS_ARRAY_LENGTH_MASK) >>> STATS_ARRAY_LENGTH_SHIFT; int stateStatsArrayLength = (firstWord & STATE_STATS_ARRAY_LENGTH_MASK) >>> STATE_STATS_ARRAY_LENGTH_SHIFT; int uidStatsArrayLength = (firstWord & UID_STATS_ARRAY_LENGTH_MASK) >>> UID_STATS_ARRAY_LENGTH_SHIFT; int powerComponentId = parcel.readInt(); String name = parcel.readString(); int stateLabelCount = parcel.readInt(); SparseArray stateLabels = new SparseArray<>(stateLabelCount); for (int i = stateLabelCount; i > 0; i--) { int key = parcel.readInt(); String label = parcel.readString(); stateLabels.put(key, label); } PersistableBundle extras = parcel.readPersistableBundle(); return new Descriptor(powerComponentId, name, statsArrayLength, stateLabels, stateStatsArrayLength, uidStatsArrayLength, extras); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Descriptor)) return false; Descriptor that = (Descriptor) o; return powerComponentId == that.powerComponentId && statsArrayLength == that.statsArrayLength && stateLabels.contentEquals(that.stateLabels) && stateStatsArrayLength == that.stateStatsArrayLength && uidStatsArrayLength == that.uidStatsArrayLength && Objects.equals(name, that.name) && extras.size() == that.extras.size() // Unparcel the Parcel if not yet && Bundle.kindofEquals(extras, that.extras); // Since the Parcel is now unparceled, do a deep comparison } /** * Stores contents in an XML doc. */ public void writeXml(TypedXmlSerializer serializer) throws IOException { serializer.startTag(null, XML_TAG_DESCRIPTOR); serializer.attributeInt(null, XML_ATTR_ID, powerComponentId); serializer.attribute(null, XML_ATTR_NAME, name); serializer.attributeInt(null, XML_ATTR_STATS_ARRAY_LENGTH, statsArrayLength); serializer.attributeInt(null, XML_ATTR_STATE_STATS_ARRAY_LENGTH, stateStatsArrayLength); serializer.attributeInt(null, XML_ATTR_UID_STATS_ARRAY_LENGTH, uidStatsArrayLength); for (int i = stateLabels.size() - 1; i >= 0; i--) { serializer.startTag(null, XML_TAG_STATE); serializer.attributeInt(null, XML_ATTR_STATE_KEY, stateLabels.keyAt(i)); serializer.attribute(null, XML_ATTR_STATE_LABEL, stateLabels.valueAt(i)); serializer.endTag(null, XML_TAG_STATE); } try { serializer.startTag(null, XML_TAG_EXTRAS); extras.saveToXml(serializer); serializer.endTag(null, XML_TAG_EXTRAS); } catch (XmlPullParserException e) { throw new IOException(e); } serializer.endTag(null, XML_TAG_DESCRIPTOR); } /** * Creates a Descriptor by parsing an XML doc. The parser is expected to be positioned * on or before the opening "descriptor" tag. */ public static Descriptor createFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { int powerComponentId = -1; String name = null; int statsArrayLength = 0; SparseArray stateLabels = new SparseArray<>(); int stateStatsArrayLength = 0; int uidStatsArrayLength = 0; PersistableBundle extras = null; int eventType = parser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT && !(eventType == XmlPullParser.END_TAG && parser.getName().equals(XML_TAG_DESCRIPTOR))) { if (eventType == XmlPullParser.START_TAG) { switch (parser.getName()) { case XML_TAG_DESCRIPTOR: powerComponentId = parser.getAttributeInt(null, XML_ATTR_ID); name = parser.getAttributeValue(null, XML_ATTR_NAME); statsArrayLength = parser.getAttributeInt(null, XML_ATTR_STATS_ARRAY_LENGTH); stateStatsArrayLength = parser.getAttributeInt(null, XML_ATTR_STATE_STATS_ARRAY_LENGTH); uidStatsArrayLength = parser.getAttributeInt(null, XML_ATTR_UID_STATS_ARRAY_LENGTH); break; case XML_TAG_STATE: int value = parser.getAttributeInt(null, XML_ATTR_STATE_KEY); String label = parser.getAttributeValue(null, XML_ATTR_STATE_LABEL); stateLabels.put(value, label); break; case XML_TAG_EXTRAS: extras = PersistableBundle.restoreFromXml(parser); break; } } eventType = parser.next(); } if (powerComponentId == -1) { return null; } else if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { return new Descriptor(powerComponentId, name, statsArrayLength, stateLabels, stateStatsArrayLength, uidStatsArrayLength, extras); } else if (powerComponentId < BatteryConsumer.POWER_COMPONENT_COUNT) { return new Descriptor(powerComponentId, statsArrayLength, stateLabels, stateStatsArrayLength, uidStatsArrayLength, extras); } else { Slog.e(TAG, "Unrecognized power component: " + powerComponentId); return null; } } @Override public int hashCode() { return Objects.hash(powerComponentId); } @Override public String toString() { if (extras != null) { extras.size(); // Unparcel } return "PowerStats.Descriptor{" + "powerComponentId=" + powerComponentId + ", name='" + name + '\'' + ", statsArrayLength=" + statsArrayLength + ", stateStatsArrayLength=" + stateStatsArrayLength + ", stateLabels=" + stateLabels + ", uidStatsArrayLength=" + uidStatsArrayLength + ", extras=" + extras + '}'; } } /** * A registry for all supported power component types (e.g. CPU, WiFi). */ public static class DescriptorRegistry { private final SparseArray mDescriptors = new SparseArray<>(); /** * Adds the specified descriptor to the registry. If the registry already * contained a descriptor for the same power component, then the new one replaces * the old one. */ public void register(Descriptor descriptor) { mDescriptors.put(descriptor.powerComponentId, descriptor); } /** * @param powerComponentId either a BatteryConsumer.PowerComponent or a custom power * component ID */ public Descriptor get(int powerComponentId) { return mDescriptors.get(powerComponentId); } } public final Descriptor descriptor; /** * Duration, in milliseconds, covered by this snapshot. */ public long durationMs; /** * Device-wide stats. */ public long[] stats; /** * Device-wide mode stats, used when the power component can operate in different modes, * e.g. RATs such as LTE and 5G. */ public final SparseArray stateStats = new SparseArray<>(); /** * Per-UID CPU stats. */ public final SparseArray uidStats = new SparseArray<>(); public PowerStats(Descriptor descriptor) { this.descriptor = descriptor; stats = new long[descriptor.statsArrayLength]; } /** * Writes the object into the parcel. */ public void writeToParcel(Parcel parcel) { int lengthPos = parcel.dataPosition(); parcel.writeInt(0); // Placeholder for length int startPos = parcel.dataPosition(); parcel.writeInt(descriptor.powerComponentId); parcel.writeLong(durationMs); VARINT_PARCELER.writeLongArray(parcel, stats); if (descriptor.stateStatsArrayLength != 0) { parcel.writeInt(stateStats.size()); for (int i = 0; i < stateStats.size(); i++) { parcel.writeInt(stateStats.keyAt(i)); VARINT_PARCELER.writeLongArray(parcel, stateStats.valueAt(i)); } } parcel.writeInt(uidStats.size()); for (int i = 0; i < uidStats.size(); i++) { parcel.writeInt(uidStats.keyAt(i)); VARINT_PARCELER.writeLongArray(parcel, uidStats.valueAt(i)); } int endPos = parcel.dataPosition(); parcel.setDataPosition(lengthPos); parcel.writeInt(endPos - startPos); parcel.setDataPosition(endPos); } /** * Reads a PowerStats object from the supplied Parcel. If the parcel has an incompatible * format, returns null. */ @Nullable public static PowerStats readFromParcel(Parcel parcel, DescriptorRegistry registry) { int length = parcel.readInt(); int startPos = parcel.dataPosition(); int endPos = startPos + length; try { int powerComponentId = parcel.readInt(); Descriptor descriptor = registry.get(powerComponentId); if (descriptor == null) { Slog.e(TAG, "Unsupported PowerStats for power component ID: " + powerComponentId); return null; } PowerStats stats = new PowerStats(descriptor); stats.durationMs = parcel.readLong(); stats.stats = new long[descriptor.statsArrayLength]; VARINT_PARCELER.readLongArray(parcel, stats.stats); if (descriptor.stateStatsArrayLength != 0) { int count = parcel.readInt(); for (int i = 0; i < count; i++) { int state = parcel.readInt(); long[] stateStats = new long[descriptor.stateStatsArrayLength]; VARINT_PARCELER.readLongArray(parcel, stateStats); stats.stateStats.put(state, stateStats); } } int uidCount = parcel.readInt(); for (int i = 0; i < uidCount; i++) { int uid = parcel.readInt(); long[] uidStats = new long[descriptor.uidStatsArrayLength]; VARINT_PARCELER.readLongArray(parcel, uidStats); stats.uidStats.put(uid, uidStats); } if (parcel.dataPosition() != endPos) { Slog.e(TAG, "Corrupted PowerStats parcel. Expected length: " + length + ", actual length: " + (parcel.dataPosition() - startPos)); return null; } return stats; } finally { // Unconditionally skip to the end of the written data, even if the actual parcel // format is incompatible if (endPos > parcel.dataPosition()) { if (endPos >= parcel.dataSize()) { throw new IndexOutOfBoundsException( "PowerStats end position: " + endPos + " is outside the parcel bounds: " + parcel.dataSize()); } parcel.setDataPosition(endPos); } } } /** * Formats the stats as a string suitable to be included in the Battery History dump. */ public String formatForBatteryHistory(String uidPrefix) { StringBuilder sb = new StringBuilder(); sb.append("duration=").append(durationMs).append(" ").append(descriptor.name); if (stats.length > 0) { sb.append("=").append(descriptor.getDeviceStatsFormatter().format(stats)); } if (descriptor.stateStatsArrayLength != 0) { PowerStatsFormatter formatter = descriptor.getStateStatsFormatter(); for (int i = 0; i < stateStats.size(); i++) { sb.append(" ("); sb.append(descriptor.getStateLabel(stateStats.keyAt(i))); sb.append(") "); sb.append(formatter.format(stateStats.valueAt(i))); } } PowerStatsFormatter uidStatsFormatter = descriptor.getUidStatsFormatter(); for (int i = 0; i < uidStats.size(); i++) { sb.append(uidPrefix) .append(UserHandle.formatUid(uidStats.keyAt(i))) .append(": ").append(uidStatsFormatter.format(uidStats.valueAt(i))); } return sb.toString(); } /** * Prints the contents of the stats snapshot. */ public void dump(IndentingPrintWriter pw) { pw.println(descriptor.name + " (" + descriptor.powerComponentId + ')'); pw.increaseIndent(); pw.print("duration", durationMs).println(); if (descriptor.statsArrayLength != 0) { pw.println(descriptor.getDeviceStatsFormatter().format(stats)); } if (descriptor.stateStatsArrayLength != 0) { PowerStatsFormatter formatter = descriptor.getStateStatsFormatter(); for (int i = 0; i < stateStats.size(); i++) { pw.print(" ("); pw.print(descriptor.getStateLabel(stateStats.keyAt(i))); pw.print(") "); pw.print(formatter.format(stateStats.valueAt(i))); pw.println(); } } PowerStatsFormatter uidStatsFormatter = descriptor.getUidStatsFormatter(); for (int i = 0; i < uidStats.size(); i++) { pw.print("UID "); pw.print(UserHandle.formatUid(uidStats.keyAt(i))); pw.print(": "); pw.print(uidStatsFormatter.format(uidStats.valueAt(i))); pw.println(); } pw.decreaseIndent(); } @Override public String toString() { return "PowerStats: " + formatForBatteryHistory(" UID "); } public static class PowerStatsFormatter { private static class Section { public String label; public int position; public int length; public boolean optional; public boolean typePower; } private static final double NANO_TO_MILLI_MULTIPLIER = 1.0 / 1000000.0; private static final Pattern SECTION_PATTERN = Pattern.compile("([^:]+):(\\d+)(\\[(?\\d+)])?(?\\S*)\\s*"); private final List
mSections; public PowerStatsFormatter(String format) { mSections = parseFormat(format); } /** * Produces a formatted string representing the supplied array, with labels * and other adornments specific to the power stats layout. */ public String format(long[] stats) { return format(mSections, stats); } private List
parseFormat(String format) { if (format == null || format.isBlank()) { return null; } ArrayList
sections = new ArrayList<>(); Matcher matcher = SECTION_PATTERN.matcher(format); for (int position = 0; position < format.length(); position = matcher.end()) { if (!matcher.find() || matcher.start() != position) { Slog.wtf(TAG, "Bad power stats format '" + format + "'"); return null; } Section section = new Section(); section.label = matcher.group(1); section.position = Integer.parseUnsignedInt(matcher.group(2)); String length = matcher.group("L"); if (length != null) { section.length = Integer.parseUnsignedInt(length); } else { section.length = 1; } String flags = matcher.group("F"); if (flags != null) { for (int i = 0; i < flags.length(); i++) { char flag = flags.charAt(i); switch (flag) { case '?': section.optional = true; break; case 'p': section.typePower = true; break; default: Slog.e(TAG, "Unsupported format option '" + flag + "' in " + format); break; } } } sections.add(section); } return sections; } private String format(List
sections, long[] stats) { if (sections == null) { return Arrays.toString(stats); } StringBuilder sb = new StringBuilder(); for (int i = 0, count = sections.size(); i < count; i++) { Section section = sections.get(i); if (section.length == 0) { continue; } if (section.optional) { boolean nonZero = false; for (int offset = 0; offset < section.length; offset++) { if (stats[section.position + offset] != 0) { nonZero = true; break; } } if (!nonZero) { continue; } } if (!sb.isEmpty()) { sb.append(' '); } sb.append(section.label).append(": "); if (section.length != 1) { sb.append('['); } for (int offset = 0; offset < section.length; offset++) { if (offset != 0) { sb.append(", "); } if (section.typePower) { sb.append(BatteryStats.formatCharge( stats[section.position + offset] * NANO_TO_MILLI_MULTIPLIER)); } else { sb.append(stats[section.position + offset]); } } if (section.length != 1) { sb.append(']'); } } return sb.toString(); } } }