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.internal.os; 18 19 import android.annotation.NonNull; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.os.BatteryStats; 23 import android.os.BatteryStats.BitDescription; 24 import android.os.BatteryStats.HistoryItem; 25 import android.os.BatteryStats.HistoryTag; 26 import android.os.Build; 27 import android.os.Parcel; 28 import android.os.Process; 29 import android.os.StatFs; 30 import android.os.SystemClock; 31 import android.os.SystemProperties; 32 import android.os.Trace; 33 import android.util.ArraySet; 34 import android.util.AtomicFile; 35 import android.util.Slog; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.util.ParseUtils; 39 40 import java.io.File; 41 import java.io.FilenameFilter; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Set; 46 47 /** 48 * BatteryStatsHistory encapsulates battery history files. 49 * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into 50 * {@link #mActiveFile}. 51 * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, 52 * current mActiveFile is closed and a new mActiveFile is open. 53 * History files are under directory /data/system/battery-history/. 54 * History files have name battery-history-<num>.bin. The file number <num> starts from zero and 55 * grows sequentially. 56 * The mActiveFile is always the highest numbered history file. 57 * The lowest number file is always the oldest file. 58 * The highest number file is always the newest file. 59 * The file number grows sequentially and we never skip number. 60 * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES}, 61 * the lowest numbered file is deleted and a new file is open. 62 * 63 * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by 64 * locks on BatteryStatsImpl object. 65 */ 66 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 67 public class BatteryStatsHistory { 68 private static final boolean DEBUG = false; 69 private static final String TAG = "BatteryStatsHistory"; 70 public static final String HISTORY_DIR = "battery-history"; 71 public static final String FILE_SUFFIX = ".bin"; 72 private static final int MIN_FREE_SPACE = 100 * 1024 * 1024; 73 74 @Nullable 75 private final BatteryStatsImpl mStats; 76 private final Parcel mHistoryBuffer; 77 private final File mHistoryDir; 78 /** 79 * The active history file that the history buffer is backed up into. 80 */ 81 private AtomicFile mActiveFile; 82 /** 83 * A list of history files with incremental indexes. 84 */ 85 private final List<Integer> mFileNumbers = new ArrayList<>(); 86 87 /** 88 * A list of small history parcels, used when BatteryStatsImpl object is created from 89 * deserialization of a parcel, such as Settings app or checkin file. 90 */ 91 private List<Parcel> mHistoryParcels = null; 92 93 /** 94 * When iterating history files, the current file index. 95 */ 96 private int mCurrentFileIndex; 97 /** 98 * When iterating history files, the current file parcel. 99 */ 100 private Parcel mCurrentParcel; 101 /** 102 * When iterating history file, the current parcel's Parcel.dataSize(). 103 */ 104 private int mCurrentParcelEnd; 105 /** 106 * When iterating history files, the current record count. 107 */ 108 private int mRecordCount = 0; 109 /** 110 * Used when BatteryStatsImpl object is created from deserialization of a parcel, 111 * such as Settings app or checkin file, to iterate over history parcels. 112 */ 113 private int mParcelIndex = 0; 114 115 /** 116 * A delegate for android.os.Trace to allow testing static calls. Due to 117 * limitations in Android Tracing (b/153319140), the delegate also records 118 * counter values in system properties which allows reading the value at the 119 * start of a tracing session. This overhead is limited to userdebug builds. 120 * On user builds, tracing still occurs but the counter value will be missing 121 * until the first change occurs. 122 */ 123 @VisibleForTesting 124 public static class TraceDelegate { 125 // Note: certain tests currently run as platform_app which is not allowed 126 // to set debug system properties. To ensure that system properties are set 127 // only when allowed, we check the current UID. 128 private final boolean mShouldSetProperty = 129 Build.IS_USERDEBUG && (Process.myUid() == Process.SYSTEM_UID); 130 131 /** 132 * Returns true if trace counters should be recorded. 133 */ tracingEnabled()134 public boolean tracingEnabled() { 135 return Trace.isTagEnabled(Trace.TRACE_TAG_POWER) || mShouldSetProperty; 136 } 137 138 /** 139 * Records the counter value with the given name. 140 */ traceCounter(@onNull String name, int value)141 public void traceCounter(@NonNull String name, int value) { 142 Trace.traceCounter(Trace.TRACE_TAG_POWER, name, value); 143 if (mShouldSetProperty) { 144 SystemProperties.set("debug.tracing." + name, Integer.toString(value)); 145 } 146 } 147 148 /** 149 * Records an instant event (one with no duration). 150 */ traceInstantEvent(@onNull String track, @NonNull String name)151 public void traceInstantEvent(@NonNull String track, @NonNull String name) { 152 Trace.instantForTrack(Trace.TRACE_TAG_POWER, track, name); 153 } 154 } 155 156 private TraceDelegate mTracer = new TraceDelegate(); 157 158 /** 159 * Constructor 160 * @param stats BatteryStatsImpl object. 161 * @param systemDir typically /data/system 162 * @param historyBuffer The in-memory history buffer. 163 */ BatteryStatsHistory(@onNull BatteryStatsImpl stats, File systemDir, Parcel historyBuffer)164 public BatteryStatsHistory(@NonNull BatteryStatsImpl stats, File systemDir, 165 Parcel historyBuffer) { 166 mStats = stats; 167 mHistoryBuffer = historyBuffer; 168 mHistoryDir = new File(systemDir, HISTORY_DIR); 169 mHistoryDir.mkdirs(); 170 if (!mHistoryDir.exists()) { 171 Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath()); 172 } 173 174 final Set<Integer> dedup = new ArraySet<>(); 175 // scan directory, fill mFileNumbers and mActiveFile. 176 mHistoryDir.listFiles(new FilenameFilter() { 177 @Override 178 public boolean accept(File dir, String name) { 179 final int b = name.lastIndexOf(FILE_SUFFIX); 180 if (b <= 0) { 181 return false; 182 } 183 final Integer c = 184 ParseUtils.parseInt(name.substring(0, b), -1); 185 if (c != -1) { 186 dedup.add(c); 187 return true; 188 } else { 189 return false; 190 } 191 } 192 }); 193 if (!dedup.isEmpty()) { 194 mFileNumbers.addAll(dedup); 195 Collections.sort(mFileNumbers); 196 setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1)); 197 } else { 198 // No file found, default to have file 0. 199 mFileNumbers.add(0); 200 setActiveFile(0); 201 } 202 } 203 204 /** 205 * Used when BatteryStatsImpl object is created from deserialization of a parcel, 206 * such as Settings app or checkin file. 207 * @param historyBuffer the history buffer 208 */ BatteryStatsHistory(Parcel historyBuffer)209 public BatteryStatsHistory(Parcel historyBuffer) { 210 mStats = null; 211 mHistoryDir = null; 212 mHistoryBuffer = historyBuffer; 213 } 214 getHistoryDirectory()215 public File getHistoryDirectory() { 216 return mHistoryDir; 217 } 218 219 /** 220 * Set the active file that mHistoryBuffer is backed up into. 221 * 222 * @param fileNumber the history file that mHistoryBuffer is backed up into. 223 */ setActiveFile(int fileNumber)224 private void setActiveFile(int fileNumber) { 225 mActiveFile = getFile(fileNumber); 226 if (DEBUG) { 227 Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath()); 228 } 229 } 230 231 /** 232 * Create history AtomicFile from file number. 233 * @param num file number. 234 * @return AtomicFile object. 235 */ getFile(int num)236 private AtomicFile getFile(int num) { 237 return new AtomicFile( 238 new File(mHistoryDir, num + FILE_SUFFIX)); 239 } 240 241 /** 242 * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, 243 * create next history file. 244 */ startNextFile()245 public void startNextFile() { 246 if (mStats == null) { 247 Slog.wtf(TAG, "mStats should not be null when writing history"); 248 return; 249 } 250 251 if (mFileNumbers.isEmpty()) { 252 Slog.wtf(TAG, "mFileNumbers should never be empty"); 253 return; 254 } 255 256 // The last number in mFileNumbers is the highest number. The next file number is highest 257 // number plus one. 258 final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1; 259 mFileNumbers.add(next); 260 setActiveFile(next); 261 262 // if free disk space is less than 100MB, delete oldest history file. 263 if (!hasFreeDiskSpace()) { 264 int oldest = mFileNumbers.remove(0); 265 getFile(oldest).delete(); 266 } 267 268 // if there are more history files than allowed, delete oldest history files. 269 // MAX_HISTORY_FILES can be updated by GService config at run time. 270 while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) { 271 int oldest = mFileNumbers.get(0); 272 getFile(oldest).delete(); 273 mFileNumbers.remove(0); 274 } 275 } 276 277 /** 278 * Delete all existing history files. Active history file start from number 0 again. 279 */ resetAllFiles()280 public void resetAllFiles() { 281 for (Integer i : mFileNumbers) { 282 getFile(i).delete(); 283 } 284 mFileNumbers.clear(); 285 mFileNumbers.add(0); 286 setActiveFile(0); 287 } 288 289 /** 290 * Start iterating history files and history buffer. 291 * @return always return true. 292 */ startIteratingHistory()293 public boolean startIteratingHistory() { 294 mRecordCount = 0; 295 mCurrentFileIndex = 0; 296 mCurrentParcel = null; 297 mCurrentParcelEnd = 0; 298 mParcelIndex = 0; 299 return true; 300 } 301 302 /** 303 * Finish iterating history files and history buffer. 304 */ finishIteratingHistory()305 public void finishIteratingHistory() { 306 // setDataPosition so mHistoryBuffer Parcel can be written. 307 mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); 308 if (DEBUG) { 309 Slog.d(TAG, "Battery history records iterated: " + mRecordCount); 310 } 311 } 312 313 /** 314 * When iterating history files and history buffer, always start from the lowest numbered 315 * history file, when reached the mActiveFile (highest numbered history file), do not read from 316 * mActiveFile, read from history buffer instead because the buffer has more updated data. 317 * @param out a history item. 318 * @return The parcel that has next record. null if finished all history files and history 319 * buffer 320 */ getNextParcel(BatteryStats.HistoryItem out)321 public Parcel getNextParcel(BatteryStats.HistoryItem out) { 322 if (mRecordCount == 0) { 323 // reset out if it is the first record. 324 out.clear(); 325 } 326 ++mRecordCount; 327 328 // First iterate through all records in current parcel. 329 if (mCurrentParcel != null) 330 { 331 if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) { 332 // There are more records in current parcel. 333 return mCurrentParcel; 334 } else if (mHistoryBuffer == mCurrentParcel) { 335 // finished iterate through all history files and history buffer. 336 return null; 337 } else if (mHistoryParcels == null 338 || !mHistoryParcels.contains(mCurrentParcel)) { 339 // current parcel is from history file. 340 mCurrentParcel.recycle(); 341 } 342 } 343 344 // Try next available history file. 345 // skip the last file because its data is in history buffer. 346 while (mCurrentFileIndex < mFileNumbers.size() - 1) { 347 mCurrentParcel = null; 348 mCurrentParcelEnd = 0; 349 final Parcel p = Parcel.obtain(); 350 AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++)); 351 if (readFileToParcel(p, file)) { 352 int bufSize = p.readInt(); 353 int curPos = p.dataPosition(); 354 mCurrentParcelEnd = curPos + bufSize; 355 mCurrentParcel = p; 356 if (curPos < mCurrentParcelEnd) { 357 return mCurrentParcel; 358 } 359 } else { 360 p.recycle(); 361 } 362 } 363 364 // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization 365 // of a parcel, such as Settings app or checkin file. 366 if (mHistoryParcels != null) { 367 while (mParcelIndex < mHistoryParcels.size()) { 368 final Parcel p = mHistoryParcels.get(mParcelIndex++); 369 if (!skipHead(p)) { 370 continue; 371 } 372 final int bufSize = p.readInt(); 373 final int curPos = p.dataPosition(); 374 mCurrentParcelEnd = curPos + bufSize; 375 mCurrentParcel = p; 376 if (curPos < mCurrentParcelEnd) { 377 return mCurrentParcel; 378 } 379 } 380 } 381 382 // finished iterator through history files (except the last one), now history buffer. 383 if (mHistoryBuffer.dataSize() <= 0) { 384 // buffer is empty. 385 return null; 386 } 387 mHistoryBuffer.setDataPosition(0); 388 mCurrentParcel = mHistoryBuffer; 389 mCurrentParcelEnd = mCurrentParcel.dataSize(); 390 return mCurrentParcel; 391 } 392 393 /** 394 * Read history file into a parcel. 395 * @param out the Parcel read into. 396 * @param file the File to read from. 397 * @return true if success, false otherwise. 398 */ readFileToParcel(Parcel out, AtomicFile file)399 public boolean readFileToParcel(Parcel out, AtomicFile file) { 400 byte[] raw = null; 401 try { 402 final long start = SystemClock.uptimeMillis(); 403 raw = file.readFully(); 404 if (DEBUG) { 405 Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() 406 + " duration ms:" + (SystemClock.uptimeMillis() - start)); 407 } 408 } catch(Exception e) { 409 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e); 410 return false; 411 } 412 out.unmarshall(raw, 0, raw.length); 413 out.setDataPosition(0); 414 return skipHead(out); 415 } 416 417 /** 418 * Skip the header part of history parcel. 419 * @param p history parcel to skip head. 420 * @return true if version match, false if not. 421 */ skipHead(Parcel p)422 private boolean skipHead(Parcel p) { 423 p.setDataPosition(0); 424 final int version = p.readInt(); 425 if (version != BatteryStatsImpl.VERSION) { 426 return false; 427 } 428 // skip historyBaseTime field. 429 p.readLong(); 430 return true; 431 } 432 433 /** 434 * Read all history files and serialize into a big Parcel. 435 * Checkin file calls this method. 436 * 437 * @param out the output parcel 438 */ writeToParcel(Parcel out)439 public void writeToParcel(Parcel out) { 440 writeToParcel(out, false /* useBlobs */); 441 } 442 443 /** 444 * This is for Settings app, when Settings app receives big history parcel, it call 445 * this method to parse it into list of parcels. 446 * @param out the output parcel 447 */ writeToBatteryUsageStatsParcel(Parcel out)448 public void writeToBatteryUsageStatsParcel(Parcel out) { 449 out.writeBlob(mHistoryBuffer.marshall()); 450 writeToParcel(out, true /* useBlobs */); 451 } 452 writeToParcel(Parcel out, boolean useBlobs)453 private void writeToParcel(Parcel out, boolean useBlobs) { 454 final long start = SystemClock.uptimeMillis(); 455 out.writeInt(mFileNumbers.size() - 1); 456 for(int i = 0; i < mFileNumbers.size() - 1; i++) { 457 AtomicFile file = getFile(mFileNumbers.get(i)); 458 byte[] raw = new byte[0]; 459 try { 460 raw = file.readFully(); 461 } catch(Exception e) { 462 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e); 463 } 464 if (useBlobs) { 465 out.writeBlob(raw); 466 } else { 467 // Avoiding blobs in the check-in file for compatibility 468 out.writeByteArray(raw); 469 } 470 } 471 if (DEBUG) { 472 Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); 473 } 474 } 475 476 /** 477 * Reads a BatteryStatsHistory from a parcel written with 478 * the {@link #writeToBatteryUsageStatsParcel} method. 479 */ createFromBatteryUsageStatsParcel(Parcel in)480 public static BatteryStatsHistory createFromBatteryUsageStatsParcel(Parcel in) { 481 final byte[] historyBlob = in.readBlob(); 482 483 Parcel historyBuffer = Parcel.obtain(); 484 historyBuffer.unmarshall(historyBlob, 0, historyBlob.length); 485 486 BatteryStatsHistory history = new BatteryStatsHistory(historyBuffer); 487 history.readFromParcel(in, true /* useBlobs */); 488 return history; 489 } 490 491 /** 492 * This is for the check-in file, which has all history files embedded. 493 * 494 * @param in the input parcel. 495 */ readFromParcel(Parcel in)496 public void readFromParcel(Parcel in) { 497 readFromParcel(in, false /* useBlobs */); 498 } 499 readFromParcel(Parcel in, boolean useBlobs)500 private void readFromParcel(Parcel in, boolean useBlobs) { 501 final long start = SystemClock.uptimeMillis(); 502 mHistoryParcels = new ArrayList<>(); 503 final int count = in.readInt(); 504 for(int i = 0; i < count; i++) { 505 byte[] temp = useBlobs ? in.readBlob() : in.createByteArray(); 506 if (temp == null || temp.length == 0) { 507 continue; 508 } 509 Parcel p = Parcel.obtain(); 510 p.unmarshall(temp, 0, temp.length); 511 p.setDataPosition(0); 512 mHistoryParcels.add(p); 513 } 514 if (DEBUG) { 515 Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start)); 516 } 517 } 518 519 /** 520 * @return true if there is more than 100MB free disk space left. 521 */ hasFreeDiskSpace()522 private boolean hasFreeDiskSpace() { 523 final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath()); 524 return stats.getAvailableBytes() > MIN_FREE_SPACE; 525 } 526 getFilesNumbers()527 public List<Integer> getFilesNumbers() { 528 return mFileNumbers; 529 } 530 getActiveFile()531 public AtomicFile getActiveFile() { 532 return mActiveFile; 533 } 534 535 /** 536 * @return the total size of all history files and history buffer. 537 */ getHistoryUsedSize()538 public int getHistoryUsedSize() { 539 int ret = 0; 540 for(int i = 0; i < mFileNumbers.size() - 1; i++) { 541 ret += getFile(mFileNumbers.get(i)).getBaseFile().length(); 542 } 543 ret += mHistoryBuffer.dataSize(); 544 if (mHistoryParcels != null) { 545 for(int i = 0; i < mHistoryParcels.size(); i++) { 546 ret += mHistoryParcels.get(i).dataSize(); 547 } 548 } 549 return ret; 550 } 551 552 /** 553 * Writes event details into Atrace. 554 */ recordTraceEvents(int code, HistoryTag tag)555 public void recordTraceEvents(int code, HistoryTag tag) { 556 if (code == HistoryItem.EVENT_NONE) return; 557 if (!mTracer.tracingEnabled()) return; 558 559 final int idx = code & HistoryItem.EVENT_TYPE_MASK; 560 final String prefix = (code & HistoryItem.EVENT_FLAG_START) != 0 ? "+" : 561 (code & HistoryItem.EVENT_FLAG_FINISH) != 0 ? "-" : ""; 562 563 final String[] names = BatteryStats.HISTORY_EVENT_NAMES; 564 if (idx < 0 || idx >= BatteryStats.HISTORY_EVENT_NAMES.length) return; 565 566 final String track = "battery_stats." + names[idx]; 567 final String name = prefix + names[idx] + "=" + tag.uid + ":\"" + tag.string + "\""; 568 mTracer.traceInstantEvent(track, name); 569 } 570 571 /** 572 * Writes changes to a HistoryItem state bitmap to Atrace. 573 */ recordTraceCounters(int oldval, int newval, BitDescription[] descriptions)574 public void recordTraceCounters(int oldval, int newval, BitDescription[] descriptions) { 575 if (!mTracer.tracingEnabled()) return; 576 577 int diff = oldval ^ newval; 578 if (diff == 0) return; 579 580 for (int i = 0; i < descriptions.length; i++) { 581 BitDescription bd = descriptions[i]; 582 if ((diff & bd.mask) == 0) continue; 583 584 int value; 585 if (bd.shift < 0) { 586 value = (newval & bd.mask) != 0 ? 1 : 0; 587 } else { 588 value = (newval & bd.mask) >> bd.shift; 589 } 590 591 mTracer.traceCounter("battery_stats." + bd.name, value); 592 } 593 } 594 } 595