• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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