diff options
author | Joe Onorato <joeo@google.com> | 2018-11-16 16:15:31 -0800 |
---|---|---|
committer | Joe Onorato <joeo@google.com> | 2018-11-28 13:01:47 -0800 |
commit | a3f265fa2e28a2ea1f4fc0e7eeea64bbb1423028 (patch) | |
tree | ae107465017db127872ecae4398ce14fdc650a84 /tools/powermodel | |
parent | 7d1851fe49b2f923bb33f2a2db2f0baeb38b413b (diff) |
Class to parse the raw batterystats csv.
This parser builds a set of objects from the csv by looking
at the annotations on the fields. Each line in the csv
corresponds to a Record object.
Test: atest frameworks/base/tools/powermodel --host
Change-Id: Ifeae68ce3bc3a6ea9330ff924204f016bff20663
Diffstat (limited to 'tools/powermodel')
-rw-r--r-- | tools/powermodel/src/com/android/powermodel/RawBatteryStats.java | 887 | ||||
-rw-r--r-- | tools/powermodel/test/com/android/powermodel/RawBatteryStatsTest.java | 96 |
2 files changed, 983 insertions, 0 deletions
diff --git a/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java b/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java new file mode 100644 index 000000000000..28004f50ee8d --- /dev/null +++ b/tools/powermodel/src/com/android/powermodel/RawBatteryStats.java @@ -0,0 +1,887 @@ +/* + * Copyright (C) 2018 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.powermodel; + +import java.io.InputStream; +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +public class RawBatteryStats { + /** + * The factory objects for the records. Initialized in the static block. + */ + private static HashMap<String,RecordFactory> sFactories + = new HashMap<String,RecordFactory>(); + + /** + * The Record objects that have been parsed. + */ + private ArrayList<Record> mRecords = new ArrayList<Record>(); + + /** + * The Record objects that have been parsed, indexed by type. + * + * Don't use this before {@link #indexRecords()} has been called. + */ + private ImmutableMap<String,ImmutableList<Record>> mRecordsByType; + + /** + * The warnings that have been issued during parsing. + */ + private ArrayList<Warning> mWarnings = new ArrayList<Warning>(); + + /** + * The version of the BatteryStats dumpsys that we are using. This value + * is set to -1 initially, and then when parsing the (hopefully) first + * line, 'vers', it is set to the correct version. + */ + private int mDumpsysVersion = -1; + + /** + * Enum used in the Line annotation to mark whether a field is expected to be + * system-wide or scoped to an app. + */ + public enum Scope { + SYSTEM, + UID + } + + /** + * Enum used to indicated the expected number of results. + */ + public enum Count { + SINGLE, + MULTIPLE + } + + /** + * Annotates classes that represent a line of CSV in the batterystats CSV + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Line { + String tag(); + Scope scope(); + Count count(); + } + + /** + * Annotates fields that should be parsed automatically from CSV + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Field { + /** + * The "column" of this field in the most recent version of the CSV. + * When parsing old versions, fields that were added will be automatically + * removed and the indices will be fixed up. + * + * The header fields (version, uid, category, type) will be automatically + * handled for the base Line type. The index 0 should start after those. + */ + int index(); + + /** + * First version that this field appears in. + */ + int added() default 0; + } + + /** + * Each line in the BatteryStats CSV is tagged with a category, that says + * which of the time collection modes was used for the data. + */ + public enum Category { + INFO("i"), + LAST("l"), + UNPLUGGED("u"), + CURRENT("c"); + + public final String tag; + + Category(String tag) { + this.tag = tag; + } + } + + /** + * Base class for all lines in a batterystats CSV file. + */ + public static class Record { + /** + * Whether all of the fields for the indicated version of this record + * have been filled in. + */ + public boolean complete; + + + @Field(index=-4) + public int lineVersion; + + @Field(index=-3) + public int uid; + + @Field(index=-2) + public Category category; + + @Field(index=-1) + public String lineType; + } + + @Line(tag="gmcd", scope=Scope.SYSTEM, count=Count.SINGLE) + public static class GlobalModemController extends Record { + @Field(index=0) + public long idleMs; + + @Field(index=1) + public long rxTimeMs; + + @Field(index=2) + public long powerMaMs; + + @Field(index=3) + public long[] txTimeMs; + } + + @Line(tag="uid", scope=Scope.UID, count=Count.MULTIPLE) + public static class Uid extends Record { + @Field(index=0) + public int uidKey; + + @Field(index=1) + public String pkg; + } + + @Line(tag="vers", scope=Scope.SYSTEM, count=Count.SINGLE) + public static class Version extends Record { + @Field(index=0) + public int dumpsysVersion; + + @Field(index=1) + public int parcelVersion; + + @Field(index=2) + public String startPlatformVersion; + + @Field(index=3) + public String endPlatformVersion; + } + + /** + * Codes for the warnings to classify warnings without parsing them. + */ + public enum WarningId { + /** + * A row didn't have enough fields to match any records and let us extract + * a line type. + */ + TOO_FEW_FIELDS_FOR_LINE_TYPE, + + /** + * We couldn't find a Record for the given line type. + */ + NO_MATCHING_LINE_TYPE, + + /** + * Couldn't set the value of a field. Usually this is because the + * contents of a numeric type couldn't be parsed. + */ + BAD_FIELD_TYPE, + + /** + * There were extra field values in the input text. + */ + TOO_MANY_FIELDS, + + /** + * There were fields that we were expecting (for this version + * of the dumpsys) that weren't provided in the CSV data. + */ + NOT_ENOUGH_FIELDS, + + /** + * The dumpsys version in the 'vers' CSV line couldn't be parsed. + */ + BAD_DUMPSYS_VERSION + } + + /** + * A non-fatal problem detected during parsing. + */ + public static class Warning { + private int mLineNumber; + private WarningId mId; + private ArrayList<String> mFields; + private String mMessage; + private String[] mExtras; + + public Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message, + String[] extras) { + mLineNumber = lineNumber; + mId = id; + mFields = fields; + mMessage = message; + mExtras = extras; + } + + public int getLineNumber() { + return mLineNumber; + } + + public ArrayList<String> getFields() { + return mFields; + } + + public String getMessage() { + return mMessage; + } + + public String[] getExtras() { + return mExtras; + } + } + + /** + * Base class for classes to set fields on Record objects via reflection. + */ + private abstract static class FieldSetter { + private int mIndex; + private int mAdded; + protected java.lang.reflect.Field mField; + + FieldSetter(int index, int added, java.lang.reflect.Field field) { + mIndex = index; + mAdded = added; + mField = field; + } + + String getName() { + return mField.getName(); + } + + int getIndex() { + return mIndex; + } + + int getAdded() { + return mAdded; + } + + boolean isArray() { + return mField.getType().isArray(); + } + + abstract void setField(int lineNumber, Record object, String value) throws ParseException; + abstract void setArray(int lineNumber, Record object, ArrayList<String> values, + int startIndex, int endIndex) throws ParseException; + + @Override + public String toString() { + final String className = getClass().getName(); + int startIndex = Math.max(className.lastIndexOf('.'), className.lastIndexOf('$')); + if (startIndex < 0) { + startIndex = 0; + } else { + startIndex++; + } + return className.substring(startIndex) + "(index=" + mIndex + " added=" + mAdded + + " field=" + mField.getName() + + " type=" + mField.getType().getSimpleName() + + ")"; + } + } + + /** + * Sets int fields on Record objects using reflection. + */ + private static class IntFieldSetter extends FieldSetter { + IntFieldSetter(int index, int added, java.lang.reflect.Field field) { + super(index, added, field); + } + + void setField(int lineNumber, Record object, String value) throws ParseException { + try { + mField.setInt(object, Integer.parseInt(value.trim())); + } catch (NumberFormatException ex) { + throw new ParseException(lineNumber, "can't parse as integer: " + value); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + + void setArray(int lineNumber, Record object, ArrayList<String> values, + int startIndex, int endIndex) throws ParseException { + try { + final int[] array = new int[endIndex-startIndex]; + for (int i=startIndex; i<endIndex; i++) { + final String value = values.get(startIndex+i); + try { + array[i] = Integer.parseInt(value.trim()); + } catch (NumberFormatException ex) { + throw new ParseException(lineNumber, "can't parse field " + + i + " as integer: " + value); + } + } + mField.set(object, array); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Sets long fields on Record objects using reflection. + */ + private static class LongFieldSetter extends FieldSetter { + LongFieldSetter(int index, int added, java.lang.reflect.Field field) { + super(index, added, field); + } + + void setField(int lineNumber, Record object, String value) throws ParseException { + try { + mField.setLong(object, Long.parseLong(value.trim())); + } catch (NumberFormatException ex) { + throw new ParseException(lineNumber, "can't parse as long: " + value); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + + void setArray(int lineNumber, Record object, ArrayList<String> values, + int startIndex, int endIndex) throws ParseException { + try { + final long[] array = new long[endIndex-startIndex]; + for (int i=0; i<(endIndex-startIndex); i++) { + final String value = values.get(startIndex+i); + try { + array[i] = Long.parseLong(value.trim()); + } catch (NumberFormatException ex) { + throw new ParseException(lineNumber, "can't parse field " + + i + " as long: " + value); + } + } + mField.set(object, array); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Sets String fields on Record objects using reflection. + */ + private static class StringFieldSetter extends FieldSetter { + StringFieldSetter(int index, int added, java.lang.reflect.Field field) { + super(index, added, field); + } + + void setField(int lineNumber, Record object, String value) throws ParseException { + try { + mField.set(object, value); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + + void setArray(int lineNumber, Record object, ArrayList<String> values, + int startIndex, int endIndex) throws ParseException { + try { + final String[] array = new String[endIndex-startIndex]; + for (int i=0; i<(endIndex-startIndex); i++) { + array[i] = values.get(startIndex+1); + } + mField.set(object, array); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Sets enum fields on Record objects using reflection. + * + * To be parsed automatically, enums must have a public final String tag + * field, which is the string that will appear in the csv for that enum value. + */ + private static class EnumFieldSetter extends FieldSetter { + private final HashMap<String,Enum> mTags = new HashMap<String,Enum>(); + + EnumFieldSetter(int index, int added, java.lang.reflect.Field field) { + super(index, added, field); + + // Build the mapping of tags to values. + final Class<?> fieldType = field.getType(); + + java.lang.reflect.Field tagField = null; + try { + tagField = fieldType.getField("tag"); + } catch (NoSuchFieldException ex) { + throw new RuntimeException("Missing tag field." + + " To be parsed automatically, enums must have" + + " a String field called tag. Enum class: " + fieldType.getName() + + " Containing class: " + field.getDeclaringClass().getName() + + " Field: " + field.getName()); + + } + if (!String.class.equals(tagField.getType())) { + throw new RuntimeException("Tag field is not string." + + " To be parsed automatically, enums must have" + + " a String field called tag. Enum class: " + fieldType.getName() + + " Containing class: " + field.getDeclaringClass().getName() + + " Field: " + field.getName() + + " Tag field type: " + tagField.getType().getName()); + } + + for (final Object enumValue: fieldType.getEnumConstants()) { + String tag = null; + try { + tag = (String)tagField.get(enumValue); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + mTags.put(tag, (Enum)enumValue); + } + } + + void setField(int lineNumber, Record object, String value) throws ParseException { + final Enum enumValue = mTags.get(value); + if (enumValue == null) { + throw new ParseException(lineNumber, "Could not find enum for field " + + getName() + " for tag: " + value); + } + try { + mField.set(object, enumValue); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + + void setArray(int lineNumber, Record object, ArrayList<String> values, + int startIndex, int endIndex) throws ParseException { + try { + final Object array = Array.newInstance(mField.getType().getComponentType(), + endIndex-startIndex); + for (int i=0; i<(endIndex-startIndex); i++) { + final String value = values.get(startIndex+i); + final Enum enumValue = mTags.get(value); + if (enumValue == null) { + throw new ParseException(lineNumber, "Could not find enum for field " + + getName() + " for tag: " + value); + } + Array.set(array, i, enumValue); + } + mField.set(object, array); + } catch (IllegalAccessException | IllegalArgumentException + | ExceptionInInitializerError ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Factory for the record classes. Uses reflection to create + * the fields. + */ + private static class RecordFactory { + private String mTag; + private Class<? extends Record> mSubclass; + private ArrayList<FieldSetter> mFieldSetters; + + RecordFactory(String tag, Class<? extends Record> subclass, + ArrayList<FieldSetter> fieldSetters) { + mTag = tag; + mSubclass = subclass; + mFieldSetters = fieldSetters; + } + + /** + * Create an object of one of the subclasses of Record, and fill + * in the fields marked with the Field annotation. + * + * @return a new Record with the fields filled in. If there are missing + * fields, the {@link Record.complete} field will be set to false. + */ + Record create(RawBatteryStats bs, int dumpsysVersion, int lineNumber, + ArrayList<String> fieldValues) { + final boolean debug = false; + Record record = null; + try { + if (debug) { + System.err.println("Creating object: " + mSubclass.getSimpleName()); + } + record = mSubclass.newInstance(); + } catch (IllegalAccessException | InstantiationException + | ExceptionInInitializerError | SecurityException ex) { + throw new RuntimeException("Exception creating " + mSubclass.getName() + + " for '" + mTag + "' record.", ex); + } + record.complete = true; + int fieldIndex = 0; + int setterIndex = 0; + while (fieldIndex < fieldValues.size() && setterIndex < mFieldSetters.size()) { + final FieldSetter setter = mFieldSetters.get(setterIndex); + + if (dumpsysVersion >= 0 && dumpsysVersion < setter.getAdded()) { + // The version being parsed doesn't have the field for this setter, + // so skip the setter but not the field. + setterIndex++; + continue; + } + + final String value = fieldValues.get(fieldIndex); + try { + if (debug) { + System.err.println(" setting field " + setter + " to: " + value); + } + if (setter.isArray()) { + setter.setArray(lineNumber, record, fieldValues, + fieldIndex, fieldValues.size()); + // The rest of the fields have been consumed. + fieldIndex = fieldValues.size(); + setterIndex = mFieldSetters.size(); + break; + } else { + setter.setField(lineNumber, record, value); + } + } catch (ParseException ex) { + bs.addWarning(lineNumber, WarningId.BAD_FIELD_TYPE, fieldValues, + ex.getMessage(), mTag, value); + record.complete = false; + } + + fieldIndex++; + setterIndex++; + } + + // If there are extra fields, this record is complete, there are just + // extra values, so we issue a warning but don't mark it incomplete. + if (fieldIndex < fieldValues.size()) { + bs.addWarning(lineNumber, WarningId.TOO_MANY_FIELDS, fieldValues, + "Line '" + mTag + "' has extra fields.", + mTag, Integer.toString(fieldIndex), Integer.toString(fieldValues.size())); + if (debug) { + for (int i=0; i<mFieldSetters.size(); i++) { + System.err.println(" setter: [" + i + "] " + mFieldSetters.get(i)); + } + } + } + + // If we have any fields that are missing, add a warning and return null. + for (; setterIndex < mFieldSetters.size(); setterIndex++) { + final FieldSetter setter = mFieldSetters.get(setterIndex); + if (dumpsysVersion >= 0 && dumpsysVersion >= setter.getAdded()) { + bs.addWarning(lineNumber, WarningId.NOT_ENOUGH_FIELDS, fieldValues, + "Line '" + mTag + "' missing field: index=" + setterIndex + + " name=" + setter.getName(), + mTag, Integer.toString(setterIndex)); + record.complete = false; + } + } + + return record; + } + } + + /** + * Parse the input stream and return a RawBatteryStats object. + */ + public static RawBatteryStats parse(InputStream input) throws ParseException, IOException { + final RawBatteryStats result = new RawBatteryStats(); + result.parseImpl(input); + return result; + } + + /** + * Get a record. + * <p> + * If multiple of that record are found, returns the first one. There will already + * have been a warning recorded if the count annotation did not match what was in the + * csv. + * <p> + * Returns null if there are no records of that type. + */ + public <T extends Record> T getSingle(Class<T> cl) { + final List<Record> list = mRecordsByType.get(cl.getName()); + if (list == null) { + return null; + } + // Notes: + // - List can never be empty because the list itself wouldn't have been added. + // - Cast is safe because list was populated based on class name (let's assume + // there's only one class loader involved here). + return (T)list.get(0); + } + + /** + * Get a record. + * <p> + * If multiple of that record are found, returns the first one that matches that uid. + * <p> + * Returns null if there are no records of that type that match the given uid. + */ + public <T extends Record> T getSingle(Class<T> cl, int uid) { + final List<Record> list = mRecordsByType.get(cl.getName()); + if (list == null) { + return null; + } + for (final Record record: list) { + if (record.uid == uid) { + // Cast is safe because list was populated based on class name (let's assume + // there's only one class loader involved here). + return (T)record; + } + } + return null; + } + + /** + * Get all the records of the given type. + */ + public <T extends Record> List<T> getMultiple(Class<T> cl) { + final List<Record> list = mRecordsByType.get(cl.getName()); + if (list == null) { + return ImmutableList.<T>of(); + } + // Cast is safe because list was populated based on class name (let's assume + // there's only one class loader involved here). + return ImmutableList.copyOf((List<T>)list); + } + + /** + * No public constructor. Use {@link #parse}. + */ + private RawBatteryStats() { + } + + /** + * Get the list of Record objects that were parsed from the csv. + */ + public List<Record> getRecords() { + return mRecords; + } + + /** + * Gets the warnings that were encountered during parsing. + */ + public List<Warning> getWarnings() { + return mWarnings; + } + + /** + * Implementation of the csv parsing. + */ + private void parseImpl(InputStream input) throws ParseException, IOException { + // Parse the csv + CsvParser.parse(input, new CsvParser.LineProcessor() { + @Override + public void onLine(int lineNumber, ArrayList<String> fields) + throws ParseException { + handleCsvLine(lineNumber, fields); + } + }); + + // Gather the records by class name for the getSingle() and getMultiple() functions. + indexRecords(); + } + + /** + * Handle a line of CSV input, creating the right Record object. + */ + private void handleCsvLine(int lineNumber, ArrayList<String> fields) throws ParseException { + // The standard rows all have the 4 core fields. Anything less isn't what we're + // looking for. + if (fields.size() <= 4) { + addWarning(lineNumber, WarningId.TOO_FEW_FIELDS_FOR_LINE_TYPE, fields, + "Line with too few fields (" + fields.size() + ")", + Integer.toString(fields.size())); + return; + } + + final String lineType = fields.get(3); + + // Handle the vers line specially, because we need the version number + // to make the rest of the machinery work. + if ("vers".equals(lineType)) { + final String versionText = fields.get(4); + try { + mDumpsysVersion = Integer.parseInt(versionText); + } catch (NumberFormatException ex) { + addWarning(lineNumber, WarningId.BAD_DUMPSYS_VERSION, fields, + "Couldn't parse dumpsys version number: '" + versionText, + versionText); + } + } + + // Find the right factory. + final RecordFactory factory = sFactories.get(lineType); + if (factory == null) { + addWarning(lineNumber, WarningId.NO_MATCHING_LINE_TYPE, fields, + "No Record for line type '" + lineType + "'", + lineType); + return; + } + + // Create the record. + final Record record = factory.create(this, mDumpsysVersion, lineNumber, fields); + mRecords.add(record); + } + + /** + * Add to the list of warnings. + */ + private void addWarning(int lineNumber, WarningId id, + ArrayList<String> fields, String message, String... extras) { + mWarnings.add(new Warning(lineNumber, id, fields, message, extras)); + final boolean debug = false; + if (debug) { + final StringBuilder text = new StringBuilder("line "); + text.append(lineNumber); + text.append(": WARNING: "); + text.append(message); + text.append("\n fields: "); + for (int i=0; i<fields.size(); i++) { + final String field = fields.get(i); + if (field.indexOf('"') >= 0) { + text.append('"'); + text.append(field.replace("\"", "\"\"")); + text.append('"'); + } else { + text.append(field); + } + if (i != fields.size() - 1) { + text.append(','); + } + } + text.append('\n'); + for (String extra: extras) { + text.append(" extra: "); + text.append(extra); + text.append('\n'); + } + System.err.print(text.toString()); + } + } + + /** + * Group records by class name. + */ + private void indexRecords() { + final HashMap<String,ArrayList<Record>> map = new HashMap<String,ArrayList<Record>>(); + + // Iterate over all of the records + for (Record record: mRecords) { + final String className = record.getClass().getName(); + + ArrayList<Record> list = map.get(className); + if (list == null) { + list = new ArrayList<Record>(); + map.put(className, list); + } + + list.add(record); + } + + // Make it immutable + final HashMap<String,ImmutableList<Record>> result + = new HashMap<String,ImmutableList<Record>>(); + for (HashMap.Entry<String,ArrayList<Record>> entry: map.entrySet()) { + result.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + + // Initialize here so uninitialized access will result in NPE. + mRecordsByType = ImmutableMap.copyOf(result); + } + + /** + * Init the factory classes. + */ + static { + for (Class<?> cl: RawBatteryStats.class.getClasses()) { + final Line lineAnnotation = cl.getAnnotation(Line.class); + if (lineAnnotation != null && Record.class.isAssignableFrom(cl)) { + final ArrayList<FieldSetter> fieldSetters = new ArrayList<FieldSetter>(); + + for (java.lang.reflect.Field field: cl.getFields()) { + final Field fa = field.getAnnotation(Field.class); + if (fa != null) { + final Class<?> fieldType = field.getType(); + final Class<?> innerType = fieldType.isArray() + ? fieldType.getComponentType() + : fieldType; + if (Integer.TYPE.equals(innerType)) { + fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field)); + } else if (Long.TYPE.equals(innerType)) { + fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field)); + } else if (String.class.equals(innerType)) { + fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field)); + } else if (innerType.isEnum()) { + fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field)); + } else { + throw new RuntimeException("Unsupported field type '" + + fieldType.getName() + "' on " + + cl.getName() + "." + field.getName()); + } + } + } + // Sort by index + Collections.sort(fieldSetters, new Comparator<FieldSetter>() { + @Override + public int compare(FieldSetter a, FieldSetter b) { + return a.getIndex() - b.getIndex(); + } + }); + // Only the last one can be an array + for (int i=0; i<fieldSetters.size()-1; i++) { + if (fieldSetters.get(i).isArray()) { + throw new RuntimeException("Only the last (highest index) @Field" + + " in class " + cl.getName() + " can be an array: " + + fieldSetters.get(i).getName()); + } + } + // Add to the map + sFactories.put(lineAnnotation.tag(), new RecordFactory(lineAnnotation.tag(), + (Class<Record>)cl, fieldSetters)); + } + } + } +} + diff --git a/tools/powermodel/test/com/android/powermodel/RawBatteryStatsTest.java b/tools/powermodel/test/com/android/powermodel/RawBatteryStatsTest.java new file mode 100644 index 000000000000..fbcac41a9e1c --- /dev/null +++ b/tools/powermodel/test/com/android/powermodel/RawBatteryStatsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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.powermodel; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.Test; +import org.junit.Assert; + +/** + * Tests {@link RawBatteryStats}. + */ +public class RawBatteryStatsTest { + private static final int BS_VERSION = 32; + + private static InputStream makeCsv(String... lines) { + return makeCsv(BS_VERSION, lines); + } + + private static InputStream makeCsv(int version, String... lines) { + final StringBuilder result = new StringBuilder("9,0,i,vers,"); + result.append(version); + result.append(",177,PPR1.180326.002,PQ1A.181105.015\n"); + for (String line: lines) { + result.append(line); + result.append('\n'); + } + return new ByteArrayInputStream(result.toString().getBytes(StandardCharsets.UTF_8)); + } + + @Test public void testVersion() throws Exception { + final InputStream is = makeCsv(); + + final RawBatteryStats bs = RawBatteryStats.parse(is); + final List<RawBatteryStats.Record> records = bs.getRecords(); + final RawBatteryStats.Version line = (RawBatteryStats.Version)records.get(0); + + Assert.assertEquals(0, bs.getWarnings().size()); + Assert.assertEquals(true, line.complete); + + Assert.assertEquals(9, line.lineVersion); + Assert.assertEquals(0, line.uid); + Assert.assertEquals(RawBatteryStats.Category.INFO, line.category); + Assert.assertEquals("vers", line.lineType); + + Assert.assertEquals(BS_VERSION, line.dumpsysVersion); + Assert.assertEquals(177, line.parcelVersion); + Assert.assertEquals("PPR1.180326.002", line.startPlatformVersion); + Assert.assertEquals("PQ1A.181105.015", line.endPlatformVersion); + } + + @Test public void testUid() throws Exception { + final InputStream is = makeCsv("9,0,i,uid,1000,com.example.app"); + + final RawBatteryStats bs = RawBatteryStats.parse(is); + final List<RawBatteryStats.Record> records = bs.getRecords(); + final RawBatteryStats.Uid line = (RawBatteryStats.Uid)records.get(1); + + Assert.assertEquals(1000, line.uidKey); + Assert.assertEquals("com.example.app", line.pkg); + } + + @Test public void testVarargs() throws Exception { + final InputStream is = makeCsv("9,0,i,gmcd,1,2,3,4,5,6,7"); + + final RawBatteryStats bs = RawBatteryStats.parse(is); + final List<RawBatteryStats.Record> records = bs.getRecords(); + final RawBatteryStats.GlobalModemController line + = (RawBatteryStats.GlobalModemController)records.get(1); + + Assert.assertEquals(1, line.idleMs); + Assert.assertEquals(2, line.rxTimeMs); + Assert.assertEquals(3, line.powerMaMs); + Assert.assertEquals(4, line.txTimeMs.length); + Assert.assertEquals(4, line.txTimeMs[0]); + Assert.assertEquals(5, line.txTimeMs[1]); + Assert.assertEquals(6, line.txTimeMs[2]); + Assert.assertEquals(7, line.txTimeMs[3]); + } +} |