1 /* 2 * Copyright (C) 2025 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 package com.android.server.power.stats; 17 18 import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; 19 20 import android.annotation.NonNull; 21 import android.os.SystemClock; 22 import android.os.Trace; 23 import android.util.ArraySet; 24 import android.util.AtomicFile; 25 import android.util.Slog; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.os.BackgroundThread; 29 import com.android.internal.os.BatteryStatsHistory; 30 import com.android.internal.os.BatteryStatsHistory.BatteryHistoryFragment; 31 32 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; 33 import org.apache.commons.compress.compressors.gzip.GzipParameters; 34 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.OutputStream; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Locale; 46 import java.util.Set; 47 import java.util.concurrent.locks.ReentrantLock; 48 import java.util.zip.Deflater; 49 import java.util.zip.GZIPInputStream; 50 51 public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHistoryStore { 52 public static final String TAG = "BatteryHistoryDirectory"; 53 private static final boolean DEBUG = false; 54 55 private static final String FILE_SUFFIX = ".bh"; 56 57 // Size of the magic number written at the start of each history file 58 private static final int FILE_FORMAT_BYTES = 4; 59 private static final byte[] FILE_FORMAT_PARCEL = {0x50, 0x52, 0x43, 0x4c}; // PRCL 60 private static final byte[] FILE_FORMAT_COMPRESSED_PARCEL = {0x47, 0x5a, 0x49, 0x50}; // GZIP 61 62 static class BatteryHistoryFile extends BatteryHistoryFragment { 63 public final AtomicFile atomicFile; 64 BatteryHistoryFile(File directory, long monotonicTimeMs)65 BatteryHistoryFile(File directory, long monotonicTimeMs) { 66 super(monotonicTimeMs); 67 atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX)); 68 } 69 70 @Override toString()71 public String toString() { 72 return atomicFile.getBaseFile().toString(); 73 } 74 } 75 76 interface Compressor { compress(OutputStream stream, byte[] data)77 void compress(OutputStream stream, byte[] data) throws IOException; uncompress(byte[] data, InputStream stream)78 void uncompress(byte[] data, InputStream stream) throws IOException; 79 readFully(byte[] data, InputStream stream)80 default void readFully(byte[] data, InputStream stream) throws IOException { 81 int pos = 0; 82 while (pos < data.length) { 83 int count = stream.read(data, pos, data.length - pos); 84 if (count == -1) { 85 throw new IOException("Invalid battery history file format"); 86 } 87 pos += count; 88 } 89 } 90 } 91 92 static final Compressor DEFAULT_COMPRESSOR = new Compressor() { 93 @Override 94 public void compress(OutputStream stream, byte[] data) throws IOException { 95 // With the BEST_SPEED hint, we see ~4x improvement in write latency over 96 // GZIPOutputStream. 97 GzipParameters parameters = new GzipParameters(); 98 parameters.setCompressionLevel(Deflater.BEST_SPEED); 99 GzipCompressorOutputStream os = new GzipCompressorOutputStream(stream, parameters); 100 os.write(data); 101 os.finish(); 102 os.flush(); 103 } 104 105 @Override 106 public void uncompress(byte[] data, InputStream stream) throws IOException { 107 readFully(data, new GZIPInputStream(stream)); 108 } 109 }; 110 111 private final File mDirectory; 112 private int mMaxHistorySize; 113 private boolean mInitialized; 114 private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>(); 115 private final ReentrantLock mLock = new ReentrantLock(); 116 private final Compressor mCompressor; 117 private boolean mWaitForDirectoryLock = false; 118 private boolean mFileCompressionEnabled; 119 BatteryHistoryDirectory(@onNull File directory, int maxHistorySize)120 public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize) { 121 this(directory, maxHistorySize, DEFAULT_COMPRESSOR); 122 } 123 BatteryHistoryDirectory(@onNull File directory, int maxHistorySize, Compressor compressor)124 public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize, 125 Compressor compressor) { 126 mDirectory = directory; 127 mMaxHistorySize = maxHistorySize; 128 if (mMaxHistorySize == 0) { 129 Slog.w(TAG, "maxHistorySize should not be zero"); 130 } 131 mCompressor = compressor; 132 } 133 setFileCompressionEnabled(boolean enabled)134 public void setFileCompressionEnabled(boolean enabled) { 135 mFileCompressionEnabled = enabled; 136 } 137 setMaxHistorySize(int maxHistorySize)138 void setMaxHistorySize(int maxHistorySize) { 139 mMaxHistorySize = maxHistorySize; 140 trim(); 141 } 142 143 /** 144 * Returns the maximum storage size allocated to battery history. 145 */ 146 @Override getMaxHistorySize()147 public int getMaxHistorySize() { 148 return mMaxHistorySize; 149 } 150 151 @Override lock()152 public void lock() { 153 mLock.lock(); 154 } 155 156 /** 157 * Turns "tryLock" into "lock" to prevent flaky unit tests. 158 * Should only be called from unit tests. 159 */ 160 @VisibleForTesting makeDirectoryLockUnconditional()161 void makeDirectoryLockUnconditional() { 162 mWaitForDirectoryLock = true; 163 } 164 165 @Override tryLock()166 public boolean tryLock() { 167 if (mWaitForDirectoryLock) { 168 mLock.lock(); 169 return true; 170 } 171 return mLock.tryLock(); 172 } 173 174 @Override writeFragment(BatteryHistoryFragment fragment, @NonNull byte[] data, boolean fragmentComplete)175 public void writeFragment(BatteryHistoryFragment fragment, 176 @NonNull byte[] data, boolean fragmentComplete) { 177 AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; 178 FileOutputStream fos = null; 179 try { 180 final long startTimeMs = SystemClock.uptimeMillis(); 181 fos = file.startWrite(); 182 fos.write(FILE_FORMAT_PARCEL); 183 writeInt(fos, data.length); 184 fos.write(data); 185 fos.flush(); 186 file.finishWrite(fos); 187 if (DEBUG) { 188 Slog.d(TAG, "writeHistoryFragment file:" + file.getBaseFile().getPath() 189 + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) 190 + " bytes:" + data.length); 191 } 192 if (fragmentComplete) { 193 if (mFileCompressionEnabled) { 194 BackgroundThread.getHandler().post( 195 () -> writeHistoryFragmentCompressed(file, data)); 196 } 197 BackgroundThread.getHandler().post(()-> trim()); 198 } 199 } catch (IOException e) { 200 Slog.w(TAG, "Error writing battery history fragment", e); 201 file.failWrite(fos); 202 } 203 } 204 writeHistoryFragmentCompressed(AtomicFile file, byte[] data)205 private void writeHistoryFragmentCompressed(AtomicFile file, byte[] data) { 206 long uncompressedSize = data.length; 207 if (uncompressedSize == 0) { 208 return; 209 } 210 211 Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.compressHistoryFile"); 212 lock(); 213 FileOutputStream fos = null; 214 try { 215 long startTimeNs = System.nanoTime(); 216 fos = file.startWrite(); 217 fos.write(FILE_FORMAT_COMPRESSED_PARCEL); 218 writeInt(fos, data.length); 219 220 mCompressor.compress(fos, data); 221 file.finishWrite(fos); 222 223 if (DEBUG) { 224 long endTimeNs = System.nanoTime(); 225 long compressedSize = file.getBaseFile().length(); 226 Slog.i(TAG, String.format(Locale.ENGLISH, 227 "Compressed battery history file %s original size: %d compressed: %d " 228 + "(%.1f%%) elapsed: %.2f ms", 229 file.getBaseFile(), uncompressedSize, compressedSize, 230 (uncompressedSize - compressedSize) * 100.0 / uncompressedSize, 231 (endTimeNs - startTimeNs) / 1000000.0)); 232 } 233 } catch (Exception e) { 234 Slog.w(TAG, "Error compressing battery history chunk " + file, e); 235 file.failWrite(fos); 236 } finally { 237 unlock(); 238 Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); 239 } 240 } 241 242 @Override readFragment(BatteryHistoryFragment fragment)243 public byte[] readFragment(BatteryHistoryFragment fragment) { 244 AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; 245 if (!file.exists()) { 246 deleteFragment(fragment); 247 return null; 248 } 249 final long start = SystemClock.uptimeMillis(); 250 Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); 251 try (FileInputStream stream = file.openRead()) { 252 byte[] header = new byte[FILE_FORMAT_BYTES]; 253 if (stream.read(header, 0, FILE_FORMAT_BYTES) == -1) { 254 if (file.getBaseFile().length() == 0) { 255 return new byte[0]; 256 } 257 258 Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); 259 deleteFragment(fragment); 260 return null; 261 } 262 263 boolean isCompressed; 264 if (Arrays.equals(header, FILE_FORMAT_COMPRESSED_PARCEL)) { 265 isCompressed = true; 266 } else if (Arrays.equals(header, FILE_FORMAT_PARCEL)) { 267 isCompressed = false; 268 } else { 269 Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); 270 deleteFragment(fragment); 271 return null; 272 } 273 274 int size = readInt(stream); 275 if (size < 0 || size > 10000000) { // Validity check to avoid a crash 276 Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); 277 deleteFragment(fragment); 278 return null; 279 } 280 281 byte[] data = new byte[size]; 282 if (isCompressed) { 283 mCompressor.uncompress(data, stream); 284 } else { 285 int pos = 0; 286 while (pos < data.length) { 287 int count = stream.read(data, pos, data.length - pos); 288 if (count == -1) { 289 throw new IOException("Invalid battery history file format"); 290 } 291 pos += count; 292 } 293 } 294 if (DEBUG) { 295 Slog.d(TAG, "readHistoryFragment:" + file.getBaseFile().getPath() 296 + " duration ms:" + (SystemClock.uptimeMillis() - start)); 297 } 298 return data; 299 } catch (Exception e) { 300 Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); 301 deleteFragment(fragment); 302 return null; 303 } finally { 304 Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); 305 } 306 } 307 deleteFragment(BatteryHistoryFragment fragment)308 private void deleteFragment(BatteryHistoryFragment fragment) { 309 mHistoryFiles.remove(fragment); 310 ((BatteryHistoryFile) fragment).atomicFile.delete(); 311 } 312 313 @Override unlock()314 public void unlock() { 315 mLock.unlock(); 316 } 317 318 @Override isLocked()319 public boolean isLocked() { 320 return mLock.isLocked(); 321 } 322 ensureInitialized()323 private void ensureInitialized() { 324 if (mInitialized) { 325 return; 326 } 327 328 Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); 329 mDirectory.mkdirs(); 330 if (!mDirectory.exists()) { 331 Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); 332 } 333 334 final List<File> toRemove = new ArrayList<>(); 335 final Set<BatteryHistoryFile> dedup = new ArraySet<>(); 336 mDirectory.listFiles((dir, name) -> { 337 final int b = name.lastIndexOf(FILE_SUFFIX); 338 if (b <= 0) { 339 toRemove.add(new File(dir, name)); 340 return false; 341 } 342 try { 343 long monotonicTime = Long.parseLong(name.substring(0, b)); 344 dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime)); 345 } catch (NumberFormatException e) { 346 toRemove.add(new File(dir, name)); 347 return false; 348 } 349 return true; 350 }); 351 if (!dedup.isEmpty()) { 352 mHistoryFiles.addAll(dedup); 353 Collections.sort(mHistoryFiles); 354 } 355 mInitialized = true; 356 if (!toRemove.isEmpty()) { 357 // Clear out legacy history files, which did not follow the X-Y.bin naming format. 358 BackgroundThread.getHandler().post(() -> { 359 lock(); 360 try { 361 for (File file : toRemove) { 362 file.delete(); 363 } 364 } finally { 365 unlock(); 366 Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); 367 } 368 }); 369 } else { 370 Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); 371 } 372 } 373 374 @SuppressWarnings("unchecked") 375 @Override getFragments()376 public List<BatteryHistoryFragment> getFragments() { 377 if (!mLock.isHeldByCurrentThread()) { 378 throw new IllegalStateException("Reading battery history without a lock"); 379 } 380 381 ensureInitialized(); 382 return (List<BatteryHistoryFragment>) 383 (List<? extends BatteryHistoryFragment>) mHistoryFiles; 384 } 385 386 @VisibleForTesting getFileNames()387 List<String> getFileNames() { 388 ensureInitialized(); 389 lock(); 390 try { 391 List<String> names = new ArrayList<>(); 392 for (BatteryHistoryFile historyFile : mHistoryFiles) { 393 names.add(historyFile.atomicFile.getBaseFile().getName()); 394 } 395 return names; 396 } finally { 397 unlock(); 398 } 399 } 400 401 @Override getEarliestFragment()402 public BatteryHistoryFragment getEarliestFragment() { 403 ensureInitialized(); 404 lock(); 405 try { 406 if (!mHistoryFiles.isEmpty()) { 407 return mHistoryFiles.get(0); 408 } 409 return null; 410 } finally { 411 unlock(); 412 } 413 } 414 415 @Override getLatestFragment()416 public BatteryHistoryFragment getLatestFragment() { 417 ensureInitialized(); 418 lock(); 419 try { 420 if (!mHistoryFiles.isEmpty()) { 421 return mHistoryFiles.get(mHistoryFiles.size() - 1); 422 } 423 return null; 424 } finally { 425 unlock(); 426 } 427 } 428 429 @Override createFragment(long monotonicStartTime)430 public BatteryHistoryFragment createFragment(long monotonicStartTime) { 431 ensureInitialized(); 432 433 BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, monotonicStartTime); 434 lock(); 435 try { 436 try { 437 file.atomicFile.getBaseFile().createNewFile(); 438 } catch (IOException e) { 439 Slog.e(TAG, "Could not create history file: " + file); 440 } 441 mHistoryFiles.add(file); 442 } finally { 443 unlock(); 444 } 445 446 return file; 447 } 448 449 @Override hasCompletedFragments()450 public boolean hasCompletedFragments() { 451 ensureInitialized(); 452 453 lock(); 454 try { 455 // Active file is partial and does not count as "competed" 456 return mHistoryFiles.size() > 1; 457 } finally { 458 unlock(); 459 } 460 } 461 462 @Override getSize()463 public int getSize() { 464 ensureInitialized(); 465 466 lock(); 467 try { 468 int ret = 0; 469 for (int i = 0; i < mHistoryFiles.size() - 1; i++) { 470 ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); 471 } 472 return ret; 473 } finally { 474 unlock(); 475 } 476 } 477 478 @Override reset()479 public void reset() { 480 ensureInitialized(); 481 482 lock(); 483 try { 484 if (DEBUG) { 485 Slog.i(TAG, "********** CLEARING HISTORY!"); 486 } 487 for (BatteryHistoryFile file : mHistoryFiles) { 488 file.atomicFile.delete(); 489 } 490 mHistoryFiles.clear(); 491 } finally { 492 unlock(); 493 } 494 } 495 trim()496 private void trim() { 497 ensureInitialized(); 498 499 Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.trim"); 500 try { 501 lock(); 502 try { 503 // if there is more history stored than allowed, delete oldest history files. 504 int size = 0; 505 for (int i = 0; i < mHistoryFiles.size(); i++) { 506 size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); 507 } 508 // Trim until the directory size is within the limit or there is just one most 509 // recent file left in the directory 510 while (size > mMaxHistorySize && mHistoryFiles.size() > 1) { 511 BatteryHistoryFile oldest = mHistoryFiles.get(0); 512 int length = (int) oldest.atomicFile.getBaseFile().length(); 513 oldest.atomicFile.delete(); 514 mHistoryFiles.remove(0); 515 size -= length; 516 } 517 } finally { 518 unlock(); 519 } 520 } finally { 521 Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); 522 } 523 } 524 writeInt(OutputStream stream, int value)525 private static void writeInt(OutputStream stream, int value) throws IOException { 526 stream.write(value >> 24); 527 stream.write(value >> 16); 528 stream.write(value >> 8); 529 stream.write(value >> 0); 530 } 531 readInt(InputStream stream)532 private static int readInt(InputStream stream) throws IOException { 533 return (readByte(stream) << 24) 534 | (readByte(stream) << 16) 535 | (readByte(stream) << 8) 536 | (readByte(stream) << 0); 537 } 538 readByte(InputStream stream)539 private static int readByte(InputStream stream) throws IOException { 540 int out = stream.read(); 541 if (out == -1) { 542 throw new IOException(); 543 } 544 return out; 545 } 546 } 547