summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/os/BatteryStatsHistory.java779
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java2
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java573
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java30
-rw-r--r--services/core/java/com/android/server/power/stats/flags.aconfig10
-rw-r--r--services/tests/powerstatstests/res/raw/history_01bin0 -> 131116 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_02bin0 -> 131432 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_03bin0 -> 132864 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_04bin0 -> 131204 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_05bin0 -> 131112 bytes
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java280
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java147
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java12
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java21
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java2
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java5
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java2
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 &lt;num&gt;.bf. The file number &lt;num&gt; 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
new file mode 100644
index 000000000000..f69eb275f2c6
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_01
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_02 b/services/tests/powerstatstests/res/raw/history_02
new file mode 100644
index 000000000000..1a536ab920db
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_02
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_03 b/services/tests/powerstatstests/res/raw/history_03
new file mode 100644
index 000000000000..76a3c7b69f01
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_03
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_04 b/services/tests/powerstatstests/res/raw/history_04
new file mode 100644
index 000000000000..7e43ac6281cc
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_04
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_05 b/services/tests/powerstatstests/res/raw/history_05
new file mode 100644
index 000000000000..b587723b7d1b
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_05
Binary files differ
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);