diff options
18 files changed, 1310 insertions, 554 deletions
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index f49c5f1c2b0f..036faef7aa65 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -34,45 +34,38 @@ import android.os.Build; import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Process; -import android.os.StatFs; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.util.ArraySet; -import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.locks.ReentrantLock; /** * BatteryStatsHistory encapsulates battery history files. * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into - * {@link #mActiveFile}. - * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, + * {@link #mActiveFragment}. + * When {@link #mHistoryBuffer} size reaches {@link #mMaxHistoryBufferSize}, * current mActiveFile is closed and a new mActiveFile is open. * History files are under directory /data/system/battery-history/. - * History files have name battery-history-<num>.bin. The file number <num> starts from zero and - * grows sequentially. + * History files have name <num>.bf. The file number <num> corresponds to the + * monotonic time when the file was started. * The mActiveFile is always the highest numbered history file. * The lowest number file is always the oldest file. * The highest number file is always the newest file. - * The file number grows sequentially and we never skip number. - * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES}, + * The file number grows monotonically and we never skip number. + * When the total size of history files exceeds the maximum allowed value, * the lowest numbered file is deleted and a new file is open. * * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by @@ -86,10 +79,6 @@ public class BatteryStatsHistory { // Current on-disk Parcel version. Must be updated when the format of the parcelable changes private static final int VERSION = 212; - private static final String HISTORY_DIR = "battery-history"; - private static final String FILE_SUFFIX = ".bh"; - private static final int MIN_FREE_SPACE = 100 * 1024 * 1024; - // Part of initial delta int that specifies the time delta. static final int DELTA_TIME_MASK = 0x7ffff; static final int DELTA_TIME_LONG = 0x7ffff; // The delta is a following long @@ -135,7 +124,7 @@ public class BatteryStatsHistory { // For state1, trace everything except the wakelock bit (which can race with // suspend) and the running bit (which isn't meaningful in traces). static final int STATE1_TRACE_MASK = ~(HistoryItem.STATE_WAKE_LOCK_FLAG - | HistoryItem.STATE_CPU_RUNNING_FLAG); + | HistoryItem.STATE_CPU_RUNNING_FLAG); // For state2, trace all bit changes. static final int STATE2_TRACE_MASK = ~0; @@ -146,22 +135,132 @@ public class BatteryStatsHistory { */ private static final int EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED = 100_000; + public abstract static class BatteryHistoryFragment + implements Comparable<BatteryHistoryFragment> { + public final long monotonicTimeMs; + + public BatteryHistoryFragment(long monotonicTimeMs) { + this.monotonicTimeMs = monotonicTimeMs; + } + + @Override + public int compareTo(BatteryHistoryFragment o) { + return Long.compare(monotonicTimeMs, o.monotonicTimeMs); + } + + @Override + public boolean equals(Object o) { + return monotonicTimeMs == ((BatteryHistoryFragment) o).monotonicTimeMs; + } + + @Override + public int hashCode() { + return Long.hashCode(monotonicTimeMs); + } + } + + /** + * Persistent storage for battery history fragments + */ + public interface BatteryHistoryStore { + /** + * Returns the table of contents, in the chronological order. + */ + List<BatteryHistoryFragment> getFragments(); + + /** + * Returns the earliest available fragment + */ + @Nullable + BatteryHistoryFragment getEarliestFragment(); + + /** + * Returns the latest available fragment + */ + @Nullable + BatteryHistoryFragment getLatestFragment(); + + /** + * Given a fragment, returns the earliest fragment that follows it whose monotonic + * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs` + * is exclusive. + */ + @Nullable + BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, + long endTimeMs); + + /** + * Acquires a lock on the entire store. + */ + void lock(); + + /** + * Acquires a lock unless the store is already locked by a different thread. Returns true + * if the lock has been successfully acquired. + */ + boolean tryLock(); + + /** + * Unlocks the store. + */ + void unlock(); + + /** + * Returns true if the store is currently locked. + */ + boolean isLocked(); + + /** + * Returns the total amount of storage occupied by history fragments, in bytes. + */ + int getSize(); + + /** + * Returns true if the store contains any history fragments, excluding the currently + * active partial fragment. + */ + boolean hasCompletedFragments(); + + /** + * Creates a new empty history fragment starting at the specified time. + */ + BatteryHistoryFragment createFragment(long monotonicStartTime); + + /** + * Writes a fragment to disk as raw bytes. + * + * @param fragmentComplete indicates if this fragment is done or still partial. + */ + void writeFragment(BatteryHistoryFragment fragment, @NonNull byte[] bytes, + boolean fragmentComplete); + + /** + * Reads a fragment as raw bytes. + */ + @Nullable + byte[] readFragment(BatteryHistoryFragment fragment); + + /** + * Removes all persistent fragments + */ + void reset(); + } + private final Parcel mHistoryBuffer; - private final File mSystemDir; private final HistoryStepDetailsCalculator mStepDetailsCalculator; private final Clock mClock; private int mMaxHistoryBufferSize; /** - * The active history file that the history buffer is backed up into. + * The active history fragment that the history buffer is backed up into. */ - private AtomicFile mActiveFile; + private BatteryHistoryFragment mActiveFragment; /** - * A list of history files with increasing timestamps. + * Persistent storage of history files. */ - private final BatteryHistoryDirectory mHistoryDir; + private final BatteryHistoryStore mStore; /** * A list of small history parcels, used when BatteryStatsImpl object is created from @@ -172,7 +271,7 @@ public class BatteryStatsHistory { /** * When iterating history files, the current file index. */ - private BatteryHistoryFile mCurrentFile; + private BatteryHistoryFragment mCurrentFragment; /** * When iterating history files, the current file parcel. @@ -221,326 +320,6 @@ public class BatteryStatsHistory { private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; - private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> { - public final long monotonicTimeMs; - public final AtomicFile atomicFile; - - private BatteryHistoryFile(File directory, long monotonicTimeMs) { - this.monotonicTimeMs = monotonicTimeMs; - atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX)); - } - - @Override - public int compareTo(BatteryHistoryFile o) { - return Long.compare(monotonicTimeMs, o.monotonicTimeMs); - } - - @Override - public boolean equals(Object o) { - return monotonicTimeMs == ((BatteryHistoryFile) o).monotonicTimeMs; - } - - @Override - public int hashCode() { - return Long.hashCode(monotonicTimeMs); - } - - @Override - public String toString() { - return atomicFile.getBaseFile().toString(); - } - } - - private static class BatteryHistoryDirectory { - private final File mDirectory; - private final MonotonicClock mMonotonicClock; - private int mMaxHistorySize; - private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>(); - private final ReentrantLock mLock = new ReentrantLock(); - private boolean mCleanupNeeded; - - BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock, int maxHistorySize) { - mDirectory = directory; - mMonotonicClock = monotonicClock; - mMaxHistorySize = maxHistorySize; - if (mMaxHistorySize == 0) { - Slog.w(TAG, "mMaxHistorySize should not be zero when writing history"); - } - } - - void setMaxHistorySize(int maxHistorySize) { - mMaxHistorySize = maxHistorySize; - cleanup(); - } - - void lock() { - mLock.lock(); - } - - boolean tryLock() { - return mLock.tryLock(); - } - - void unlock() { - mLock.unlock(); - if (mCleanupNeeded) { - cleanup(); - } - } - - boolean isLocked() { - return mLock.isLocked(); - } - - void load() { - Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - mDirectory.mkdirs(); - if (!mDirectory.exists()) { - Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); - } - - final List<File> toRemove = new ArrayList<>(); - final Set<BatteryHistoryFile> dedup = new ArraySet<>(); - mDirectory.listFiles((dir, name) -> { - final int b = name.lastIndexOf(FILE_SUFFIX); - if (b <= 0) { - toRemove.add(new File(dir, name)); - return false; - } - try { - long monotonicTime = Long.parseLong(name.substring(0, b)); - dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime)); - } catch (NumberFormatException e) { - toRemove.add(new File(dir, name)); - return false; - } - return true; - }); - if (!dedup.isEmpty()) { - mHistoryFiles.addAll(dedup); - Collections.sort(mHistoryFiles); - } - if (!toRemove.isEmpty()) { - // Clear out legacy history files, which did not follow the X-Y.bin naming format. - BackgroundThread.getHandler().post(() -> { - lock(); - try { - for (File file : toRemove) { - file.delete(); - } - } finally { - unlock(); - Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - } - }); - } else { - Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - } - } - - List<String> getFileNames() { - lock(); - try { - List<String> names = new ArrayList<>(); - for (BatteryHistoryFile historyFile : mHistoryFiles) { - names.add(historyFile.atomicFile.getBaseFile().getName()); - } - return names; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getFirstFile() { - lock(); - try { - if (!mHistoryFiles.isEmpty()) { - return mHistoryFiles.get(0); - } - return null; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getLastFile() { - lock(); - try { - if (!mHistoryFiles.isEmpty()) { - return mHistoryFiles.get(mHistoryFiles.size() - 1); - } - return null; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getNextFile(BatteryHistoryFile current, long startTimeMs, - long endTimeMs) { - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("Iterating battery history without a lock"); - } - - int nextFileIndex = 0; - int firstFileIndex = 0; - // skip the last file because its data is in history buffer. - int lastFileIndex = mHistoryFiles.size() - 2; - for (int i = lastFileIndex; i >= 0; i--) { - BatteryHistoryFile file = mHistoryFiles.get(i); - if (current != null && file.monotonicTimeMs == current.monotonicTimeMs) { - nextFileIndex = i + 1; - } - if (file.monotonicTimeMs > endTimeMs) { - lastFileIndex = i - 1; - } - if (file.monotonicTimeMs <= startTimeMs) { - firstFileIndex = i; - break; - } - } - - if (nextFileIndex < firstFileIndex) { - nextFileIndex = firstFileIndex; - } - - if (nextFileIndex <= lastFileIndex) { - return mHistoryFiles.get(nextFileIndex); - } - - return null; - } - - BatteryHistoryFile makeBatteryHistoryFile() { - BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, - mMonotonicClock.monotonicTime()); - lock(); - try { - mHistoryFiles.add(file); - } finally { - unlock(); - } - return file; - } - - void writeToParcel(Parcel out, boolean useBlobs, - long preferredEarliestIncludedTimestampMs) { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); - lock(); - try { - final long start = SystemClock.uptimeMillis(); - for (int i = 0; i < mHistoryFiles.size() - 1; i++) { - long monotonicEndTime = Long.MAX_VALUE; - if (i < mHistoryFiles.size() - 1) { - monotonicEndTime = mHistoryFiles.get(i + 1).monotonicTimeMs; - } - - if (monotonicEndTime < preferredEarliestIncludedTimestampMs) { - continue; - } - - AtomicFile file = mHistoryFiles.get(i).atomicFile; - byte[] raw = new byte[0]; - try { - raw = file.readFully(); - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - } - - out.writeBoolean(true); - if (useBlobs) { - out.writeBlob(raw); - } else { - // Avoiding blobs in the check-in file for compatibility - out.writeByteArray(raw); - } - } - out.writeBoolean(false); - if (DEBUG) { - Slog.d(TAG, - "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); - } - } finally { - unlock(); - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); - } - } - - int getFileCount() { - lock(); - try { - return mHistoryFiles.size(); - } finally { - unlock(); - } - } - - int getSize() { - lock(); - try { - int ret = 0; - for (int i = 0; i < mHistoryFiles.size() - 1; i++) { - ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); - } - return ret; - } finally { - unlock(); - } - } - - void reset() { - lock(); - try { - if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!"); - for (BatteryHistoryFile file : mHistoryFiles) { - file.atomicFile.delete(); - } - mHistoryFiles.clear(); - } finally { - unlock(); - } - } - - private void cleanup() { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup"); - try { - if (mDirectory == null) { - return; - } - - if (!tryLock()) { - mCleanupNeeded = true; - return; - } - - mCleanupNeeded = false; - try { - // if free disk space is less than 100MB, delete oldest history file. - if (!hasFreeDiskSpace(mDirectory)) { - BatteryHistoryFile oldest = mHistoryFiles.remove(0); - oldest.atomicFile.delete(); - } - - // if there is more history stored than allowed, delete oldest history files. - int size = getSize(); - while (size > mMaxHistorySize) { - BatteryHistoryFile oldest = mHistoryFiles.get(0); - int length = (int) oldest.atomicFile.getBaseFile().length(); - oldest.atomicFile.delete(); - mHistoryFiles.remove(0); - size -= length; - } - } finally { - unlock(); - } - } finally { - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); - } - } - } - /** * A delegate responsible for computing additional details for a step in battery history. */ @@ -621,24 +400,22 @@ public class BatteryStatsHistory { /** * Constructor * - * @param systemDir typically /data/system - * @param maxHistorySize the largest amount of battery history to keep on disk * @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps */ - public BatteryStatsHistory(Parcel historyBuffer, File systemDir, - int maxHistorySize, int maxHistoryBufferSize, - HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, - MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) { - this(historyBuffer, systemDir, maxHistorySize, maxHistoryBufferSize, stepDetailsCalculator, + public BatteryStatsHistory(Parcel historyBuffer, int maxHistoryBufferSize, + @Nullable BatteryHistoryStore store, HistoryStepDetailsCalculator stepDetailsCalculator, + Clock clock, MonotonicClock monotonicClock, TraceDelegate tracer, + EventLogger eventLogger) { + this(historyBuffer, maxHistoryBufferSize, store, + stepDetailsCalculator, clock, monotonicClock, tracer, eventLogger, null); } - private BatteryStatsHistory(@Nullable Parcel historyBuffer, @Nullable File systemDir, - int maxHistorySize, int maxHistoryBufferSize, + private BatteryStatsHistory(@Nullable Parcel historyBuffer, int maxHistoryBufferSize, + @Nullable BatteryHistoryStore store, @NonNull HistoryStepDetailsCalculator stepDetailsCalculator, @NonNull Clock clock, @NonNull MonotonicClock monotonicClock, @NonNull TraceDelegate tracer, @NonNull EventLogger eventLogger, @Nullable BatteryStatsHistory writableHistory) { - mSystemDir = systemDir; mMaxHistoryBufferSize = maxHistoryBufferSize; mStepDetailsCalculator = stepDetailsCalculator; mTracer = tracer; @@ -659,18 +436,16 @@ public class BatteryStatsHistory { } if (writableHistory != null) { - mHistoryDir = writableHistory.mHistoryDir; - } else if (systemDir != null) { - mHistoryDir = new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR), - monotonicClock, maxHistorySize); - mHistoryDir.load(); - BatteryHistoryFile activeFile = mHistoryDir.getLastFile(); - if (activeFile == null) { - activeFile = mHistoryDir.makeBatteryHistoryFile(); - } - setActiveFile(activeFile); + mStore = writableHistory.mStore; } else { - mHistoryDir = null; + mStore = store; + if (mStore != null) { + BatteryHistoryFragment activeFile = mStore.getLatestFragment(); + if (activeFile == null) { + activeFile = mStore.createFragment(mMonotonicClock.monotonicTime()); + } + setActiveFragment(activeFile); + } } } @@ -681,8 +456,7 @@ public class BatteryStatsHistory { private BatteryStatsHistory(Parcel parcel) { mClock = Clock.SYSTEM_CLOCK; mTracer = null; - mSystemDir = null; - mHistoryDir = null; + mStore = null; mStepDetailsCalculator = null; mEventLogger = new EventLogger(); mWritableHistory = null; @@ -718,15 +492,6 @@ public class BatteryStatsHistory { } /** - * Changes the maximum amount of history to be kept on disk. - */ - public void setMaxHistorySize(int maxHistorySize) { - if (mHistoryDir != null) { - mHistoryDir.setMaxHistorySize(maxHistorySize); - } - } - - /** * Changes the maximum size of the history buffer, in bytes. */ public void setMaxHistoryBufferSize(int maxHistoryBufferSize) { @@ -745,8 +510,8 @@ public class BatteryStatsHistory { Parcel historyBufferCopy = Parcel.obtain(); historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, - null, null, mEventLogger, this); + return new BatteryStatsHistory(historyBufferCopy, 0, mStore, null, + null, null, null, mEventLogger, this); } } finally { Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); @@ -757,45 +522,40 @@ public class BatteryStatsHistory { * Returns true if this instance only supports reading history. */ public boolean isReadOnly() { - return !mMutable || mActiveFile == null/* || mHistoryDir == null*/; + return !mMutable || mActiveFragment == null || mStore == null; } /** * Set the active file that mHistoryBuffer is backed up into. */ - private void setActiveFile(BatteryHistoryFile file) { - mActiveFile = file.atomicFile; + private void setActiveFragment(BatteryHistoryFragment file) { + mActiveFragment = file; if (DEBUG) { - Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath()); + Slog.d(TAG, "activeHistoryFile:" + mActiveFragment); } } /** - * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, - * create next history file. + * When {@link #mHistoryBuffer} reaches {@link #mMaxHistoryBufferSize}, + * create next history fragment. */ - public void startNextFile(long elapsedRealtimeMs) { + public void startNextFragment(long elapsedRealtimeMs) { synchronized (this) { - startNextFileLocked(elapsedRealtimeMs); + startNextFragmentLocked(elapsedRealtimeMs); } } @GuardedBy("this") - private void startNextFileLocked(long elapsedRealtimeMs) { + private void startNextFragmentLocked(long elapsedRealtimeMs) { final long start = SystemClock.uptimeMillis(); - writeHistory(); + writeHistory(true /* fragmentComplete */); if (DEBUG) { Slog.d(TAG, "writeHistory took ms:" + (SystemClock.uptimeMillis() - start)); } - setActiveFile(mHistoryDir.makeBatteryHistoryFile()); - try { - mActiveFile.getBaseFile().createNewFile(); - } catch (IOException e) { - Slog.e(TAG, "Could not create history file: " + mActiveFile.getBaseFile()); - } - - mHistoryBufferStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs); + long monotonicStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs); + setActiveFragment(mStore.createFragment(monotonicStartTime)); + mHistoryBufferStartTime = monotonicStartTime; mHistoryBuffer.setDataSize(0); mHistoryBuffer.setDataPosition(0); mHistoryBuffer.setDataCapacity(mMaxHistoryBufferSize / 2); @@ -810,7 +570,6 @@ public class BatteryStatsHistory { } mWrittenPowerStatsDescriptors.clear(); - mHistoryDir.cleanup(); } /** @@ -818,7 +577,7 @@ public class BatteryStatsHistory { * currently being read. */ public boolean isResetEnabled() { - return mHistoryDir == null || !mHistoryDir.isLocked(); + return mStore == null || !mStore.isLocked(); } /** @@ -827,11 +586,11 @@ public class BatteryStatsHistory { */ public void reset() { synchronized (this) { - if (mHistoryDir != null) { - mHistoryDir.reset(); - setActiveFile(mHistoryDir.makeBatteryHistoryFile()); - } initHistoryBuffer(); + if (mStore != null) { + mStore.reset(); + setActiveFragment(mStore.createFragment(mHistoryBufferStartTime)); + } } } @@ -840,9 +599,9 @@ public class BatteryStatsHistory { */ public long getStartTime() { synchronized (this) { - BatteryHistoryFile file = mHistoryDir.getFirstFile(); - if (file != null) { - return file.monotonicTimeMs; + BatteryHistoryFragment firstFragment = mStore.getEarliestFragment(); + if (firstFragment != null) { + return firstFragment.monotonicTimeMs; } else { return mHistoryBufferStartTime; } @@ -863,10 +622,10 @@ public class BatteryStatsHistory { return copy().iterate(startTimeMs, endTimeMs); } - if (mHistoryDir != null) { - mHistoryDir.lock(); + if (mStore != null) { + mStore.lock(); } - mCurrentFile = null; + mCurrentFragment = null; mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; @@ -883,8 +642,8 @@ public class BatteryStatsHistory { */ void iteratorFinished() { mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); - if (mHistoryDir != null) { - mHistoryDir.unlock(); + if (mStore != null) { + mStore.unlock(); } Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", mIteratorCookie); @@ -918,27 +677,26 @@ public class BatteryStatsHistory { } } - if (mHistoryDir != null) { - BatteryHistoryFile nextFile = mHistoryDir.getNextFile(mCurrentFile, startTimeMs, + if (mStore != null) { + BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs, endTimeMs); - while (nextFile != null) { + while (next != null) { mCurrentParcel = null; mCurrentParcelEnd = 0; final Parcel p = Parcel.obtain(); - AtomicFile file = nextFile.atomicFile; - if (readFileToParcel(p, file)) { + if (readFragmentToParcel(p, next)) { int bufSize = p.readInt(); int curPos = p.dataPosition(); mCurrentParcelEnd = curPos + bufSize; mCurrentParcel = p; if (curPos < mCurrentParcelEnd) { - mCurrentFile = nextFile; + mCurrentFragment = next; return mCurrentParcel; } } else { p.recycle(); } - nextFile = mHistoryDir.getNextFile(nextFile, startTimeMs, endTimeMs); + next = mStore.getNextFragment(next, startTimeMs, endTimeMs); } } @@ -988,39 +746,26 @@ public class BatteryStatsHistory { * Read history file into a parcel. * * @param out the Parcel read into. - * @param file the File to read from. + * @param fragment the fragment to read from. * @return true if success, false otherwise. */ - public boolean readFileToParcel(Parcel out, AtomicFile file) { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); - try { - byte[] raw = null; - try { - final long start = SystemClock.uptimeMillis(); - raw = file.readFully(); - if (DEBUG) { - Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - start)); - } - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - return false; - } - out.unmarshall(raw, 0, raw.length); - out.setDataPosition(0); - if (!verifyVersion(out)) { - return false; - } - // skip monotonic time field. - out.readLong(); - // skip monotonic end time field - out.readLong(); - // skip monotonic size field - out.readLong(); - return true; - } finally { - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + public boolean readFragmentToParcel(Parcel out, BatteryHistoryFragment fragment) { + byte[] data = mStore.readFragment(fragment); + if (data == null) { + return false; + } + out.unmarshall(data, 0, data.length); + out.setDataPosition(0); + if (!verifyVersion(out)) { + return false; } + // skip monotonic time field. + out.readLong(); + // skip monotonic end time field + out.readLong(); + // skip monotonic size field + out.readLong(); + return true; } /** @@ -1106,9 +851,8 @@ public class BatteryStatsHistory { public void writeToParcel(Parcel out) { synchronized (this) { writeHistoryBuffer(out); - /* useBlobs */ - if (mHistoryDir != null) { - mHistoryDir.writeToParcel(out, false /* useBlobs */, 0); + if (mStore != null) { + writeToParcel(out, false /* useBlobs */, 0); } } } @@ -1122,13 +866,54 @@ public class BatteryStatsHistory { public void writeToBatteryUsageStatsParcel(Parcel out, long preferredHistoryDurationMs) { synchronized (this) { out.writeBlob(mHistoryBuffer.marshall()); - if (mHistoryDir != null) { - mHistoryDir.writeToParcel(out, true /* useBlobs */, + if (mStore != null) { + writeToParcel(out, true /* useBlobs */, mHistoryMonotonicEndTime - preferredHistoryDurationMs); } } } + private void writeToParcel(Parcel out, boolean useBlobs, + long preferredEarliestIncludedTimestampMs) { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); + mStore.lock(); + try { + final long start = SystemClock.uptimeMillis(); + List<BatteryHistoryFragment> fragments = mStore.getFragments(); + for (int i = 0; i < fragments.size() - 1; i++) { + long monotonicEndTime = Long.MAX_VALUE; + if (i < fragments.size() - 1) { + monotonicEndTime = fragments.get(i + 1).monotonicTimeMs; + } + + if (monotonicEndTime < preferredEarliestIncludedTimestampMs) { + continue; + } + + byte[] data = mStore.readFragment(fragments.get(i)); + if (data == null) { + Slog.e(TAG, "Error reading history fragment " + fragments.get(i)); + continue; + } + + out.writeBoolean(true); + if (useBlobs) { + out.writeBlob(data, 0, data.length); + } else { + // Avoiding blobs in the check-in file for compatibility + out.writeByteArray(data, 0, data.length); + } + } + out.writeBoolean(false); + if (DEBUG) { + Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); + } + } finally { + mStore.unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + /** * Reads a BatteryStatsHistory from a parcel written with * the {@link #writeToBatteryUsageStatsParcel} method. @@ -1141,28 +926,21 @@ public class BatteryStatsHistory { * Read history from a check-in file. */ public boolean readSummary() { - if (mActiveFile == null) { + if (mActiveFragment == null) { Slog.w(TAG, "readSummary: no history file associated with this instance"); return false; } Parcel parcel = Parcel.obtain(); try { - final long start = SystemClock.uptimeMillis(); - if (mActiveFile.exists()) { - byte[] raw = mActiveFile.readFully(); - if (raw.length > 0) { - parcel.unmarshall(raw, 0, raw.length); - parcel.setDataPosition(0); - readHistoryBuffer(parcel); - } - if (DEBUG) { - Slog.d(TAG, "read history file::" - + mActiveFile.getBaseFile().getPath() - + " bytes:" + raw.length + " took ms:" + (SystemClock.uptimeMillis() - - start)); - } + byte[] data = mStore.readFragment(mActiveFragment); + if (data == null) { + return false; } + + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + readHistoryBuffer(parcel); } catch (Exception e) { Slog.e(TAG, "Error reading battery history", e); reset(); @@ -1201,41 +979,21 @@ public class BatteryStatsHistory { } } - /** - * @return true if there is more than 100MB free disk space left. - */ - @android.ravenwood.annotation.RavenwoodReplace - private static boolean hasFreeDiskSpace(File systemDir) { - final StatFs stats = new StatFs(systemDir.getAbsolutePath()); - return stats.getAvailableBytes() > MIN_FREE_SPACE; - } - - private static boolean hasFreeDiskSpace$ravenwood(File systemDir) { - return true; - } - @VisibleForTesting - public List<String> getFilesNames() { - return mHistoryDir.getFileNames(); + public BatteryHistoryStore getBatteryHistoryStore() { + return mStore; } @VisibleForTesting - public AtomicFile getActiveFile() { - return mActiveFile; - } - - /** - * Returns the maximum storage size allocated to battery history. - */ - public int getMaxHistorySize() { - return mHistoryDir.mMaxHistorySize; + public BatteryHistoryFragment getActiveFragment() { + return mActiveFragment; } /** * @return the total size of all history files and history buffer. */ public int getHistoryUsedSize() { - int ret = mHistoryDir.getSize(); + int ret = mStore.getSize(); ret += mHistoryBuffer.dataSize(); if (mHistoryParcels != null) { for (int i = 0; i < mHistoryParcels.size(); i++) { @@ -1293,7 +1051,7 @@ public class BatteryStatsHistory { */ public void continueRecordingHistory() { synchronized (this) { - if (mHistoryBuffer.dataPosition() <= 0 && mHistoryDir.getFileCount() <= 1) { + if (mHistoryBuffer.dataPosition() <= 0 && !mStore.hasCompletedFragments()) { return; } @@ -1852,7 +1610,7 @@ public class BatteryStatsHistory { } final long timeDiffMs = mMonotonicClock.monotonicTime(elapsedRealtimeMs) - - mHistoryLastWritten.time; + - mHistoryLastWritten.time; final int diffStates = mHistoryLastWritten.states ^ cur.states; final int diffStates2 = mHistoryLastWritten.states2 ^ cur.states2; final int lastDiffStates = mHistoryLastWritten.states ^ mHistoryLastLastWritten.states; @@ -1953,7 +1711,7 @@ public class BatteryStatsHistory { mMaxHistoryBufferSize = 1024; } - boolean successfullyLocked = mHistoryDir.tryLock(); + boolean successfullyLocked = mStore.tryLock(); if (!successfullyLocked) { // Already locked by another thread // If the buffer size is below the allowed overflow limit, just keep going if (dataSize < mMaxHistoryBufferSize + EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED) { @@ -1971,10 +1729,10 @@ public class BatteryStatsHistory { copy.setTo(cur); try { - startNextFile(elapsedRealtimeMs); + startNextFragment(elapsedRealtimeMs); } finally { if (successfullyLocked) { - mHistoryDir.unlock(); + mStore.unlock(); } } @@ -2095,6 +1853,7 @@ public class BatteryStatsHistory { Battery charge int: if F in the first token is set, an int representing the battery charge in coulombs follows. */ + /** * Writes the delta between the previous and current history items into history buffer. */ @@ -2376,9 +2135,13 @@ public class BatteryStatsHistory { } /** - * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} . + * Saves the accumulated history buffer in the active file, see {@link #getActiveFragment()} . */ public void writeHistory() { + writeHistory(false /* fragmentComplete */); + } + + private void writeHistory(boolean fragmentComplete) { synchronized (this) { if (isReadOnly()) { Slog.w(TAG, "writeHistory: this instance instance is read-only"); @@ -2397,7 +2160,7 @@ public class BatteryStatsHistory { Slog.d(TAG, "writeHistoryBuffer duration ms:" + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize()); } - writeParcelToFileLocked(p, mActiveFile); + writeParcelLocked(p, mActiveFragment, fragmentComplete); } finally { p.recycle(); } @@ -2457,30 +2220,18 @@ public class BatteryStatsHistory { } @GuardedBy("this") - private void writeParcelToFileLocked(Parcel p, AtomicFile file) { - FileOutputStream fos = null; + private void writeParcelLocked(Parcel p, BatteryHistoryFragment fragment, + boolean fragmentComplete) { mWriteLock.lock(); try { final long startTimeMs = SystemClock.uptimeMillis(); - fos = file.startWrite(); - fos.write(p.marshall()); - fos.flush(); - file.finishWrite(fos); - if (DEBUG) { - Slog.d(TAG, "writeParcelToFileLocked file:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) - + " bytes:" + p.dataSize()); - } + mStore.writeFragment(fragment, p.marshall(), fragmentComplete); mEventLogger.writeCommitSysConfigFile(startTimeMs); - } catch (IOException e) { - Slog.w(TAG, "Error writing battery statistics", e); - file.failWrite(fos); } finally { mWriteLock.unlock(); } } - /** * Returns the total number of history tags in the tag pool. */ diff --git a/services/core/Android.bp b/services/core/Android.bp index 420dcfe9cea6..9b0caf561544 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -227,6 +227,7 @@ java_library_static { "com.android.sysprop.watchdog", "securebox", "apache-commons-math", + "apache-commons-compress", "battery_saver_flag_lib", "notification_flags_lib", "power_hint_flags_lib", diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 644077a7e6bb..c8b0a57fe9f0 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -526,6 +526,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } public void systemServicesReady() { + mStats.setBatteryHistoryCompressionEnabled( + Flags.extendedBatteryHistoryCompressionEnabled()); mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore, isBatteryUsageStatsAccumulationSupported()); mStats.resetBatteryHistoryOnNewSession( diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java new file mode 100644 index 000000000000..adf308a522ed --- /dev/null +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; + +import android.annotation.NonNull; +import android.os.SystemClock; +import android.os.Trace; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.os.BatteryStatsHistory; +import com.android.internal.os.BatteryStatsHistory.BatteryHistoryFragment; + +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipParameters; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.Deflater; +import java.util.zip.GZIPInputStream; + +public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHistoryStore { + public static final String TAG = "BatteryHistoryDirectory"; + private static final boolean DEBUG = false; + + private static final String FILE_SUFFIX = ".bh"; + + // Size of the magic number written at the start of each history file + private static final int FILE_FORMAT_BYTES = 4; + private static final byte[] FILE_FORMAT_PARCEL = {0x50, 0x52, 0x43, 0x4c}; // PRCL + private static final byte[] FILE_FORMAT_COMPRESSED_PARCEL = {0x47, 0x5a, 0x49, 0x50}; // GZIP + + static class BatteryHistoryFile extends BatteryHistoryFragment { + public final AtomicFile atomicFile; + + BatteryHistoryFile(File directory, long monotonicTimeMs) { + super(monotonicTimeMs); + atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX)); + } + + @Override + public String toString() { + return atomicFile.getBaseFile().toString(); + } + } + + interface Compressor { + void compress(OutputStream stream, byte[] data) throws IOException; + void uncompress(byte[] data, InputStream stream) throws IOException; + + default void readFully(byte[] data, InputStream stream) throws IOException { + int pos = 0; + while (pos < data.length) { + int count = stream.read(data, pos, data.length - pos); + if (count == -1) { + throw new IOException("Invalid battery history file format"); + } + pos += count; + } + } + } + + static final Compressor DEFAULT_COMPRESSOR = new Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + // With the BEST_SPEED hint, we see ~4x improvement in write latency over + // GZIPOutputStream. + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + GzipCompressorOutputStream os = new GzipCompressorOutputStream(stream, parameters); + os.write(data); + os.finish(); + os.flush(); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, new GZIPInputStream(stream)); + } + }; + + private final File mDirectory; + private int mMaxHistorySize; + private boolean mInitialized; + private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>(); + private final ReentrantLock mLock = new ReentrantLock(); + private final Compressor mCompressor; + private boolean mWaitForDirectoryLock = false; + private boolean mFileCompressionEnabled; + + public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize) { + this(directory, maxHistorySize, DEFAULT_COMPRESSOR); + } + + public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize, + Compressor compressor) { + mDirectory = directory; + mMaxHistorySize = maxHistorySize; + if (mMaxHistorySize == 0) { + Slog.w(TAG, "maxHistorySize should not be zero"); + } + mCompressor = compressor; + } + + public void setFileCompressionEnabled(boolean enabled) { + mFileCompressionEnabled = enabled; + } + + void setMaxHistorySize(int maxHistorySize) { + mMaxHistorySize = maxHistorySize; + trim(); + } + + /** + * Returns the maximum storage size allocated to battery history. + */ + public int getMaxHistorySize() { + return mMaxHistorySize; + } + + @Override + public void lock() { + mLock.lock(); + } + + /** + * Turns "tryLock" into "lock" to prevent flaky unit tests. + * Should only be called from unit tests. + */ + @VisibleForTesting + void makeDirectoryLockUnconditional() { + mWaitForDirectoryLock = true; + } + + @Override + public boolean tryLock() { + if (mWaitForDirectoryLock) { + mLock.lock(); + return true; + } + return mLock.tryLock(); + } + + @Override + public void writeFragment(BatteryHistoryFragment fragment, + @NonNull byte[] data, boolean fragmentComplete) { + AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; + FileOutputStream fos = null; + try { + final long startTimeMs = SystemClock.uptimeMillis(); + fos = file.startWrite(); + fos.write(FILE_FORMAT_PARCEL); + writeInt(fos, data.length); + fos.write(data); + fos.flush(); + file.finishWrite(fos); + if (DEBUG) { + Slog.d(TAG, "writeHistoryFragment file:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) + + " bytes:" + data.length); + } + if (fragmentComplete) { + if (mFileCompressionEnabled) { + BackgroundThread.getHandler().post( + () -> writeHistoryFragmentCompressed(file, data)); + } + BackgroundThread.getHandler().post(()-> trim()); + } + } catch (IOException e) { + Slog.w(TAG, "Error writing battery history fragment", e); + file.failWrite(fos); + } + } + + private void writeHistoryFragmentCompressed(AtomicFile file, byte[] data) { + long uncompressedSize = data.length; + if (uncompressedSize == 0) { + return; + } + + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.compressHistoryFile"); + lock(); + FileOutputStream fos = null; + try { + long startTimeNs = System.nanoTime(); + fos = file.startWrite(); + fos.write(FILE_FORMAT_COMPRESSED_PARCEL); + writeInt(fos, data.length); + + mCompressor.compress(fos, data); + file.finishWrite(fos); + + if (DEBUG) { + long endTimeNs = System.nanoTime(); + long compressedSize = file.getBaseFile().length(); + Slog.i(TAG, String.format(Locale.ENGLISH, + "Compressed battery history file %s original size: %d compressed: %d " + + "(%.1f%%) elapsed: %.2f ms", + file.getBaseFile(), uncompressedSize, compressedSize, + (uncompressedSize - compressedSize) * 100.0 / uncompressedSize, + (endTimeNs - startTimeNs) / 1000000.0)); + } + } catch (Exception e) { + Slog.w(TAG, "Error compressing battery history chunk " + file, e); + file.failWrite(fos); + } finally { + unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + @Override + public byte[] readFragment(BatteryHistoryFragment fragment) { + AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; + if (!file.exists()) { + deleteFragment(fragment); + return null; + } + final long start = SystemClock.uptimeMillis(); + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); + try (FileInputStream stream = file.openRead()) { + byte[] header = new byte[FILE_FORMAT_BYTES]; + if (stream.read(header, 0, FILE_FORMAT_BYTES) == -1) { + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + boolean isCompressed; + if (Arrays.equals(header, FILE_FORMAT_COMPRESSED_PARCEL)) { + isCompressed = true; + } else if (Arrays.equals(header, FILE_FORMAT_PARCEL)) { + isCompressed = false; + } else { + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + int size = readInt(stream); + if (size < 0 || size > 10000000) { // Validity check to avoid a crash + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + byte[] data = new byte[size]; + if (isCompressed) { + mCompressor.uncompress(data, stream); + } else { + int pos = 0; + while (pos < data.length) { + int count = stream.read(data, pos, data.length - pos); + if (count == -1) { + throw new IOException("Invalid battery history file format"); + } + pos += count; + } + } + if (DEBUG) { + Slog.d(TAG, "readHistoryFragment:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - start)); + } + return data; + } catch (Exception e) { + Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); + deleteFragment(fragment); + return null; + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + private void deleteFragment(BatteryHistoryFragment fragment) { + mHistoryFiles.remove(fragment); + ((BatteryHistoryFile) fragment).atomicFile.delete(); + } + + @Override + public void unlock() { + mLock.unlock(); + } + + @Override + public boolean isLocked() { + return mLock.isLocked(); + } + + private void ensureInitialized() { + if (mInitialized) { + return; + } + + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + mDirectory.mkdirs(); + if (!mDirectory.exists()) { + Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); + } + + final List<File> toRemove = new ArrayList<>(); + final Set<BatteryHistoryFile> dedup = new ArraySet<>(); + mDirectory.listFiles((dir, name) -> { + final int b = name.lastIndexOf(FILE_SUFFIX); + if (b <= 0) { + toRemove.add(new File(dir, name)); + return false; + } + try { + long monotonicTime = Long.parseLong(name.substring(0, b)); + dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime)); + } catch (NumberFormatException e) { + toRemove.add(new File(dir, name)); + return false; + } + return true; + }); + if (!dedup.isEmpty()) { + mHistoryFiles.addAll(dedup); + Collections.sort(mHistoryFiles); + } + mInitialized = true; + if (!toRemove.isEmpty()) { + // Clear out legacy history files, which did not follow the X-Y.bin naming format. + BackgroundThread.getHandler().post(() -> { + lock(); + try { + for (File file : toRemove) { + file.delete(); + } + } finally { + unlock(); + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + } + }); + } else { + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + } + } + + @SuppressWarnings("unchecked") + @Override + public List<BatteryHistoryFragment> getFragments() { + ensureInitialized(); + return (List<BatteryHistoryFragment>) + (List<? extends BatteryHistoryFragment>) mHistoryFiles; + } + + @VisibleForTesting + List<String> getFileNames() { + ensureInitialized(); + lock(); + try { + List<String> names = new ArrayList<>(); + for (BatteryHistoryFile historyFile : mHistoryFiles) { + names.add(historyFile.atomicFile.getBaseFile().getName()); + } + return names; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment getEarliestFragment() { + ensureInitialized(); + lock(); + try { + if (!mHistoryFiles.isEmpty()) { + return mHistoryFiles.get(0); + } + return null; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment getLatestFragment() { + ensureInitialized(); + lock(); + try { + if (!mHistoryFiles.isEmpty()) { + return mHistoryFiles.get(mHistoryFiles.size() - 1); + } + return null; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment createFragment(long monotonicStartTime) { + ensureInitialized(); + + BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, monotonicStartTime); + lock(); + try { + try { + file.atomicFile.getBaseFile().createNewFile(); + } catch (IOException e) { + Slog.e(TAG, "Could not create history file: " + file); + } + mHistoryFiles.add(file); + } finally { + unlock(); + } + + return file; + } + + @Override + public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, + long endTimeMs) { + ensureInitialized(); + + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Iterating battery history without a lock"); + } + + int nextFileIndex = 0; + int firstFileIndex = 0; + // skip the last file because its data is in history buffer. + int lastFileIndex = mHistoryFiles.size() - 2; + for (int i = lastFileIndex; i >= 0; i--) { + BatteryHistoryFragment fragment = mHistoryFiles.get(i); + if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) { + nextFileIndex = i + 1; + } + if (fragment.monotonicTimeMs > endTimeMs) { + lastFileIndex = i - 1; + } + if (fragment.monotonicTimeMs <= startTimeMs) { + firstFileIndex = i; + break; + } + } + + if (nextFileIndex < firstFileIndex) { + nextFileIndex = firstFileIndex; + } + + if (nextFileIndex <= lastFileIndex) { + return mHistoryFiles.get(nextFileIndex); + } + + return null; + } + + @Override + public boolean hasCompletedFragments() { + ensureInitialized(); + + lock(); + try { + // Active file is partial and does not count as "competed" + return mHistoryFiles.size() > 1; + } finally { + unlock(); + } + } + + @Override + public int getSize() { + ensureInitialized(); + + lock(); + try { + int ret = 0; + for (int i = 0; i < mHistoryFiles.size() - 1; i++) { + ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); + } + return ret; + } finally { + unlock(); + } + } + + @Override + public void reset() { + ensureInitialized(); + + lock(); + try { + if (DEBUG) { + Slog.i(TAG, "********** CLEARING HISTORY!"); + } + for (BatteryHistoryFile file : mHistoryFiles) { + file.atomicFile.delete(); + } + mHistoryFiles.clear(); + } finally { + unlock(); + } + } + + private void trim() { + ensureInitialized(); + + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.trim"); + try { + lock(); + try { + // if there is more history stored than allowed, delete oldest history files. + int size = 0; + for (int i = 0; i < mHistoryFiles.size(); i++) { + size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); + } + while (size > mMaxHistorySize) { + BatteryHistoryFile oldest = mHistoryFiles.get(0); + int length = (int) oldest.atomicFile.getBaseFile().length(); + oldest.atomicFile.delete(); + mHistoryFiles.remove(0); + size -= length; + } + } finally { + unlock(); + } + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + private static void writeInt(OutputStream stream, int value) throws IOException { + stream.write(value >> 24); + stream.write(value >> 16); + stream.write(value >> 8); + stream.write(value >> 0); + } + + private static int readInt(InputStream stream) throws IOException { + return (readByte(stream) << 24) + | (readByte(stream) << 16) + | (readByte(stream) << 8) + | (readByte(stream) << 0); + } + + private static int readByte(InputStream stream) throws IOException { + int out = stream.read(); + if (out == -1) { + throw new IOException(); + } + return out; + } +} diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 68768b8fa223..90bc54b06c0a 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -195,6 +195,8 @@ public class BatteryStatsImpl extends BatteryStats { private static final boolean DEBUG_BINDER_STATS = false; private static final boolean DEBUG_MEMORY = false; + private static final String HISTORY_DIR = "battery-history"; + // TODO: remove "tcp" from network methods, since we measure total stats. // Current on-disk Parcel version. Must be updated when the format of the parcelable changes @@ -1143,6 +1145,8 @@ public class BatteryStatsImpl extends BatteryStats { private int mBatteryTemperature; private int mBatteryVoltageMv; + @Nullable + private final BatteryHistoryDirectory mBatteryHistoryDirectory; @NonNull private final BatteryStatsHistory mHistory; @@ -11476,7 +11480,10 @@ public class BatteryStatsImpl extends BatteryStats { @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies, @NonNull PowerStatsUidResolver powerStatsUidResolver) { - this(config, clock, monotonicClock, systemDir, handler, platformIdleStateCallback, + this(config, clock, monotonicClock, systemDir, + systemDir != null ? new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR), + config.getMaxHistorySizeBytes()) : null, + handler, platformIdleStateCallback, energyStatsRetriever, userInfoProvider, powerProfile, cpuScalingPolicies, powerStatsUidResolver, new FrameworkStatsLogger(), new BatteryStatsHistory.TraceDelegate(), new BatteryStatsHistory.EventLogger()); @@ -11484,6 +11491,7 @@ public class BatteryStatsImpl extends BatteryStats { public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock, @NonNull MonotonicClock monotonicClock, @Nullable File systemDir, + @Nullable BatteryHistoryDirectory batteryHistoryDirectory, @NonNull Handler handler, @Nullable PlatformIdleStateCallback platformIdleStateCallback, @Nullable EnergyStatsRetriever energyStatsRetriever, @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @@ -11517,9 +11525,10 @@ public class BatteryStatsImpl extends BatteryStats { mDailyFile = null; } - mHistory = new BatteryStatsHistory(null /* historyBuffer */, systemDir, - mConstants.MAX_HISTORY_SIZE, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, - mClock, mMonotonicClock, traceDelegate, eventLogger); + mBatteryHistoryDirectory = batteryHistoryDirectory; + mHistory = new BatteryStatsHistory(null /* historyBuffer */, mConstants.MAX_HISTORY_BUFFER, + mBatteryHistoryDirectory, mStepDetailsCalculator, mClock, mMonotonicClock, + traceDelegate, eventLogger); mCpuPowerStatsCollector = new CpuPowerStatsCollector(mPowerStatsCollectorInjector); mCpuPowerStatsCollector.addConsumer(this::recordPowerStats); @@ -12060,7 +12069,7 @@ public class BatteryStatsImpl extends BatteryStats { } public int getHistoryTotalSize() { - return mHistory.getMaxHistorySize(); + return mBatteryHistoryDirectory.getMaxHistorySize(); } public int getHistoryUsedSize() { @@ -12160,6 +12169,13 @@ public class BatteryStatsImpl extends BatteryStats { mResetBatteryHistoryOnNewSession = enabled; } + /** + * Enables or disables battery history file compression. + */ + public void setBatteryHistoryCompressionEnabled(boolean enabled) { + mBatteryHistoryDirectory.setFileCompressionEnabled(enabled); + } + @GuardedBy("this") public void resetAllStatsAndHistoryLocked(int reason) { final long mSecUptime = mClock.uptimeMillis(); @@ -16354,7 +16370,9 @@ public class BatteryStatsImpl extends BatteryStats { */ @VisibleForTesting public void onChange() { - mHistory.setMaxHistorySize(MAX_HISTORY_SIZE); + if (mBatteryHistoryDirectory != null) { + mBatteryHistoryDirectory.setMaxHistorySize(MAX_HISTORY_SIZE); + } mHistory.setMaxHistoryBufferSize(MAX_HISTORY_BUFFER); } diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index c8dbbd29823c..521ee58decea 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -97,3 +97,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "extended_battery_history_compression_enabled" + namespace: "backstage_power" + description: "Compress each battery history chunk on disk" + bug: "381937912" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/tests/powerstatstests/res/raw/history_01 b/services/tests/powerstatstests/res/raw/history_01 Binary files differnew file mode 100644 index 000000000000..f69eb275f2c6 --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_01 diff --git a/services/tests/powerstatstests/res/raw/history_02 b/services/tests/powerstatstests/res/raw/history_02 Binary files differnew file mode 100644 index 000000000000..1a536ab920db --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_02 diff --git a/services/tests/powerstatstests/res/raw/history_03 b/services/tests/powerstatstests/res/raw/history_03 Binary files differnew file mode 100644 index 000000000000..76a3c7b69f01 --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_03 diff --git a/services/tests/powerstatstests/res/raw/history_04 b/services/tests/powerstatstests/res/raw/history_04 Binary files differnew file mode 100644 index 000000000000..7e43ac6281cc --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_04 diff --git a/services/tests/powerstatstests/res/raw/history_05 b/services/tests/powerstatstests/res/raw/history_05 Binary files differnew file mode 100644 index 000000000000..b587723b7d1b --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_05 diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java new file mode 100644 index 000000000000..48e0daa9dba0 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.platform.test.annotations.LargeTest; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import libcore.io.Streams; + +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipParameters; +import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream; +import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream; +import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream; +import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorOutputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.zip.Deflater; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test") +@Ignore("Performance experiment. Comment out @Ignore to run") +public class BatteryStatsHistoryCompressionPerfTest { + + @Rule + public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Rule + public final TestName mTestName = new TestName(); + + private final List<byte[]> mHistorySamples = new ArrayList<>(); + + @Before + public void loadHistorySamples() throws IOException { + Context context = InstrumentationRegistry.getContext(); + Resources resources = context.getResources(); + + for (String sampleResource + : List.of("history_01", "history_02", "history_03", "history_04", "history_05")) { + int resId = resources.getIdentifier(sampleResource, "raw", context.getPackageName()); + try (InputStream stream = resources.openRawResource(resId)) { + byte[] data = Streams.readFully(stream); + mHistorySamples.add(data); + } + } + } + + private interface StreamWrapper<T> { + T wrap(T stream) throws IOException; + } + + private static class CompressorTester implements BatteryHistoryDirectory.Compressor { + private final StreamWrapper<OutputStream> mCompressorSupplier; + private final StreamWrapper<InputStream> mUncompressorSupplier; + private final ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(200000); + private final Random mRandom = new Random(); + + private static class Sample { + public byte[] uncompressed; + public byte[] compressed; + } + + private final List<Sample> mSamples; + + CompressorTester(StreamWrapper<OutputStream> compressorSupplier, + StreamWrapper<InputStream> uncompressorSupplier, + List<byte[]> uncompressedSamples) throws IOException { + mCompressorSupplier = compressorSupplier; + mUncompressorSupplier = uncompressorSupplier; + mSamples = new ArrayList<>(); + for (byte[] uncompressed : uncompressedSamples) { + Sample s = new Sample(); + s.uncompressed = Arrays.copyOf(uncompressed, uncompressed.length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + compress(baos, s.uncompressed); + s.compressed = baos.toByteArray(); + mSamples.add(s); + } + } + + float getCompressionRatio() { + long totalUncompressed = 0; + long totalCompressed = 0; + for (Sample sample : mSamples) { + totalUncompressed += sample.uncompressed.length; + totalCompressed += sample.compressed.length; + } + return (float) totalUncompressed / totalCompressed; + } + + void compressSample() throws IOException { + Sample sample = mSamples.get(mRandom.nextInt(mSamples.size())); + mOutputStream.reset(); + compress(mOutputStream, sample.uncompressed); + // Absence of an exception indicates success + } + + void uncompressSample() throws IOException { + Sample sample = mSamples.get(mRandom.nextInt(mSamples.size())); + uncompress(sample.uncompressed, new ByteArrayInputStream(sample.compressed)); + // Absence of an exception indicates success + } + + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + OutputStream cos = mCompressorSupplier.wrap(stream); + cos.write(data); + cos.close(); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + InputStream cos = mUncompressorSupplier.wrap(stream); + readFully(data, cos); + } + } + + private void benchmarkCompress(StreamWrapper<OutputStream> compressorSupplier) + throws IOException { + CompressorTester tester = new CompressorTester(compressorSupplier, null, mHistorySamples); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + tester.compressSample(); + } + Bundle status = new Bundle(); + status.putFloat(mTestName.getMethodName() + "_compressionRatio", + tester.getCompressionRatio()); + InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } + + private void benchmarkUncompress(StreamWrapper<OutputStream> compressorSupplier, + StreamWrapper<InputStream> uncompressorSupplier) throws IOException { + CompressorTester tester = new CompressorTester(compressorSupplier, uncompressorSupplier, + mHistorySamples); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + tester.uncompressSample(); + } + } + + @Test + public void block_lz4_compress() throws IOException { + benchmarkCompress(BlockLZ4CompressorOutputStream::new); + } + + @Test + public void block_lz4_uncompress() throws IOException { + benchmarkUncompress(BlockLZ4CompressorOutputStream::new, + BlockLZ4CompressorInputStream::new); + } + + @Test + public void framed_lz4_compress() throws IOException { + benchmarkCompress(FramedLZ4CompressorOutputStream::new); + } + + @Test + public void framed_lz4_uncompress() throws IOException { + benchmarkUncompress(FramedLZ4CompressorOutputStream::new, + FramedLZ4CompressorInputStream::new); + } + + @Test + public void gzip_compress() throws IOException { + benchmarkCompress(GzipCompressorOutputStream::new); + } + + @Test + public void gzip_uncompress() throws IOException { + benchmarkUncompress(GzipCompressorOutputStream::new, + GzipCompressorInputStream::new); + } + + @Test + public void best_speed_gzip_compress() throws IOException { + benchmarkCompress(stream -> { + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + return new GzipCompressorOutputStream(stream, parameters); + }); + } + + @Test + public void best_speed_gzip_uncompress() throws IOException { + benchmarkUncompress(stream -> { + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + return new GzipCompressorOutputStream(stream, parameters); + }, GzipCompressorInputStream::new); + } + + @Test + public void java_util_gzip_compress() throws IOException { + benchmarkCompress(GZIPOutputStream::new); + } + + @Test + public void java_util_gzip_uncompress() throws IOException { + benchmarkUncompress(GZIPOutputStream::new, + GZIPInputStream::new); + } + + @Test + public void bzip2_compress() throws IOException { + benchmarkCompress(BZip2CompressorOutputStream::new); + } + + @Test + public void bzip2_uncompress() throws IOException { + benchmarkUncompress(BZip2CompressorOutputStream::new, + BZip2CompressorInputStream::new); + } + + @Test + public void xz_compress() throws IOException { + benchmarkCompress(XZCompressorOutputStream::new); + } + + @Test + public void xz_uncompress() throws IOException { + benchmarkUncompress(XZCompressorOutputStream::new, + XZCompressorInputStream::new); + } + + @Test + public void deflate_compress() throws IOException { + benchmarkCompress(DeflateCompressorOutputStream::new); + } + + @Test + public void deflate_uncompress() throws IOException { + benchmarkUncompress(DeflateCompressorOutputStream::new, + DeflateCompressorInputStream::new); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 164eec6fbc49..8fad93184732 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -17,6 +17,7 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -30,18 +31,20 @@ import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.BatteryStats.HistoryItem; +import android.os.ConditionVariable; import android.os.Parcel; import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.ravenwood.RavenwoodRule; import android.telephony.NetworkRegistrationInfo; -import android.util.AtomicFile; import android.util.Log; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.os.BackgroundThread; import com.android.internal.os.BatteryStatsHistory; import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.MonotonicClock; @@ -58,6 +61,8 @@ import org.mockito.MockitoAnnotations; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.Files; @@ -85,6 +90,7 @@ public class BatteryStatsHistoryTest { private File mHistoryDir; private final MockClock mClock = new MockClock(); private final MonotonicClock mMonotonicClock = new MonotonicClock(0, mClock); + private BatteryHistoryDirectory mDirectory; private BatteryStatsHistory mHistory; private BatteryStats.HistoryPrinter mHistoryPrinter; @Mock @@ -108,11 +114,30 @@ public class BatteryStatsHistoryTest { } mHistoryDir.delete(); + + BatteryHistoryDirectory.Compressor compressor; + if (RavenwoodRule.isOnRavenwood()) { + compressor = new BatteryHistoryDirectory.Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + stream.write(data); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, stream); + } + }; + } else { + compressor = BatteryHistoryDirectory.DEFAULT_COMPRESSOR; + } + mDirectory = new BatteryHistoryDirectory(mHistoryDir, 32768, compressor); + mClock.realtime = 123; mClock.currentTime = 1743645660000L; // 2025-04-03, 2:01:00 AM - mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32768, - MAX_HISTORY_BUFFER_SIZE, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, + mHistory = new BatteryStatsHistory(mHistoryBuffer, MAX_HISTORY_BUFFER_SIZE, mDirectory, + mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); mHistory.startRecordingHistory(mClock.realtime, mClock.uptime, false); @@ -210,8 +235,9 @@ public class BatteryStatsHistoryTest { } @Test - public void testStartNextFile() throws Exception { + public void testStartNextFile() { mHistory.forceRecordAllHistory(); + mDirectory.setFileCompressionEnabled(false); mClock.realtime = 123; @@ -225,7 +251,7 @@ public class BatteryStatsHistoryTest { mClock.realtime = 1000 * i; fileList.add(mClock.realtime + ".bh"); - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); fillActiveFile(mHistory); @@ -235,8 +261,9 @@ public class BatteryStatsHistoryTest { // create file 32 mClock.realtime = 1000 * 32; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); + fillActiveFile(mHistory); fileList.add("32000.bh"); fileList.remove(0); // verify file 0 is deleted. @@ -244,21 +271,22 @@ public class BatteryStatsHistoryTest { verifyFileNames(mHistory, fileList); verifyActiveFile(mHistory, "32000.bh"); - fillActiveFile(mHistory); - // create file 33 mClock.realtime = 1000 * 33; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); - // verify file 1 is deleted + fillActiveFile(mHistory); fileList.add("33000.bh"); fileList.remove(0); + mHistory.writeHistory(); + + // verify file 1 is deleted verifyFileDeleted("1000.bh"); verifyFileNames(mHistory, fileList); verifyActiveFile(mHistory, "33000.bh"); // create a new BatteryStatsHistory object, it will pick up existing history files. - BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024, + BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, 1024, mDirectory, null, mClock, mMonotonicClock, mTracer, mEventLogger); // verify constructor can pick up all files from file system. verifyFileNames(history2, fileList); @@ -281,7 +309,7 @@ public class BatteryStatsHistoryTest { // create file 1. mClock.realtime = 2345678; - history2.startNextFile(mClock.realtime); + history2.startNextFragment(mClock.realtime); createActiveFile(history2); verifyFileNames(history2, Arrays.asList("1234567.bh", "2345678.bh")); verifyActiveFile(history2, "2345678.bh"); @@ -297,10 +325,10 @@ public class BatteryStatsHistoryTest { mHistory = spy(mHistory.copy()); doAnswer(invocation -> { - AtomicFile file = invocation.getArgument(1); - mReadFiles.add(file.getBaseFile().getName()); + BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1); + mReadFiles.add(file.atomicFile.getBaseFile().getName()); return invocation.callRealMethod(); - }).when(mHistory).readFileToParcel(any(), any()); + }).when(mHistory).readFragmentToParcel(any(), any()); // Prepare history for iteration mHistory.iterate(0, MonotonicClock.UNDEFINED); @@ -339,10 +367,10 @@ public class BatteryStatsHistoryTest { mHistory = spy(mHistory.copy()); doAnswer(invocation -> { - AtomicFile file = invocation.getArgument(1); - mReadFiles.add(file.getBaseFile().getName()); + BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1); + mReadFiles.add(file.atomicFile.getBaseFile().getName()); return invocation.callRealMethod(); - }).when(mHistory).readFileToParcel(any(), any()); + }).when(mHistory).readFragmentToParcel(any(), any()); // Prepare history for iteration mHistory.iterate(1000, 3000); @@ -371,14 +399,14 @@ public class BatteryStatsHistoryTest { mHistory.recordEvent(mClock.realtime, mClock.uptime, BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42); - mHistory.startNextFile(mClock.realtime); // 1000.bh + mHistory.startNextFragment(mClock.realtime); // 1000.bh mClock.realtime = 2000; mClock.uptime = 2000; mHistory.recordEvent(mClock.realtime, mClock.uptime, BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42); - mHistory.startNextFile(mClock.realtime); // 2000.bh + mHistory.startNextFragment(mClock.realtime); // 2000.bh mClock.realtime = 3000; mClock.uptime = 3000; @@ -386,30 +414,37 @@ public class BatteryStatsHistoryTest { HistoryItem.EVENT_ALARM, "alarm", 42); // Flush accumulated history to disk - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); } private void verifyActiveFile(BatteryStatsHistory history, String file) { final File expectedFile = new File(mHistoryDir, file); - assertEquals(expectedFile.getPath(), history.getActiveFile().getBaseFile().getPath()); + assertEquals(expectedFile.getPath(), + ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment()) + .atomicFile.getBaseFile().getPath()); assertTrue(expectedFile.exists()); } private void verifyFileNames(BatteryStatsHistory history, List<String> fileList) { - assertEquals(fileList.size(), history.getFilesNames().size()); + awaitCompletion(); + List<String> fileNames = + ((BatteryHistoryDirectory) history.getBatteryHistoryStore()).getFileNames(); + assertThat(fileNames).isEqualTo(fileList); for (int i = 0; i < fileList.size(); i++) { - assertEquals(fileList.get(i), history.getFilesNames().get(i)); final File expectedFile = new File(mHistoryDir, fileList.get(i)); - assertTrue(expectedFile.exists()); + assertWithMessage("File does not exist " + expectedFile) + .that(expectedFile.exists()).isTrue(); } } private void verifyFileDeleted(String file) { + awaitCompletion(); assertFalse(new File(mHistoryDir, file).exists()); } private void createActiveFile(BatteryStatsHistory history) { - final File file = history.getActiveFile().getBaseFile(); + File file = ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment()) + .atomicFile.getBaseFile(); if (file.exists()) { return; } @@ -561,7 +596,7 @@ public class BatteryStatsHistoryTest { public void largeTagPool() { // Keep the preserved part of history short - we only need to capture the very tail of // history. - mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000, + mHistory = new BatteryStatsHistory(mHistoryBuffer, 6000, mDirectory, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); @@ -699,7 +734,7 @@ public class BatteryStatsHistoryTest { assertThat(size).isGreaterThan(lastHistorySize); lastHistorySize = size; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); size = mHistory.getMonotonicHistorySize(); assertThat(size).isEqualTo(lastHistorySize); @@ -713,7 +748,7 @@ public class BatteryStatsHistoryTest { assertThat(size).isGreaterThan(lastHistorySize); lastHistorySize = size; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); mClock.realtime = 3000; mClock.uptime = 3000; @@ -788,4 +823,58 @@ public class BatteryStatsHistoryTest { parcel.recycle(); } + + @Test + public void compressHistoryFiles() { + // The first history file will be uncompressed + mDirectory.setFileCompressionEnabled(false); + + mClock.realtime = 1000; + mClock.uptime = 1000; + mHistory.recordEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42); + + mHistory.startNextFragment(mClock.realtime); + + // The second file will be compressed + mDirectory.setFileCompressionEnabled(true); + + mClock.realtime = 2000; + mClock.uptime = 2000; + mHistory.recordEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42); + + mHistory.startNextFragment(mClock.realtime); + + awaitCompletion(); + + assertThat(historySummary(mHistory)).isEqualTo(List.of("+42:job", "-42:job")); + + Parcel parcel = Parcel.obtain(); + mHistory.writeToBatteryUsageStatsParcel(parcel, Long.MAX_VALUE); + parcel.setDataPosition(0); + + BatteryStatsHistory actual = BatteryStatsHistory.createFromBatteryUsageStatsParcel(parcel); + assertThat(historySummary(actual)).isEqualTo(List.of("+42:job", "-42:job")); + } + + private List<String> historySummary(BatteryStatsHistory history) { + List<String> events = new ArrayList<>(); + try (BatteryStatsHistoryIterator it = history.iterate(0, Long.MAX_VALUE)) { + HistoryItem item; + while ((item = it.next()) != null) { + if ((item.eventCode & HistoryItem.EVENT_TYPE_MASK) == HistoryItem.EVENT_JOB) { + events.add(((item.eventCode & HistoryItem.EVENT_FLAG_START) != 0 ? "+" : "-") + + item.eventTag.uid + ":" + item.eventTag.string); + } + } + } + return events; + } + + private static void awaitCompletion() { + ConditionVariable done = new ConditionVariable(); + BackgroundThread.getHandler().post(done::open); + done.block(); + } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index e94ef5bb4871..31ff50f8ca58 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -877,9 +877,19 @@ public class BatteryUsageStatsProviderTest { } @Test - @EnableFlags(Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED) + @EnableFlags({ + Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED, + Flags.FLAG_EXTENDED_BATTERY_HISTORY_COMPRESSION_ENABLED + }) public void testIncludeSubsetOfHistory() throws IOException { MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); + BatteryHistoryDirectory store = + (BatteryHistoryDirectory) batteryStats.getHistory().getBatteryHistoryStore(); + store.setFileCompressionEnabled(true); + // Make history fragment size predictable. Without this protection, holding the history + // directory lock in the background would prevent new fragments from being created. + store.makeDirectoryLockUnconditional(); + batteryStats.getHistory().setMaxHistoryBufferSize(100); synchronized (batteryStats) { batteryStats.setRecordAllHistoryLocked(true); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index a69e2fdb0b03..c7a19ce7b233 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -41,6 +41,9 @@ import com.android.internal.os.PowerProfile; import com.android.internal.power.EnergyConsumerStats; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Queue; @@ -49,6 +52,18 @@ import java.util.Queue; * Mocks a BatteryStatsImpl object. */ public class MockBatteryStatsImpl extends BatteryStatsImpl { + public static final BatteryHistoryDirectory.Compressor PASS_THROUGH_COMPRESSOR = + new BatteryHistoryDirectory.Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + stream.write(data); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, stream); + } + }; public boolean mForceOnBattery; // The mNetworkStats will be used for both wifi and mobile categories private NetworkStats mNetworkStats; @@ -83,7 +98,11 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { MockBatteryStatsImpl(BatteryStatsConfig config, Clock clock, MonotonicClock monotonicClock, File historyDirectory, Handler handler, PowerProfile powerProfile, PowerStatsUidResolver powerStatsUidResolver) { - super(config, clock, monotonicClock, historyDirectory, handler, + super(config, clock, monotonicClock, historyDirectory, + historyDirectory != null ? new BatteryHistoryDirectory( + new File(historyDirectory, "battery-history"), + config.getMaxHistorySizeBytes(), PASS_THROUGH_COMPRESSOR) : null, + handler, mock(PlatformIdleStateCallback.class), mock(EnergyStatsRetriever.class), mock(UserInfoProvider.class), powerProfile, new CpuScalingPolicies(new SparseArray<>(), new SparseArray<>()), diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java index 3bdbcb50e601..73d491c93bb5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java @@ -59,7 +59,7 @@ public class PowerStatsAggregatorTest { @Before public void setup() throws ParseException { - mHistory = new BatteryStatsHistory(null, null, 0, 1024, + mHistory = new BatteryStatsHistory(null, 1024, null, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java index d243f92a139f..9ef58cc28a69 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java @@ -42,6 +42,7 @@ import com.android.internal.os.CpuScalingPolicies; import com.android.internal.os.MonotonicClock; import com.android.internal.os.PowerProfile; import com.android.internal.os.PowerStats; +import com.android.server.power.stats.BatteryHistoryDirectory; import com.android.server.power.stats.BatteryUsageStatsRule; import com.android.server.power.stats.MockClock; import com.android.server.power.stats.PowerStatsStore; @@ -84,6 +85,7 @@ public class PowerStatsExporterTest { private PowerStatsStore mPowerStatsStore; private PowerStatsAggregator mPowerStatsAggregator; private MultiStatePowerAttributor mPowerAttributor; + private BatteryHistoryDirectory mDirectory; private BatteryStatsHistory mHistory; private CpuPowerStatsLayout mCpuStatsArrayLayout; private PowerStats.Descriptor mPowerStatsDescriptor; @@ -117,7 +119,8 @@ public class PowerStatsExporterTest { AggregatedPowerStatsConfig.STATE_PROCESS_STATE); mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler()); - mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000, + mDirectory = new BatteryHistoryDirectory(storeDirectory, 0); + mHistory = new BatteryStatsHistory(Parcel.obtain(), 10000, mDirectory, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, mMonotonicClock, null, null); mPowerStatsAggregator = new PowerStatsAggregator(config); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java index ed3cda0f76ef..8257d714a5d5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java @@ -87,7 +87,7 @@ public class WakelockPowerStatsProcessorTest { PowerStats ps = new PowerStats(descriptor); long[] uidStats = new long[descriptor.uidStatsArrayLength]; - BatteryStatsHistory history = new BatteryStatsHistory(null, null, 0, 10000, + BatteryStatsHistory history = new BatteryStatsHistory(null, 10000, null, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mStatsRule.getMockClock(), new MonotonicClock(START_TIME, mStatsRule.getMockClock()), null, null); |