1 /* 2 * Copyright (C) 2018 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.powermodel; 18 19 import java.io.InputStream; 20 import java.io.IOException; 21 import java.lang.annotation.ElementType; 22 import java.lang.annotation.Retention; 23 import java.lang.annotation.RetentionPolicy; 24 import java.lang.annotation.Target; 25 import java.lang.reflect.Array; 26 import java.lang.reflect.Field; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collections; 30 import java.util.Comparator; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.ImmutableMap; 38 import com.google.common.collect.ImmutableSet; 39 40 public class RawBatteryStats { 41 /** 42 * The factory objects for the records. Initialized in the static block. 43 */ 44 private static HashMap<String,RecordFactory> sFactories 45 = new HashMap<String,RecordFactory>(); 46 47 /** 48 * The Record objects that have been parsed. 49 */ 50 private ArrayList<Record> mRecords = new ArrayList<Record>(); 51 52 /** 53 * The Record objects that have been parsed, indexed by type. 54 * 55 * Don't use this before {@link #indexRecords()} has been called. 56 */ 57 private ImmutableMap<String,ImmutableList<Record>> mRecordsByType; 58 59 /** 60 * The attribution keys for which we have data (corresponding to UIDs we've seen). 61 * <p> 62 * Does not include the synthetic apps. 63 * <p> 64 * Don't use this before {@link #indexRecords()} has been called. 65 */ 66 private ImmutableSet<AttributionKey> mApps; 67 68 /** 69 * The warnings that have been issued during parsing. 70 */ 71 private ArrayList<Warning> mWarnings = new ArrayList<Warning>(); 72 73 /** 74 * The version of the BatteryStats dumpsys that we are using. This value 75 * is set to -1 initially, and then when parsing the (hopefully) first 76 * line, 'vers', it is set to the correct version. 77 */ 78 private int mDumpsysVersion = -1; 79 80 /** 81 * Enum used in the Line annotation to mark whether a field is expected to be 82 * system-wide or scoped to an app. 83 */ 84 public enum Scope { 85 SYSTEM, 86 UID 87 } 88 89 /** 90 * Enum used to indicated the expected number of results. 91 */ 92 public enum Count { 93 SINGLE, 94 MULTIPLE 95 } 96 97 /** 98 * Annotates classes that represent a line of CSV in the batterystats CSV 99 */ 100 @Retention(RetentionPolicy.RUNTIME) 101 @Target(ElementType.TYPE) 102 @interface Line { tag()103 String tag(); scope()104 Scope scope(); count()105 Count count(); 106 } 107 108 /** 109 * Annotates fields that should be parsed automatically from CSV 110 */ 111 @Retention(RetentionPolicy.RUNTIME) 112 @Target(ElementType.FIELD) 113 @interface Field { 114 /** 115 * The "column" of this field in the most recent version of the CSV. 116 * When parsing old versions, fields that were added will be automatically 117 * removed and the indices will be fixed up. 118 * 119 * The header fields (version, uid, category, type) will be automatically 120 * handled for the base Line type. The index 0 should start after those. 121 */ index()122 int index(); 123 124 /** 125 * First version that this field appears in. 126 */ added()127 int added() default 0; 128 } 129 130 /** 131 * Each line in the BatteryStats CSV is tagged with a category, that says 132 * which of the time collection modes was used for the data. 133 */ 134 public enum Category { 135 INFO("i"), 136 LAST("l"), 137 UNPLUGGED("u"), 138 CURRENT("c"); 139 140 public final String tag; 141 Category(String tag)142 Category(String tag) { 143 this.tag = tag; 144 } 145 } 146 147 /** 148 * Base class for all lines in a batterystats CSV file. 149 */ 150 public static class Record { 151 /** 152 * Whether all of the fields for the indicated version of this record 153 * have been filled in. 154 */ 155 public boolean complete; 156 157 158 @Field(index=-4) 159 public int lineVersion; 160 161 @Field(index=-3) 162 public int uid; 163 164 @Field(index=-2) 165 public Category category; 166 167 @Field(index=-1) 168 public String lineType; 169 } 170 171 @Line(tag="bt", scope=Scope.SYSTEM, count=Count.SINGLE) 172 public static class Battery extends Record { 173 // If which != STATS_SINCE_CHARGED, the csv will be "N/A" and we will get 174 // a parsing warning. Nobody uses anything other than STATS_SINCE_CHARGED. 175 @Field(index=0) 176 public int startCount; 177 178 @Field(index=1) 179 public long whichBatteryRealtimeMs; 180 181 @Field(index=2) 182 public long whichBatteryUptimeMs; 183 184 @Field(index=3) 185 public long totalRealtimeMs; 186 187 @Field(index=4) 188 public long totalUptimeMs; 189 190 @Field(index=5) 191 public long getStartClockTimeMs; 192 193 @Field(index=6) 194 public long whichBatteryScreenOffRealtimeMs; 195 196 @Field(index=7) 197 public long whichBatteryScreenOffUptimeMs; 198 199 @Field(index=8) 200 public long estimatedBatteryCapacityMah; 201 202 @Field(index=9) 203 public long minLearnedBatteryCapacityMah; 204 205 @Field(index=10) 206 public long maxLearnedBatteryCapacityMah; 207 208 @Field(index=11) 209 public long screenDozeTimeMs; 210 } 211 212 @Line(tag="gn", scope=Scope.SYSTEM, count=Count.SINGLE) 213 public static class GlobalNetwork extends Record { 214 @Field(index=0) 215 public long mobileRxTotalBytes; 216 217 @Field(index=1) 218 public long mobileTxTotalBytes; 219 220 @Field(index=2) 221 public long wifiRxTotalBytes; 222 223 @Field(index=3) 224 public long wifiTxTotalBytes; 225 226 @Field(index=4) 227 public long mobileRxTotalPackets; 228 229 @Field(index=5) 230 public long mobileTxTotalPackets; 231 232 @Field(index=6) 233 public long wifiRxTotalPackets; 234 235 @Field(index=7) 236 public long wifiTxTotalPackets; 237 238 @Field(index=8) 239 public long btRxTotalBytes; 240 241 @Field(index=9) 242 public long btTxTotalBytes; 243 } 244 245 @Line(tag="gmcd", scope=Scope.SYSTEM, count=Count.SINGLE) 246 public static class GlobalModemController extends Record { 247 @Field(index=0) 248 public long idleMs; 249 250 @Field(index=1) 251 public long rxTimeMs; 252 253 @Field(index=2) 254 public long powerMaMs; 255 256 @Field(index=3) 257 public long[] txTimeMs; 258 } 259 260 @Line(tag="m", scope=Scope.SYSTEM, count=Count.SINGLE) 261 public static class Misc extends Record { 262 @Field(index=0) 263 public long screenOnTimeMs; 264 265 @Field(index=1) 266 public long phoneOnTimeMs; 267 268 @Field(index=2) 269 public long fullWakeLockTimeTotalMs; 270 271 @Field(index=3) 272 public long partialWakeLockTimeTotalMs; 273 274 @Field(index=4) 275 public long mobileRadioActiveTimeMs; 276 277 @Field(index=5) 278 public long mobileRadioActiveAdjustedTimeMs; 279 280 @Field(index=6) 281 public long interactiveTimeMs; 282 283 @Field(index=7) 284 public long powerSaveModeEnabledTimeMs; 285 286 @Field(index=8) 287 public int connectivityChangeCount; 288 289 @Field(index=9) 290 public long deepDeviceIdleModeTimeMs; 291 292 @Field(index=10) 293 public long deepDeviceIdleModeCount; 294 295 @Field(index=11) 296 public long deepDeviceIdlingTimeMs; 297 298 @Field(index=12) 299 public long deepDeviceIdlingCount; 300 301 @Field(index=13) 302 public long mobileRadioActiveCount; 303 304 @Field(index=14) 305 public long mobileRadioActiveUnknownTimeMs; 306 307 @Field(index=15) 308 public long lightDeviceIdleModeTimeMs; 309 310 @Field(index=16) 311 public long lightDeviceIdleModeCount; 312 313 @Field(index=17) 314 public long lightDeviceIdlingTimeMs; 315 316 @Field(index=18) 317 public long lightDeviceIdlingCount; 318 319 @Field(index=19) 320 public long lightLongestDeviceIdleModeTimeMs; 321 322 @Field(index=20) 323 public long deepLongestDeviceIdleModeTimeMs; 324 } 325 326 @Line(tag="nt", scope=Scope.UID, count=Count.SINGLE) 327 public static class Network extends Record { 328 @Field(index=0) 329 public long mobileRxBytes; 330 331 @Field(index=1) 332 public long mobileTxBytes; 333 334 @Field(index=2) 335 public long wifiRxBytes; 336 337 @Field(index=3) 338 public long wifiTxBytes; 339 340 @Field(index=4) 341 public long mobileRxPackets; 342 343 @Field(index=5) 344 public long mobileTxPackets; 345 346 @Field(index=6) 347 public long wifiRxPackets; 348 349 @Field(index=7) 350 public long wifiTxPackets; 351 352 // This is microseconds, because... batterystats. 353 @Field(index=8) 354 public long mobileRadioActiveTimeUs; 355 356 @Field(index=9) 357 public long mobileRadioActiveCount; 358 359 @Field(index=10) 360 public long btRxBytes; 361 362 @Field(index=11) 363 public long btTxBytes; 364 365 @Field(index=12) 366 public long mobileWakeupCount; 367 368 @Field(index=13) 369 public long wifiWakeupCount; 370 371 @Field(index=14) 372 public long mobileBgRxBytes; 373 374 @Field(index=15) 375 public long mobileBgTxBytes; 376 377 @Field(index=16) 378 public long wifiBgRxBytes; 379 380 @Field(index=17) 381 public long wifiBgTxBytes; 382 383 @Field(index=18) 384 public long mobileBgRxPackets; 385 386 @Field(index=19) 387 public long mobileBgTxPackets; 388 389 @Field(index=20) 390 public long wifiBgRxPackets; 391 392 @Field(index=21) 393 public long wifiBgTxPackets; 394 } 395 396 @Line(tag="sgt", scope=Scope.SYSTEM, count=Count.SINGLE) 397 public static class SignalStrengthTime extends Record { 398 @Field(index=0) 399 public long[] phoneSignalStrengthTimeMs; 400 } 401 402 @Line(tag="sst", scope=Scope.SYSTEM, count=Count.SINGLE) 403 public static class SignalScanningTime extends Record { 404 @Field(index=0) 405 public long phoneSignalScanningTimeMs; 406 } 407 408 @Line(tag="uid", scope=Scope.UID, count=Count.MULTIPLE) 409 public static class Uid extends Record { 410 @Field(index=0) 411 public int uidKey; 412 413 @Field(index=1) 414 public String pkg; 415 } 416 417 @Line(tag="vers", scope=Scope.SYSTEM, count=Count.SINGLE) 418 public static class Version extends Record { 419 @Field(index=0) 420 public int dumpsysVersion; 421 422 @Field(index=1) 423 public int parcelVersion; 424 425 @Field(index=2) 426 public String startPlatformVersion; 427 428 @Field(index=3) 429 public String endPlatformVersion; 430 } 431 432 /** 433 * Codes for the warnings to classify warnings without parsing them. 434 */ 435 public enum WarningId { 436 /** 437 * A row didn't have enough fields to match any records and let us extract 438 * a line type. 439 */ 440 TOO_FEW_FIELDS_FOR_LINE_TYPE, 441 442 /** 443 * We couldn't find a Record for the given line type. 444 */ 445 NO_MATCHING_LINE_TYPE, 446 447 /** 448 * Couldn't set the value of a field. Usually this is because the 449 * contents of a numeric type couldn't be parsed. 450 */ 451 BAD_FIELD_TYPE, 452 453 /** 454 * There were extra field values in the input text. 455 */ 456 TOO_MANY_FIELDS, 457 458 /** 459 * There were fields that we were expecting (for this version 460 * of the dumpsys) that weren't provided in the CSV data. 461 */ 462 NOT_ENOUGH_FIELDS, 463 464 /** 465 * The dumpsys version in the 'vers' CSV line couldn't be parsed. 466 */ 467 BAD_DUMPSYS_VERSION 468 } 469 470 /** 471 * A non-fatal problem detected during parsing. 472 */ 473 public static class Warning { 474 private int mLineNumber; 475 private WarningId mId; 476 private ArrayList<String> mFields; 477 private String mMessage; 478 private String[] mExtras; 479 Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message, String[] extras)480 public Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message, 481 String[] extras) { 482 mLineNumber = lineNumber; 483 mId = id; 484 mFields = fields; 485 mMessage = message; 486 mExtras = extras; 487 } 488 getLineNumber()489 public int getLineNumber() { 490 return mLineNumber; 491 } 492 getFields()493 public ArrayList<String> getFields() { 494 return mFields; 495 } 496 getMessage()497 public String getMessage() { 498 return mMessage; 499 } 500 getExtras()501 public String[] getExtras() { 502 return mExtras; 503 } 504 } 505 506 /** 507 * Base class for classes to set fields on Record objects via reflection. 508 */ 509 private abstract static class FieldSetter { 510 private int mIndex; 511 private int mAdded; 512 protected java.lang.reflect.Field mField; 513 FieldSetter(int index, int added, java.lang.reflect.Field field)514 FieldSetter(int index, int added, java.lang.reflect.Field field) { 515 mIndex = index; 516 mAdded = added; 517 mField = field; 518 } 519 getName()520 String getName() { 521 return mField.getName(); 522 } 523 getIndex()524 int getIndex() { 525 return mIndex; 526 } 527 getAdded()528 int getAdded() { 529 return mAdded; 530 } 531 isArray()532 boolean isArray() { 533 return mField.getType().isArray(); 534 } 535 setField(int lineNumber, Record object, String value)536 abstract void setField(int lineNumber, Record object, String value) throws ParseException; setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)537 abstract void setArray(int lineNumber, Record object, ArrayList<String> values, 538 int startIndex, int endIndex) throws ParseException; 539 540 @Override toString()541 public String toString() { 542 final String className = getClass().getName(); 543 int startIndex = Math.max(className.lastIndexOf('.'), className.lastIndexOf('$')); 544 if (startIndex < 0) { 545 startIndex = 0; 546 } else { 547 startIndex++; 548 } 549 return className.substring(startIndex) + "(index=" + mIndex + " added=" + mAdded 550 + " field=" + mField.getName() 551 + " type=" + mField.getType().getSimpleName() 552 + ")"; 553 } 554 } 555 556 /** 557 * Sets int fields on Record objects using reflection. 558 */ 559 private static class IntFieldSetter extends FieldSetter { IntFieldSetter(int index, int added, java.lang.reflect.Field field)560 IntFieldSetter(int index, int added, java.lang.reflect.Field field) { 561 super(index, added, field); 562 } 563 setField(int lineNumber, Record object, String value)564 void setField(int lineNumber, Record object, String value) throws ParseException { 565 try { 566 mField.setInt(object, Integer.parseInt(value.trim())); 567 } catch (NumberFormatException ex) { 568 throw new ParseException(lineNumber, "can't parse as integer: " + value); 569 } catch (IllegalAccessException | IllegalArgumentException 570 | ExceptionInInitializerError ex) { 571 throw new RuntimeException(ex); 572 } 573 } 574 setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)575 void setArray(int lineNumber, Record object, ArrayList<String> values, 576 int startIndex, int endIndex) throws ParseException { 577 try { 578 final int[] array = new int[endIndex-startIndex]; 579 for (int i=startIndex; i<endIndex; i++) { 580 final String value = values.get(startIndex+i); 581 try { 582 array[i] = Integer.parseInt(value.trim()); 583 } catch (NumberFormatException ex) { 584 throw new ParseException(lineNumber, "can't parse field " 585 + i + " as integer: " + value); 586 } 587 } 588 mField.set(object, array); 589 } catch (IllegalAccessException | IllegalArgumentException 590 | ExceptionInInitializerError ex) { 591 throw new RuntimeException(ex); 592 } 593 } 594 } 595 596 /** 597 * Sets long fields on Record objects using reflection. 598 */ 599 private static class LongFieldSetter extends FieldSetter { LongFieldSetter(int index, int added, java.lang.reflect.Field field)600 LongFieldSetter(int index, int added, java.lang.reflect.Field field) { 601 super(index, added, field); 602 } 603 setField(int lineNumber, Record object, String value)604 void setField(int lineNumber, Record object, String value) throws ParseException { 605 try { 606 mField.setLong(object, Long.parseLong(value.trim())); 607 } catch (NumberFormatException ex) { 608 throw new ParseException(lineNumber, "can't parse as long: " + value); 609 } catch (IllegalAccessException | IllegalArgumentException 610 | ExceptionInInitializerError ex) { 611 throw new RuntimeException(ex); 612 } 613 } 614 setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)615 void setArray(int lineNumber, Record object, ArrayList<String> values, 616 int startIndex, int endIndex) throws ParseException { 617 try { 618 final long[] array = new long[endIndex-startIndex]; 619 for (int i=0; i<(endIndex-startIndex); i++) { 620 final String value = values.get(startIndex+i); 621 try { 622 array[i] = Long.parseLong(value.trim()); 623 } catch (NumberFormatException ex) { 624 throw new ParseException(lineNumber, "can't parse field " 625 + i + " as long: " + value); 626 } 627 } 628 mField.set(object, array); 629 } catch (IllegalAccessException | IllegalArgumentException 630 | ExceptionInInitializerError ex) { 631 throw new RuntimeException(ex); 632 } 633 } 634 } 635 636 /** 637 * Sets String fields on Record objects using reflection. 638 */ 639 private static class StringFieldSetter extends FieldSetter { StringFieldSetter(int index, int added, java.lang.reflect.Field field)640 StringFieldSetter(int index, int added, java.lang.reflect.Field field) { 641 super(index, added, field); 642 } 643 setField(int lineNumber, Record object, String value)644 void setField(int lineNumber, Record object, String value) throws ParseException { 645 try { 646 mField.set(object, value); 647 } catch (IllegalAccessException | IllegalArgumentException 648 | ExceptionInInitializerError ex) { 649 throw new RuntimeException(ex); 650 } 651 } 652 setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)653 void setArray(int lineNumber, Record object, ArrayList<String> values, 654 int startIndex, int endIndex) throws ParseException { 655 try { 656 final String[] array = new String[endIndex-startIndex]; 657 for (int i=0; i<(endIndex-startIndex); i++) { 658 array[i] = values.get(startIndex+1); 659 } 660 mField.set(object, array); 661 } catch (IllegalAccessException | IllegalArgumentException 662 | ExceptionInInitializerError ex) { 663 throw new RuntimeException(ex); 664 } 665 } 666 } 667 668 /** 669 * Sets enum fields on Record objects using reflection. 670 * 671 * To be parsed automatically, enums must have a public final String tag 672 * field, which is the string that will appear in the csv for that enum value. 673 */ 674 private static class EnumFieldSetter extends FieldSetter { 675 private final HashMap<String,Enum> mTags = new HashMap<String,Enum>(); 676 EnumFieldSetter(int index, int added, java.lang.reflect.Field field)677 EnumFieldSetter(int index, int added, java.lang.reflect.Field field) { 678 super(index, added, field); 679 680 // Build the mapping of tags to values. 681 final Class<?> fieldType = field.getType(); 682 683 java.lang.reflect.Field tagField = null; 684 try { 685 tagField = fieldType.getField("tag"); 686 } catch (NoSuchFieldException ex) { 687 throw new RuntimeException("Missing tag field." 688 + " To be parsed automatically, enums must have" 689 + " a String field called tag. Enum class: " + fieldType.getName() 690 + " Containing class: " + field.getDeclaringClass().getName() 691 + " Field: " + field.getName()); 692 693 } 694 if (!String.class.equals(tagField.getType())) { 695 throw new RuntimeException("Tag field is not string." 696 + " To be parsed automatically, enums must have" 697 + " a String field called tag. Enum class: " + fieldType.getName() 698 + " Containing class: " + field.getDeclaringClass().getName() 699 + " Field: " + field.getName() 700 + " Tag field type: " + tagField.getType().getName()); 701 } 702 703 for (final Object enumValue: fieldType.getEnumConstants()) { 704 String tag = null; 705 try { 706 tag = (String)tagField.get(enumValue); 707 } catch (IllegalAccessException | IllegalArgumentException 708 | ExceptionInInitializerError ex) { 709 throw new RuntimeException(ex); 710 } 711 mTags.put(tag, (Enum)enumValue); 712 } 713 } 714 setField(int lineNumber, Record object, String value)715 void setField(int lineNumber, Record object, String value) throws ParseException { 716 final Enum enumValue = mTags.get(value); 717 if (enumValue == null) { 718 throw new ParseException(lineNumber, "Could not find enum for field " 719 + getName() + " for tag: " + value); 720 } 721 try { 722 mField.set(object, enumValue); 723 } catch (IllegalAccessException | IllegalArgumentException 724 | ExceptionInInitializerError ex) { 725 throw new RuntimeException(ex); 726 } 727 } 728 setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)729 void setArray(int lineNumber, Record object, ArrayList<String> values, 730 int startIndex, int endIndex) throws ParseException { 731 try { 732 final Object array = Array.newInstance(mField.getType().getComponentType(), 733 endIndex-startIndex); 734 for (int i=0; i<(endIndex-startIndex); i++) { 735 final String value = values.get(startIndex+i); 736 final Enum enumValue = mTags.get(value); 737 if (enumValue == null) { 738 throw new ParseException(lineNumber, "Could not find enum for field " 739 + getName() + " for tag: " + value); 740 } 741 Array.set(array, i, enumValue); 742 } 743 mField.set(object, array); 744 } catch (IllegalAccessException | IllegalArgumentException 745 | ExceptionInInitializerError ex) { 746 throw new RuntimeException(ex); 747 } 748 } 749 } 750 751 /** 752 * Factory for the record classes. Uses reflection to create 753 * the fields. 754 */ 755 private static class RecordFactory { 756 private String mTag; 757 private Class<? extends Record> mSubclass; 758 private ArrayList<FieldSetter> mFieldSetters; 759 RecordFactory(String tag, Class<? extends Record> subclass, ArrayList<FieldSetter> fieldSetters)760 RecordFactory(String tag, Class<? extends Record> subclass, 761 ArrayList<FieldSetter> fieldSetters) { 762 mTag = tag; 763 mSubclass = subclass; 764 mFieldSetters = fieldSetters; 765 } 766 767 /** 768 * Create an object of one of the subclasses of Record, and fill 769 * in the fields marked with the Field annotation. 770 * 771 * @return a new Record with the fields filled in. If there are missing 772 * fields, the {@link Record.complete} field will be set to false. 773 */ create(RawBatteryStats bs, int dumpsysVersion, int lineNumber, ArrayList<String> fieldValues)774 Record create(RawBatteryStats bs, int dumpsysVersion, int lineNumber, 775 ArrayList<String> fieldValues) { 776 final boolean debug = false; 777 Record record = null; 778 try { 779 if (debug) { 780 System.err.println("Creating object: " + mSubclass.getSimpleName()); 781 } 782 record = mSubclass.newInstance(); 783 } catch (IllegalAccessException | InstantiationException 784 | ExceptionInInitializerError | SecurityException ex) { 785 throw new RuntimeException("Exception creating " + mSubclass.getName() 786 + " for '" + mTag + "' record.", ex); 787 } 788 record.complete = true; 789 int fieldIndex = 0; 790 int setterIndex = 0; 791 while (fieldIndex < fieldValues.size() && setterIndex < mFieldSetters.size()) { 792 final FieldSetter setter = mFieldSetters.get(setterIndex); 793 794 if (dumpsysVersion >= 0 && dumpsysVersion < setter.getAdded()) { 795 // The version being parsed doesn't have the field for this setter, 796 // so skip the setter but not the field. 797 setterIndex++; 798 continue; 799 } 800 801 final String value = fieldValues.get(fieldIndex); 802 try { 803 if (debug) { 804 System.err.println(" setting field " + setter + " to: " + value); 805 } 806 if (setter.isArray()) { 807 setter.setArray(lineNumber, record, fieldValues, 808 fieldIndex, fieldValues.size()); 809 // The rest of the fields have been consumed. 810 fieldIndex = fieldValues.size(); 811 setterIndex = mFieldSetters.size(); 812 break; 813 } else { 814 setter.setField(lineNumber, record, value); 815 } 816 } catch (ParseException ex) { 817 bs.addWarning(lineNumber, WarningId.BAD_FIELD_TYPE, fieldValues, 818 ex.getMessage(), mTag, value); 819 record.complete = false; 820 } 821 822 fieldIndex++; 823 setterIndex++; 824 } 825 826 // If there are extra fields, this record is complete, there are just 827 // extra values, so we issue a warning but don't mark it incomplete. 828 if (fieldIndex < fieldValues.size()) { 829 bs.addWarning(lineNumber, WarningId.TOO_MANY_FIELDS, fieldValues, 830 "Line '" + mTag + "' has extra fields.", 831 mTag, Integer.toString(fieldIndex), Integer.toString(fieldValues.size())); 832 if (debug) { 833 for (int i=0; i<mFieldSetters.size(); i++) { 834 System.err.println(" setter: [" + i + "] " + mFieldSetters.get(i)); 835 } 836 } 837 } 838 839 // If we have any fields that are missing, add a warning and return null. 840 for (; setterIndex < mFieldSetters.size(); setterIndex++) { 841 final FieldSetter setter = mFieldSetters.get(setterIndex); 842 if (dumpsysVersion >= 0 && dumpsysVersion >= setter.getAdded()) { 843 bs.addWarning(lineNumber, WarningId.NOT_ENOUGH_FIELDS, fieldValues, 844 "Line '" + mTag + "' missing field: index=" + setterIndex 845 + " name=" + setter.getName(), 846 mTag, Integer.toString(setterIndex)); 847 record.complete = false; 848 } 849 } 850 851 return record; 852 } 853 } 854 855 /** 856 * Parse the input stream and return a RawBatteryStats object. 857 */ parse(InputStream input)858 public static RawBatteryStats parse(InputStream input) throws ParseException, IOException { 859 final RawBatteryStats result = new RawBatteryStats(); 860 result.parseImpl(input); 861 return result; 862 } 863 864 /** 865 * Get a record. 866 * <p> 867 * If multiple of that record are found, returns the first one. There will already 868 * have been a warning recorded if the count annotation did not match what was in the 869 * csv. 870 * <p> 871 * Returns null if there are no records of that type. 872 */ getSingle(Class<T> cl)873 public <T extends Record> T getSingle(Class<T> cl) { 874 final List<Record> list = mRecordsByType.get(cl.getName()); 875 if (list == null) { 876 return null; 877 } 878 // Notes: 879 // - List can never be empty because the list itself wouldn't have been added. 880 // - Cast is safe because list was populated based on class name (let's assume 881 // there's only one class loader involved here). 882 return (T)list.get(0); 883 } 884 885 /** 886 * Get a record. 887 * <p> 888 * If multiple of that record are found, returns the first one that matches that uid. 889 * <p> 890 * Returns null if there are no records of that type that match the given uid. 891 */ getSingle(Class<T> cl, int uid)892 public <T extends Record> T getSingle(Class<T> cl, int uid) { 893 final List<Record> list = mRecordsByType.get(cl.getName()); 894 if (list == null) { 895 return null; 896 } 897 for (final Record record: list) { 898 if (record.uid == uid) { 899 // Cast is safe because list was populated based on class name (let's assume 900 // there's only one class loader involved here). 901 return (T)record; 902 } 903 } 904 return null; 905 } 906 907 /** 908 * Get all the records of the given type. 909 */ getMultiple(Class<T> cl)910 public <T extends Record> List<T> getMultiple(Class<T> cl) { 911 final List<Record> list = mRecordsByType.get(cl.getName()); 912 if (list == null) { 913 return ImmutableList.<T>of(); 914 } 915 // Cast is safe because list was populated based on class name (let's assume 916 // there's only one class loader involved here). 917 return ImmutableList.copyOf((List<T>)list); 918 } 919 920 /** 921 * Get the UIDs that are covered by this batterystats dump. 922 */ getApps()923 public Set<AttributionKey> getApps() { 924 return mApps; 925 } 926 927 /** 928 * No public constructor. Use {@link #parse}. 929 */ RawBatteryStats()930 private RawBatteryStats() { 931 } 932 933 /** 934 * Get the list of Record objects that were parsed from the csv. 935 */ getRecords()936 public List<Record> getRecords() { 937 return mRecords; 938 } 939 940 /** 941 * Gets the warnings that were encountered during parsing. 942 */ getWarnings()943 public List<Warning> getWarnings() { 944 return mWarnings; 945 } 946 947 /** 948 * Implementation of the csv parsing. 949 */ parseImpl(InputStream input)950 private void parseImpl(InputStream input) throws ParseException, IOException { 951 // Parse the csv 952 CsvParser.parse(input, new CsvParser.LineProcessor() { 953 @Override 954 public void onLine(int lineNumber, ArrayList<String> fields) 955 throws ParseException { 956 handleCsvLine(lineNumber, fields); 957 } 958 }); 959 960 // Gather the records by class name for the getSingle() and getMultiple() functions. 961 indexRecords(); 962 963 // Gather the uids from all the places UIDs come from, for getApps(). 964 indexApps(); 965 } 966 967 /** 968 * Handle a line of CSV input, creating the right Record object. 969 */ handleCsvLine(int lineNumber, ArrayList<String> fields)970 private void handleCsvLine(int lineNumber, ArrayList<String> fields) throws ParseException { 971 // The standard rows all have the 4 core fields. Anything less isn't what we're 972 // looking for. 973 if (fields.size() <= 4) { 974 addWarning(lineNumber, WarningId.TOO_FEW_FIELDS_FOR_LINE_TYPE, fields, 975 "Line with too few fields (" + fields.size() + ")", 976 Integer.toString(fields.size())); 977 return; 978 } 979 980 final String lineType = fields.get(3); 981 982 // Handle the vers line specially, because we need the version number 983 // to make the rest of the machinery work. 984 if ("vers".equals(lineType)) { 985 final String versionText = fields.get(4); 986 try { 987 mDumpsysVersion = Integer.parseInt(versionText); 988 } catch (NumberFormatException ex) { 989 addWarning(lineNumber, WarningId.BAD_DUMPSYS_VERSION, fields, 990 "Couldn't parse dumpsys version number: '" + versionText, 991 versionText); 992 } 993 } 994 995 // Find the right factory. 996 final RecordFactory factory = sFactories.get(lineType); 997 if (factory == null) { 998 addWarning(lineNumber, WarningId.NO_MATCHING_LINE_TYPE, fields, 999 "No Record for line type '" + lineType + "'", 1000 lineType); 1001 return; 1002 } 1003 1004 // Create the record. 1005 final Record record = factory.create(this, mDumpsysVersion, lineNumber, fields); 1006 mRecords.add(record); 1007 } 1008 1009 /** 1010 * Add to the list of warnings. 1011 */ addWarning(int lineNumber, WarningId id, ArrayList<String> fields, String message, String... extras)1012 private void addWarning(int lineNumber, WarningId id, 1013 ArrayList<String> fields, String message, String... extras) { 1014 mWarnings.add(new Warning(lineNumber, id, fields, message, extras)); 1015 final boolean debug = false; 1016 if (debug) { 1017 final StringBuilder text = new StringBuilder("line "); 1018 text.append(lineNumber); 1019 text.append(": WARNING: "); 1020 text.append(message); 1021 text.append("\n fields: "); 1022 for (int i=0; i<fields.size(); i++) { 1023 final String field = fields.get(i); 1024 if (field.indexOf('"') >= 0) { 1025 text.append('"'); 1026 text.append(field.replace("\"", "\"\"")); 1027 text.append('"'); 1028 } else { 1029 text.append(field); 1030 } 1031 if (i != fields.size() - 1) { 1032 text.append(','); 1033 } 1034 } 1035 text.append('\n'); 1036 for (String extra: extras) { 1037 text.append(" extra: "); 1038 text.append(extra); 1039 text.append('\n'); 1040 } 1041 System.err.print(text.toString()); 1042 } 1043 } 1044 1045 /** 1046 * Group records by class name. 1047 */ indexRecords()1048 private void indexRecords() { 1049 final HashMap<String,ArrayList<Record>> map = new HashMap<String,ArrayList<Record>>(); 1050 1051 // Iterate over all of the records 1052 for (Record record: mRecords) { 1053 final String className = record.getClass().getName(); 1054 1055 ArrayList<Record> list = map.get(className); 1056 if (list == null) { 1057 list = new ArrayList<Record>(); 1058 map.put(className, list); 1059 } 1060 1061 list.add(record); 1062 } 1063 1064 // Make it immutable 1065 final HashMap<String,ImmutableList<Record>> result 1066 = new HashMap<String,ImmutableList<Record>>(); 1067 for (HashMap.Entry<String,ArrayList<Record>> entry: map.entrySet()) { 1068 result.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); 1069 } 1070 1071 // Initialize here so uninitialized access will result in NPE. 1072 mRecordsByType = ImmutableMap.copyOf(result); 1073 } 1074 1075 /** 1076 * Collect the UIDs from the csv. 1077 * 1078 * They come from two places. 1079 * <ul> 1080 * <li>The uid to package name map entries ({@link #Uid}) at the beginning. 1081 * <li>The uid fields of the rest of the entries, some of which might not 1082 * have package names associated with them. 1083 * </ul> 1084 * 1085 * TODO: Is this where we should also do the logic about the special UIDs? 1086 */ indexApps()1087 private void indexApps() { 1088 final HashMap<Integer,HashSet<String>> uids = new HashMap<Integer,HashSet<String>>(); 1089 1090 // The Uid rows, from which we get package names 1091 for (Uid record: getMultiple(Uid.class)) { 1092 HashSet<String> list = uids.get(record.uidKey); 1093 if (list == null) { 1094 list = new HashSet<String>(); 1095 uids.put(record.uidKey, list); 1096 } 1097 list.add(record.pkg); 1098 } 1099 1100 // The uid fields of everything 1101 for (Record record: mRecords) { 1102 // The 0 in the INFO records isn't really root, it's just unfilled data. 1103 // The root uid (0) will show up practically in every record, but don't force it. 1104 if (record.category != Category.INFO) { 1105 if (uids.get(record.uid) == null) { 1106 // There is no other data about this UID, but it does exist! 1107 uids.put(record.uid, new HashSet<String>()); 1108 } 1109 } 1110 } 1111 1112 // Turn our temporary lists of package names into AttributionKeys. 1113 final HashSet<AttributionKey> result = new HashSet<AttributionKey>(); 1114 for (HashMap.Entry<Integer,HashSet<String>> entry: uids.entrySet()) { 1115 result.add(new AttributionKey(entry.getKey(), entry.getValue())); 1116 } 1117 1118 // Initialize here so uninitialized access will result in NPE. 1119 mApps = ImmutableSet.copyOf(result); 1120 } 1121 1122 /** 1123 * Init the factory classes. 1124 */ 1125 static { 1126 for (Class<?> cl: RawBatteryStats.class.getClasses()) { 1127 final Line lineAnnotation = cl.getAnnotation(Line.class); 1128 if (lineAnnotation != null && Record.class.isAssignableFrom(cl)) { 1129 final ArrayList<FieldSetter> fieldSetters = new ArrayList<FieldSetter>(); 1130 1131 for (java.lang.reflect.Field field: cl.getFields()) { 1132 final Field fa = field.getAnnotation(Field.class); 1133 if (fa != null) { 1134 final Class<?> fieldType = field.getType(); 1135 final Class<?> innerType = fieldType.isArray() 1136 ? fieldType.getComponentType() 1137 : fieldType; 1138 if (Integer.TYPE.equals(innerType)) { fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field))1139 fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field)); 1140 } else if (Long.TYPE.equals(innerType)) { fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field))1141 fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field)); 1142 } else if (String.class.equals(innerType)) { fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field))1143 fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field)); 1144 } else if (innerType.isEnum()) { fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field))1145 fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field)); 1146 } else { 1147 throw new RuntimeException("Unsupported field type '" 1148 + fieldType.getName() + "' on " 1149 + cl.getName() + "." + field.getName()); 1150 } 1151 } 1152 } 1153 // Sort by index Collections.sort(fieldSetters, new Comparator<FieldSetter>() { @Override public int compare(FieldSetter a, FieldSetter b) { return a.getIndex() - b.getIndex(); } })1154 Collections.sort(fieldSetters, new Comparator<FieldSetter>() { 1155 @Override 1156 public int compare(FieldSetter a, FieldSetter b) { 1157 return a.getIndex() - b.getIndex(); 1158 } 1159 }); 1160 // Only the last one can be an array 1161 for (int i=0; i<fieldSetters.size()-1; i++) { 1162 if (fieldSetters.get(i).isArray()) { 1163 throw new RuntimeException("Only the last (highest index) @Field" 1164 + " in class " + cl.getName() + " can be an array: " 1165 + fieldSetters.get(i).getName()); 1166 } 1167 } 1168 // Add to the map lineAnnotation.tag()1169 sFactories.put(lineAnnotation.tag(), new RecordFactory(lineAnnotation.tag(), 1170 (Class<Record>)cl, fieldSetters)); 1171 } 1172 } 1173 } 1174 } 1175 1176