| /* |
| * 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<String> 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<String> 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<String> 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<String> 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<String> 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<Descriptor> 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<long[]> stateStats = new SparseArray<>(); |
| |
| /** |
| * Per-UID CPU stats. |
| */ |
| public final SparseArray<long[]> 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+)(\\[(?<L>\\d+)])?(?<F>\\S*)\\s*"); |
| private final List<Section> 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<Section> parseFormat(String format) { |
| if (format == null || format.isBlank()) { |
| return null; |
| } |
| |
| ArrayList<Section> 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<Section> 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(); |
| } |
| } |
| } |