summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/os/BatteryUsageStatsQuery.java1
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java103
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java58
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryUsageStatsSection.java49
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java338
-rw-r--r--services/core/java/com/android/server/power/stats/PowerStatsScheduler.java73
-rw-r--r--services/core/java/com/android/server/power/stats/PowerStatsSpan.java433
-rw-r--r--services/core/java/com/android/server/power/stats/PowerStatsStore.java328
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java63
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java4
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java227
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsSchedulerTest.java139
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsStoreTest.java179
-rw-r--r--services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java18
14 files changed, 1389 insertions, 624 deletions
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index 49d7e8bc5632..32840d4b5837 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -300,6 +300,7 @@ public final class BatteryUsageStatsQuery implements Parcelable {
* @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
* @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
*/
+ // TODO(b/298459065): switch to monotonic clock
public Builder aggregateSnapshots(long fromTimestamp, long toTimestamp) {
mFromTimestamp = fromTimestamp;
mToTimestamp = toTimestamp;
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 1eb66737d751..56061ffb6ebf 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -91,6 +91,7 @@ import android.telephony.ModemActivityInfo;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
+import android.util.AtomicFile;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.StatsEvent;
@@ -119,13 +120,16 @@ import com.android.server.power.optimization.Flags;
import com.android.server.power.stats.BatteryExternalStatsWorker;
import com.android.server.power.stats.BatteryStatsImpl;
import com.android.server.power.stats.BatteryUsageStatsProvider;
-import com.android.server.power.stats.BatteryUsageStatsStore;
+import com.android.server.power.stats.PowerStatsScheduler;
+import com.android.server.power.stats.PowerStatsStore;
import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
import com.android.server.power.stats.wakeups.CpuWakeupStats;
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
@@ -138,6 +142,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -155,7 +160,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub
static final String TAG = "BatteryStatsService";
static final String TRACE_TRACK_WAKEUP_REASON = "wakeup_reason";
static final boolean DBG = false;
- private static final boolean BATTERY_USAGE_STORE_ENABLED = true;
private static IBatteryStats sService;
@@ -165,11 +169,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub
private final BatteryStatsImpl.BatteryStatsConfig mBatteryStatsConfig;
final BatteryStatsImpl mStats;
final CpuWakeupStats mCpuWakeupStats;
- private final BatteryUsageStatsStore mBatteryUsageStatsStore;
+ private final PowerStatsStore mPowerStatsStore;
+ private final PowerStatsScheduler mPowerStatsScheduler;
private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider;
private final Context mContext;
private final BatteryExternalStatsWorker mWorker;
private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
+ private final AtomicFile mConfigFile;
+
private volatile boolean mMonitorEnabled = true;
private native void getRailEnergyPowerStats(RailStats railStats);
@@ -403,16 +410,13 @@ public final class BatteryStatsService extends IBatteryStats.Stub
mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
mStats.startTrackingSystemServerCpuTime();
-
- if (BATTERY_USAGE_STORE_ENABLED) {
- mBatteryUsageStatsStore =
- new BatteryUsageStatsStore(context, mStats, systemDir, mHandler);
- } else {
- mBatteryUsageStatsStore = null;
- }
+ mPowerStatsStore = new PowerStatsStore(systemDir, mHandler);
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mStats,
- mBatteryUsageStatsStore);
+ mPowerStatsStore);
+ mPowerStatsScheduler = new PowerStatsScheduler(mPowerStatsStore, mMonotonicClock, mHandler,
+ mStats, mBatteryUsageStatsProvider);
mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
+ mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
}
/**
@@ -471,9 +475,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
*/
public void onSystemReady() {
mStats.onSystemReady();
- if (BATTERY_USAGE_STORE_ENABLED) {
- mBatteryUsageStatsStore.onSystemReady();
- }
+ mPowerStatsScheduler.start();
}
private final class LocalService extends BatteryStatsInternal {
@@ -900,12 +902,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub
bus = getBatteryUsageStats(List.of(queryPowerProfile)).get(0);
break;
case FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET:
- if (!BATTERY_USAGE_STORE_ENABLED) {
- return StatsManager.PULL_SKIP;
- }
-
- final long sessionStart = mBatteryUsageStatsStore
- .getLastBatteryUsageStatsBeforeResetAtomPullTimestamp();
+ final long sessionStart =
+ getLastBatteryUsageStatsBeforeResetAtomPullTimestamp();
final long sessionEnd;
synchronized (mStats) {
sessionEnd = mStats.getStartClockTime();
@@ -918,8 +916,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
.aggregateSnapshots(sessionStart, sessionEnd)
.build();
bus = getBatteryUsageStats(List.of(queryBeforeReset)).get(0);
- mBatteryUsageStatsStore
- .setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(sessionEnd);
+ setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(sessionEnd);
break;
default:
throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
@@ -2652,6 +2649,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub
mStats.dumpAggregatedStats(pw, /* startTime */ 0, /* endTime */0);
}
+ private void dumpPowerStatsStore(PrintWriter pw) {
+ mPowerStatsStore.dump(new IndentingPrintWriter(pw, " "));
+ }
+
+ private void dumpPowerStatsStoreTableOfContents(PrintWriter pw) {
+ mPowerStatsStore.dumpTableOfContents(new IndentingPrintWriter(pw, " "));
+ }
+
private void dumpMeasuredEnergyStats(PrintWriter pw) {
// Wait for the completion of pending works if there is any
awaitCompletion();
@@ -2797,7 +2802,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
synchronized (mStats) {
mStats.resetAllStatsAndHistoryLocked(
BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
- mBatteryUsageStatsStore.removeAllSnapshots();
+ mPowerStatsStore.reset();
pw.println("Battery stats and history reset.");
noOutput = true;
}
@@ -2899,6 +2904,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
} else if ("--aggregated".equals(arg)) {
dumpAggregatedStats(pw);
return;
+ } else if ("--store".equals(arg)) {
+ dumpPowerStatsStore(pw);
+ return;
+ } else if ("--store-toc".equals(arg)) {
+ dumpPowerStatsStoreTableOfContents(pw);
+ return;
} else if ("-a".equals(arg)) {
flags |= BatteryStats.DUMP_VERBOSE;
} else if (arg.length() > 0 && arg.charAt(0) == '-'){
@@ -3368,6 +3379,52 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ private static final String BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY =
+ "BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP";
+
+ /**
+ * Saves the supplied timestamp of the BATTERY_USAGE_STATS_BEFORE_RESET statsd atom pull
+ * in persistent file.
+ */
+ public void setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp) {
+ synchronized (mConfigFile) {
+ Properties props = new Properties();
+ try (InputStream in = mConfigFile.openRead()) {
+ props.load(in);
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
+ }
+ props.put(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY,
+ String.valueOf(timestamp));
+ FileOutputStream out = null;
+ try {
+ out = mConfigFile.startWrite();
+ props.store(out, "Statsd atom pull timestamps");
+ mConfigFile.finishWrite(out);
+ } catch (IOException e) {
+ mConfigFile.failWrite(out);
+ Slog.e(TAG, "Cannot save config file " + mConfigFile, e);
+ }
+ }
+ }
+
+ /**
+ * Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET
+ * statsd atom pull.
+ */
+ public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() {
+ synchronized (mConfigFile) {
+ Properties props = new Properties();
+ try (InputStream in = mConfigFile.openRead()) {
+ props.load(in);
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
+ }
+ return Long.parseLong(
+ props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0"));
+ }
+ }
+
/**
* Sets battery AC charger to enabled/disabled, and freezes the battery state.
*/
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index f6fa9f244252..851a3f7bdaba 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -46,7 +46,7 @@ public class BatteryUsageStatsProvider {
private static final String TAG = "BatteryUsageStatsProv";
private final Context mContext;
private final BatteryStats mStats;
- private final BatteryUsageStatsStore mBatteryUsageStatsStore;
+ private final PowerStatsStore mPowerStatsStore;
private final PowerProfile mPowerProfile;
private final CpuScalingPolicies mCpuScalingPolicies;
private final Object mLock = new Object();
@@ -58,10 +58,10 @@ public class BatteryUsageStatsProvider {
@VisibleForTesting
public BatteryUsageStatsProvider(Context context, BatteryStats stats,
- BatteryUsageStatsStore batteryUsageStatsStore) {
+ PowerStatsStore powerStatsStore) {
mContext = context;
mStats = stats;
- mBatteryUsageStatsStore = batteryUsageStatsStore;
+ mPowerStatsStore = powerStatsStore;
mPowerProfile = stats instanceof BatteryStatsImpl
? ((BatteryStatsImpl) stats).getPowerProfile()
: new PowerProfile(context);
@@ -314,20 +314,52 @@ public class BatteryUsageStatsProvider {
final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
customEnergyConsumerNames, includePowerModels, includeProcessStateData,
minConsumedPowerThreshold);
- if (mBatteryUsageStatsStore == null) {
- Log.e(TAG, "BatteryUsageStatsStore is unavailable");
+ if (mPowerStatsStore == null) {
+ Log.e(TAG, "PowerStatsStore is unavailable");
return builder.build();
}
- final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
- for (long timestamp : timestamps) {
- if (timestamp > query.getFromTimestamp() && timestamp <= query.getToTimestamp()) {
- final BatteryUsageStats snapshot =
- mBatteryUsageStatsStore.loadBatteryUsageStats(timestamp);
- if (snapshot == null) {
- continue;
- }
+ List<PowerStatsSpan.Metadata> toc = mPowerStatsStore.getTableOfContents();
+ for (PowerStatsSpan.Metadata spanMetadata : toc) {
+ if (!spanMetadata.getSections().contains(BatteryUsageStatsSection.TYPE)) {
+ continue;
+ }
+
+ // BatteryUsageStatsQuery is expressed in terms of wall-clock time range for the
+ // session end time.
+ //
+ // The following algorithm is correct when there is only one time frame in the span.
+ // When the wall-clock time is adjusted in the middle of an stats span,
+ // constraining it by wall-clock time becomes ambiguous. In this case, the algorithm
+ // only covers some situations, but not others. When using the resulting data for
+ // analysis, we should always pay attention to the full set of included timeframes.
+ // TODO(b/298459065): switch to monotonic clock
+ long minTime = Long.MAX_VALUE;
+ long maxTime = 0;
+ for (PowerStatsSpan.TimeFrame timeFrame : spanMetadata.getTimeFrames()) {
+ long spanEndTime = timeFrame.startTime + timeFrame.duration;
+ minTime = Math.min(minTime, spanEndTime);
+ maxTime = Math.max(maxTime, spanEndTime);
+ }
+
+ // Per BatteryUsageStatsQuery API, the "from" timestamp is *exclusive*,
+ // while the "to" timestamp is *inclusive*.
+ boolean isInRange =
+ (query.getFromTimestamp() == 0 || minTime > query.getFromTimestamp())
+ && (query.getToTimestamp() == 0 || maxTime <= query.getToTimestamp());
+ if (!isInRange) {
+ continue;
+ }
+
+ PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+ spanMetadata.getId(), BatteryUsageStatsSection.TYPE);
+ if (powerStatsSpan == null) {
+ continue;
+ }
+ for (PowerStatsSpan.Section section : powerStatsSpan.getSections()) {
+ BatteryUsageStats snapshot =
+ ((BatteryUsageStatsSection) section).getBatteryUsageStats();
if (!Arrays.equals(snapshot.getCustomPowerComponentNames(),
customEnergyConsumerNames)) {
Log.w(TAG, "Ignoring older BatteryUsageStats snapshot, which has different "
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsSection.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsSection.java
new file mode 100644
index 000000000000..b95faac7c111
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsSection.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.os.BatteryUsageStats;
+import android.util.IndentingPrintWriter;
+
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+
+class BatteryUsageStatsSection extends PowerStatsSpan.Section {
+ public static final String TYPE = "battery-usage-stats";
+
+ private final BatteryUsageStats mBatteryUsageStats;
+
+ BatteryUsageStatsSection(BatteryUsageStats batteryUsageStats) {
+ super(TYPE);
+ mBatteryUsageStats = batteryUsageStats;
+ }
+
+ public BatteryUsageStats getBatteryUsageStats() {
+ return mBatteryUsageStats;
+ }
+
+ @Override
+ void write(TypedXmlSerializer serializer) throws IOException {
+ mBatteryUsageStats.writeXml(serializer);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter ipw) {
+ mBatteryUsageStats.dump(ipw, "");
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
deleted file mode 100644
index 0d7a1406f751..000000000000
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * Copyright (C) 2020 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.annotation.Nullable;
-import android.content.Context;
-import android.os.BatteryUsageStats;
-import android.os.BatteryUsageStatsQuery;
-import android.os.Handler;
-import android.util.AtomicFile;
-import android.util.Log;
-import android.util.LongArray;
-import android.util.Slog;
-import android.util.Xml;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.TypedXmlPullParser;
-import com.android.modules.utils.TypedXmlSerializer;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-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.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.StandardOpenOption;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Properties;
-import java.util.TreeMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * A storage mechanism for BatteryUsageStats snapshots.
- */
-public class BatteryUsageStatsStore {
- private static final String TAG = "BatteryUsageStatsStore";
-
- private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of(
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includePowerModels()
- .includeProcessStateData()
- .build());
- private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats";
- private static final String SNAPSHOT_FILE_EXTENSION = ".bus";
- private static final String DIR_LOCK_FILENAME = ".lock";
- private static final String CONFIG_FILENAME = "config";
- private static final String BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY =
- "BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP";
- private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024;
-
- private final Context mContext;
- private final BatteryStatsImpl mBatteryStats;
- private boolean mSystemReady;
- private final File mStoreDir;
- private final File mLockFile;
- private final ReentrantLock mFileLock = new ReentrantLock();
- private FileLock mJvmLock;
- private final AtomicFile mConfigFile;
- private final long mMaxStorageBytes;
- private final Handler mHandler;
- private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
-
- public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir,
- Handler handler) {
- this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
- }
-
- @VisibleForTesting
- public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir,
- Handler handler, long maxStorageBytes) {
- mContext = context;
- mBatteryStats = batteryStats;
- mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR);
- mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME);
- mConfigFile = new AtomicFile(new File(mStoreDir, CONFIG_FILENAME));
- mHandler = handler;
- mMaxStorageBytes = maxStorageBytes;
- mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset);
- mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats);
- }
-
- /**
- * Notifies BatteryUsageStatsStore that the system server is ready.
- */
- public void onSystemReady() {
- mSystemReady = true;
- }
-
- private void prepareForBatteryStatsReset(int resetReason) {
- if (resetReason == BatteryStatsImpl.RESET_REASON_CORRUPT_FILE || !mSystemReady) {
- return;
- }
-
- final List<BatteryUsageStats> stats =
- mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY);
- if (stats.isEmpty()) {
- Slog.wtf(TAG, "No battery usage stats generated");
- return;
- }
-
- mHandler.post(() -> storeBatteryUsageStats(stats.get(0)));
- }
-
- private void storeBatteryUsageStats(BatteryUsageStats stats) {
- lockSnapshotDirectory();
- try {
- if (!mStoreDir.exists()) {
- if (!mStoreDir.mkdirs()) {
- Slog.e(TAG, "Could not create a directory for battery usage stats snapshots");
- return;
- }
- }
- File file = makeSnapshotFilename(stats.getStatsEndTimestamp());
- try {
- writeXmlFileLocked(stats, file);
- } catch (Exception e) {
- Slog.e(TAG, "Cannot save battery usage stats", e);
- }
-
- removeOldSnapshotsLocked();
- } finally {
- unlockSnapshotDirectory();
- }
- }
-
- /**
- * Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds
- * to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}.
- */
- public long[] listBatteryUsageStatsTimestamps() {
- LongArray timestamps = new LongArray(100);
- lockSnapshotDirectory();
- try {
- for (File file : mStoreDir.listFiles()) {
- String fileName = file.getName();
- if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) {
- try {
- String fileNameWithoutExtension = fileName.substring(0,
- fileName.length() - SNAPSHOT_FILE_EXTENSION.length());
- timestamps.add(Long.parseLong(fileNameWithoutExtension));
- } catch (NumberFormatException e) {
- Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: "
- + fileName);
- }
- }
- }
- } finally {
- unlockSnapshotDirectory();
- }
- return timestamps.toArray();
- }
-
- /**
- * Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot
- * does not exist.
- */
- @Nullable
- public BatteryUsageStats loadBatteryUsageStats(long timestamp) {
- lockSnapshotDirectory();
- try {
- File file = makeSnapshotFilename(timestamp);
- try {
- return readXmlFileLocked(file);
- } catch (Exception e) {
- Slog.e(TAG, "Cannot read battery usage stats", e);
- }
- } finally {
- unlockSnapshotDirectory();
- }
- return null;
- }
-
- /**
- * Saves the supplied timestamp of the BATTERY_USAGE_STATS_BEFORE_RESET statsd atom pull
- * in persistent file.
- */
- public void setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp) {
- Properties props = new Properties();
- lockSnapshotDirectory();
- try {
- try (InputStream in = mConfigFile.openRead()) {
- props.load(in);
- } catch (IOException e) {
- Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
- }
- props.put(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY,
- String.valueOf(timestamp));
- FileOutputStream out = null;
- try {
- out = mConfigFile.startWrite();
- props.store(out, "Statsd atom pull timestamps");
- mConfigFile.finishWrite(out);
- } catch (IOException e) {
- mConfigFile.failWrite(out);
- Slog.e(TAG, "Cannot save config file " + mConfigFile, e);
- }
- } finally {
- unlockSnapshotDirectory();
- }
- }
-
- /**
- * Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET
- * statsd atom pull.
- */
- public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() {
- Properties props = new Properties();
- lockSnapshotDirectory();
- try {
- try (InputStream in = mConfigFile.openRead()) {
- props.load(in);
- } catch (IOException e) {
- Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
- }
- } finally {
- unlockSnapshotDirectory();
- }
- return Long.parseLong(
- props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0"));
- }
-
- private void lockSnapshotDirectory() {
- mFileLock.lock();
-
- // Lock the directory from access by other JVMs
- try {
- mLockFile.getParentFile().mkdirs();
- mLockFile.createNewFile();
- mJvmLock = FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock();
- } catch (IOException e) {
- Log.e(TAG, "Cannot lock snapshot directory", e);
- }
- }
-
- private void unlockSnapshotDirectory() {
- try {
- mJvmLock.close();
- } catch (IOException e) {
- Log.e(TAG, "Cannot unlock snapshot directory", e);
- } finally {
- mFileLock.unlock();
- }
- }
-
- /**
- * Creates a file name by formatting the timestamp as 19-digit zero-padded number.
- * This ensures that sorted directory list follows the chronological order.
- */
- private File makeSnapshotFilename(long statsEndTimestamp) {
- return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp)
- + SNAPSHOT_FILE_EXTENSION);
- }
-
- private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException {
- try (OutputStream out = new FileOutputStream(file)) {
- TypedXmlSerializer serializer = Xml.newBinarySerializer();
- serializer.setOutput(out, StandardCharsets.UTF_8.name());
- serializer.startDocument(null, true);
- stats.writeXml(serializer);
- serializer.endDocument();
- }
- }
-
- private BatteryUsageStats readXmlFileLocked(File file)
- throws IOException, XmlPullParserException {
- try (InputStream in = new FileInputStream(file)) {
- TypedXmlPullParser parser = Xml.newBinaryPullParser();
- parser.setInput(in, StandardCharsets.UTF_8.name());
- return BatteryUsageStats.createFromXml(parser);
- }
- }
-
- private void removeOldSnapshotsLocked() {
- // Read the directory list into a _sorted_ map. The alphanumeric ordering
- // corresponds to the historical order of snapshots because the file names
- // are timestamps zero-padded to the same length.
- long totalSize = 0;
- TreeMap<File, Long> mFileSizes = new TreeMap<>();
- for (File file : mStoreDir.listFiles()) {
- final long fileSize = file.length();
- totalSize += fileSize;
- if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) {
- mFileSizes.put(file, fileSize);
- }
- }
-
- while (totalSize > mMaxStorageBytes) {
- final Map.Entry<File, Long> entry = mFileSizes.firstEntry();
- if (entry == null) {
- break;
- }
-
- File file = entry.getKey();
- if (!file.delete()) {
- Slog.e(TAG, "Cannot delete battery usage stats " + file);
- }
- totalSize -= entry.getValue();
- mFileSizes.remove(file);
- }
- }
-
- public void removeAllSnapshots() {
- lockSnapshotDirectory();
- try {
- for (File file : mStoreDir.listFiles()) {
- if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) {
- if (!file.delete()) {
- Slog.e(TAG, "Cannot delete battery usage stats " + file);
- }
- }
- }
- } finally {
- unlockSnapshotDirectory();
- }
- }
-}
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java b/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java
new file mode 100644
index 000000000000..93b0bac86d34
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.Handler;
+
+import com.android.internal.os.MonotonicClock;
+
+/**
+ * Controls the frequency at which {@link PowerStatsSpan}'s are generated and stored in
+ * {@link PowerStatsStore}.
+ */
+public class PowerStatsScheduler {
+ private final PowerStatsStore mPowerStatsStore;
+ private final MonotonicClock mMonotonicClock;
+ private final Handler mHandler;
+ private final BatteryStatsImpl mBatteryStats;
+ private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
+
+ public PowerStatsScheduler(PowerStatsStore powerStatsStore, MonotonicClock monotonicClock,
+ Handler handler, BatteryStatsImpl batteryStats,
+ BatteryUsageStatsProvider batteryUsageStatsProvider) {
+ mPowerStatsStore = powerStatsStore;
+ mMonotonicClock = monotonicClock;
+ mHandler = handler;
+ mBatteryStats = batteryStats;
+ mBatteryUsageStatsProvider = batteryUsageStatsProvider;
+ }
+
+ /**
+ * Kicks off the scheduling of power stats aggregation spans.
+ */
+ public void start() {
+ mBatteryStats.setBatteryResetListener(this::storeBatteryUsageStatsOnReset);
+ }
+
+ private void storeBatteryUsageStatsOnReset(int resetReason) {
+ if (resetReason == BatteryStatsImpl.RESET_REASON_CORRUPT_FILE) {
+ return;
+ }
+
+ final BatteryUsageStats batteryUsageStats =
+ mBatteryUsageStatsProvider.getBatteryUsageStats(
+ new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includePowerModels()
+ .includeProcessStateData()
+ .build());
+
+ // TODO(b/188068523): BatteryUsageStats should contain monotonic time for start and end
+ // When that is done, we will be able to use the BatteryUsageStats' monotonic start time
+ long monotonicStartTime =
+ mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
+ mHandler.post(() ->
+ mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats));
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsSpan.java b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
new file mode 100644
index 000000000000..3b260ca0c3b9
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2023 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.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import com.google.android.collect.Sets;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Contains power stats of various kinds, aggregated over a time span.
+ */
+public class PowerStatsSpan {
+ private static final String TAG = "PowerStatsStore";
+
+ /**
+ * Increment VERSION when the XML format of the store changes. Also, update
+ * {@link #isCompatibleXmlFormat} to return true for all legacy versions
+ * that are compatible with the new one.
+ */
+ private static final int VERSION = 1;
+
+ private static final String XML_TAG_METADATA = "metadata";
+ private static final String XML_ATTR_ID = "id";
+ private static final String XML_ATTR_VERSION = "version";
+ private static final String XML_TAG_TIMEFRAME = "timeframe";
+ private static final String XML_ATTR_MONOTONIC = "monotonic";
+ private static final String XML_ATTR_START_TIME = "start";
+ private static final String XML_ATTR_DURATION = "duration";
+ private static final String XML_TAG_SECTION = "section";
+ private static final String XML_ATTR_SECTION_TYPE = "type";
+
+ private static final DateTimeFormatter DATE_FORMAT =
+ DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
+
+ static class TimeFrame {
+ public final long startMonotonicTime;
+ @CurrentTimeMillisLong
+ public final long startTime;
+ @DurationMillisLong
+ public final long duration;
+
+ TimeFrame(long startMonotonicTime, @CurrentTimeMillisLong long startTime,
+ @DurationMillisLong long duration) {
+ this.startMonotonicTime = startMonotonicTime;
+ this.startTime = startTime;
+ this.duration = duration;
+ }
+
+ void write(TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(null, XML_TAG_TIMEFRAME);
+ serializer.attributeLong(null, XML_ATTR_START_TIME, startTime);
+ serializer.attributeLong(null, XML_ATTR_MONOTONIC, startMonotonicTime);
+ serializer.attributeLong(null, XML_ATTR_DURATION, duration);
+ serializer.endTag(null, XML_TAG_TIMEFRAME);
+ }
+
+ static TimeFrame read(TypedXmlPullParser parser) throws XmlPullParserException {
+ return new TimeFrame(
+ parser.getAttributeLong(null, XML_ATTR_MONOTONIC),
+ parser.getAttributeLong(null, XML_ATTR_START_TIME),
+ parser.getAttributeLong(null, XML_ATTR_DURATION));
+ }
+
+ /**
+ * Prints the contents of this TimeFrame.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(DATE_FORMAT.format(Instant.ofEpochMilli(startTime)))
+ .append(" (monotonic=").append(startMonotonicTime).append(") ")
+ .append(" duration=");
+ String durationString = TimeUtils.formatDuration(duration);
+ if (durationString.startsWith("+")) {
+ sb.append(durationString.substring(1));
+ } else {
+ sb.append(durationString);
+ }
+ pw.print(sb);
+ }
+ }
+
+ static class Metadata {
+ static final Comparator<Metadata> COMPARATOR = Comparator.comparing(Metadata::getId);
+
+ private final long mId;
+ private final List<TimeFrame> mTimeFrames = new ArrayList<>();
+ private final List<String> mSections = new ArrayList<>();
+
+ Metadata(long id) {
+ mId = id;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public List<TimeFrame> getTimeFrames() {
+ return mTimeFrames;
+ }
+
+ public List<String> getSections() {
+ return mSections;
+ }
+
+ void addTimeFrame(TimeFrame timeFrame) {
+ mTimeFrames.add(timeFrame);
+ }
+
+ void addSection(String sectionType) {
+ // The number of sections per span is small, so there is no need to use a Set
+ if (!mSections.contains(sectionType)) {
+ mSections.add(sectionType);
+ }
+ }
+
+ void write(TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(null, XML_TAG_METADATA);
+ serializer.attributeLong(null, XML_ATTR_ID, mId);
+ serializer.attributeInt(null, XML_ATTR_VERSION, VERSION);
+ for (TimeFrame timeFrame : mTimeFrames) {
+ timeFrame.write(serializer);
+ }
+ for (String section : mSections) {
+ serializer.startTag(null, XML_TAG_SECTION);
+ serializer.attribute(null, XML_ATTR_SECTION_TYPE, section);
+ serializer.endTag(null, XML_TAG_SECTION);
+ }
+ serializer.endTag(null, XML_TAG_METADATA);
+ }
+
+ /**
+ * Reads just the header of the XML file containing metadata.
+ * Returns null if the file does not contain a compatible &lt;metadata&gt; element.
+ */
+ @Nullable
+ public static Metadata read(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ Metadata metadata = null;
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && !(eventType == XmlPullParser.END_TAG
+ && parser.getName().equals(XML_TAG_METADATA))) {
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ if (tagName.equals(XML_TAG_METADATA)) {
+ int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
+ if (!isCompatibleXmlFormat(version)) {
+ Slog.e(TAG,
+ "Incompatible version " + version + "; expected " + VERSION);
+ return null;
+ }
+
+ long id = parser.getAttributeLong(null, XML_ATTR_ID);
+ metadata = new Metadata(id);
+ } else if (metadata != null && tagName.equals(XML_TAG_TIMEFRAME)) {
+ metadata.addTimeFrame(TimeFrame.read(parser));
+ } else if (metadata != null && tagName.equals(XML_TAG_SECTION)) {
+ metadata.addSection(parser.getAttributeValue(null, XML_ATTR_SECTION_TYPE));
+ }
+ }
+ eventType = parser.next();
+ }
+ return metadata;
+ }
+
+ /**
+ * Prints the metadata.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ dump(pw, true);
+ }
+
+ void dump(IndentingPrintWriter pw, boolean includeSections) {
+ pw.print("Span ");
+ if (mTimeFrames.size() > 0) {
+ mTimeFrames.get(0).dump(pw);
+ pw.println();
+ }
+
+ // Sometimes, when the wall clock is adjusted in the middle of a stats session,
+ // we will have more than one time frame.
+ for (int i = 1; i < mTimeFrames.size(); i++) {
+ TimeFrame timeFrame = mTimeFrames.get(i);
+ pw.print(" "); // Aligned below "Span "
+ timeFrame.dump(pw);
+ pw.println();
+ }
+
+ if (includeSections) {
+ pw.increaseIndent();
+ for (String section : mSections) {
+ pw.print("section", section);
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringWriter sw = new StringWriter();
+ IndentingPrintWriter ipw = new IndentingPrintWriter(sw);
+ ipw.print("id", mId);
+ for (int i = 0; i < mTimeFrames.size(); i++) {
+ TimeFrame timeFrame = mTimeFrames.get(i);
+ ipw.print("timeframe=[");
+ timeFrame.dump(ipw);
+ ipw.print("] ");
+ }
+ for (String section : mSections) {
+ ipw.print("section", section);
+ }
+ ipw.flush();
+ return sw.toString().trim();
+ }
+ }
+
+ /**
+ * Contains a specific type of aggregate power stats. The contents type is determined by
+ * the section type.
+ */
+ public abstract static class Section {
+ private final String mType;
+
+ Section(String type) {
+ mType = type;
+ }
+
+ /**
+ * Returns the section type, which determines the type of data stored in the corresponding
+ * section of {@link PowerStatsSpan}
+ */
+ public String getType() {
+ return mType;
+ }
+
+ abstract void write(TypedXmlSerializer serializer) throws IOException;
+
+ /**
+ * Prints the section type.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ ipw.println(mType);
+ }
+ }
+
+ /**
+ * A universal XML parser for {@link PowerStatsSpan.Section}'s. It is aware of all
+ * supported section types as well as their corresponding XML formats.
+ */
+ public interface SectionReader {
+ /**
+ * Reads the contents of the section using the parser. The type of the object
+ * read and the corresponding XML format are determined by the section type.
+ */
+ Section read(String sectionType, TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException;
+ }
+
+ private final Metadata mMetadata;
+ private final List<Section> mSections = new ArrayList<>();
+
+ public PowerStatsSpan(long id) {
+ this(new Metadata(id));
+ }
+
+ private PowerStatsSpan(Metadata metadata) {
+ mMetadata = metadata;
+ }
+
+ public Metadata getMetadata() {
+ return mMetadata;
+ }
+
+ public long getId() {
+ return mMetadata.mId;
+ }
+
+ void addTimeFrame(long monotonicTime, @CurrentTimeMillisLong long wallClockTime,
+ @DurationMillisLong long duration) {
+ mMetadata.mTimeFrames.add(new TimeFrame(monotonicTime, wallClockTime, duration));
+ }
+
+ void addSection(Section section) {
+ mMetadata.addSection(section.getType());
+ mSections.add(section);
+ }
+
+ @NonNull
+ public List<Section> getSections() {
+ return mSections;
+ }
+
+ private static boolean isCompatibleXmlFormat(int version) {
+ return version == VERSION;
+ }
+
+ /**
+ * Creates an XML file containing the persistent state of the power stats span.
+ */
+ @VisibleForTesting
+ public void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException {
+ serializer.setOutput(out, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ mMetadata.write(serializer);
+ for (Section section : mSections) {
+ serializer.startTag(null, XML_TAG_SECTION);
+ serializer.attribute(null, XML_ATTR_SECTION_TYPE, section.mType);
+ section.write(serializer);
+ serializer.endTag(null, XML_TAG_SECTION);
+ }
+ serializer.endDocument();
+ }
+
+ @Nullable
+ static PowerStatsSpan read(InputStream in, TypedXmlPullParser parser,
+ SectionReader sectionReader, String... sectionTypes)
+ throws IOException, XmlPullParserException {
+ Set<String> neededSections = Sets.newArraySet(sectionTypes);
+ boolean selectSections = !neededSections.isEmpty();
+ parser.setInput(in, StandardCharsets.UTF_8.name());
+
+ Metadata metadata = Metadata.read(parser);
+ if (metadata == null) {
+ return null;
+ }
+
+ PowerStatsSpan span = new PowerStatsSpan(metadata);
+ boolean skipSection = false;
+ int nestingLevel = 0;
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (skipSection) {
+ if (eventType == XmlPullParser.END_TAG
+ && parser.getName().equals(XML_TAG_SECTION)) {
+ nestingLevel--;
+ if (nestingLevel == 0) {
+ skipSection = false;
+ }
+ } else if (eventType == XmlPullParser.START_TAG
+ && parser.getName().equals(XML_TAG_SECTION)) {
+ nestingLevel++;
+ }
+ } else if (eventType == XmlPullParser.START_TAG) {
+ String tag = parser.getName();
+ if (tag.equals(XML_TAG_SECTION)) {
+ String sectionType = parser.getAttributeValue(null, XML_ATTR_SECTION_TYPE);
+ if (!selectSections || neededSections.contains(sectionType)) {
+ Section section = sectionReader.read(sectionType, parser);
+ if (section == null) {
+ if (selectSections) {
+ throw new XmlPullParserException(
+ "Unsupported PowerStatsStore section type: " + sectionType);
+ } else {
+ section = new Section(sectionType) {
+ @Override
+ public void dump(IndentingPrintWriter ipw) {
+ ipw.println("Unsupported PowerStatsStore section type: "
+ + sectionType);
+ }
+
+ @Override
+ void write(TypedXmlSerializer serializer) {
+ }
+ };
+ }
+ }
+ span.addSection(section);
+ } else {
+ skipSection = true;
+ }
+ } else if (tag.equals(XML_TAG_METADATA)) {
+ Metadata.read(parser);
+ }
+ }
+ eventType = parser.next();
+ }
+ return span;
+ }
+
+ /**
+ * Prints the contents of this power stats span.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ mMetadata.dump(ipw, /* includeSections */ false);
+ for (Section section : mSections) {
+ ipw.increaseIndent();
+ ipw.println(section.mType);
+ section.dump(ipw);
+ ipw.decreaseIndent();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
new file mode 100644
index 000000000000..ec84fd7a6d28
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2023 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.BatteryUsageStats;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A storage mechanism for aggregated power/battery stats.
+ */
+public class PowerStatsStore {
+ private static final String TAG = "PowerStatsStore";
+
+ private static final String POWER_STATS_DIR = "power-stats";
+ private static final String POWER_STATS_SPAN_FILE_EXTENSION = ".pss";
+ private static final String DIR_LOCK_FILENAME = ".lock";
+ private static final long MAX_POWER_STATS_SPAN_STORAGE_BYTES = 100 * 1024;
+
+ private final File mSystemDir;
+ private final File mStoreDir;
+ private final File mLockFile;
+ private final ReentrantLock mFileLock = new ReentrantLock();
+ private FileLock mJvmLock;
+ private final long mMaxStorageBytes;
+ private final Handler mHandler;
+ private final PowerStatsSpan.SectionReader mSectionReader;
+ private final List<PowerStatsSpan.Metadata> mTableOfContents = new ArrayList<>();
+ private boolean mTableOfContentsLoaded;
+
+ public PowerStatsStore(@NonNull File systemDir, Handler handler) {
+ this(systemDir, MAX_POWER_STATS_SPAN_STORAGE_BYTES, handler);
+ }
+
+ @VisibleForTesting
+ public PowerStatsStore(@NonNull File systemDir, long maxStorageBytes, Handler handler) {
+ this(systemDir, maxStorageBytes, handler, new DefaultSectionReader());
+ }
+
+ @VisibleForTesting
+ public PowerStatsStore(@NonNull File systemDir, long maxStorageBytes, Handler handler,
+ @NonNull PowerStatsSpan.SectionReader sectionReader) {
+ mSystemDir = systemDir;
+ mStoreDir = new File(systemDir, POWER_STATS_DIR);
+ mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME);
+ mHandler = handler;
+ mMaxStorageBytes = maxStorageBytes;
+ mSectionReader = sectionReader;
+ mHandler.post(this::maybeClearLegacyStore);
+ }
+
+ /**
+ * Returns the metadata for all {@link PowerStatsSpan}'s contained in the store.
+ */
+ @NonNull
+ public List<PowerStatsSpan.Metadata> getTableOfContents() {
+ if (mTableOfContentsLoaded) {
+ return mTableOfContents;
+ }
+
+ TypedXmlPullParser parser = Xml.newBinaryPullParser();
+ lockStoreDirectory();
+ try {
+ for (File file : mStoreDir.listFiles()) {
+ String fileName = file.getName();
+ if (!fileName.endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) {
+ continue;
+ }
+ try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
+ parser.setInput(inputStream, StandardCharsets.UTF_8.name());
+ PowerStatsSpan.Metadata metadata = PowerStatsSpan.Metadata.read(parser);
+ if (metadata != null) {
+ mTableOfContents.add(metadata);
+ } else {
+ Slog.e(TAG, "Removing incompatible PowerStatsSpan file: " + fileName);
+ file.delete();
+ }
+ } catch (IOException | XmlPullParserException e) {
+ Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + fileName);
+ }
+ }
+ mTableOfContentsLoaded = true;
+ } finally {
+ unlockStoreDirectory();
+ }
+
+ mTableOfContents.sort(PowerStatsSpan.Metadata.COMPARATOR);
+ return mTableOfContents;
+ }
+
+ /**
+ * Saves the specified span in the store.
+ */
+ public void storePowerStatsSpan(PowerStatsSpan span) {
+ maybeClearLegacyStore();
+ lockStoreDirectory();
+ try {
+ if (!mStoreDir.exists()) {
+ if (!mStoreDir.mkdirs()) {
+ Slog.e(TAG, "Could not create a directory for power stats store");
+ return;
+ }
+ }
+
+ AtomicFile file = new AtomicFile(makePowerStatsSpanFilename(span.getId()));
+ file.write(out-> {
+ try {
+ span.writeXml(out, Xml.newBinarySerializer());
+ } catch (Exception e) {
+ // AtomicFile will log the exception and delete the file.
+ throw new RuntimeException(e);
+ }
+ });
+ if (mTableOfContentsLoaded) {
+ mTableOfContents.add(span.getMetadata());
+ mTableOfContents.sort(PowerStatsSpan.Metadata.COMPARATOR);
+ }
+ removeOldSpansLocked();
+ } finally {
+ unlockStoreDirectory();
+ }
+ }
+
+ /**
+ * Loads the PowerStatsSpan identified by its ID. Only loads the sections with
+ * the specified types. Loads all sections if no sectionTypes is empty.
+ */
+ @Nullable
+ public PowerStatsSpan loadPowerStatsSpan(long id, String... sectionTypes) {
+ TypedXmlPullParser parser = Xml.newBinaryPullParser();
+ lockStoreDirectory();
+ try {
+ File file = makePowerStatsSpanFilename(id);
+ try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
+ return PowerStatsSpan.read(inputStream, parser, mSectionReader, sectionTypes);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + file);
+ }
+ } finally {
+ unlockStoreDirectory();
+ }
+ return null;
+ }
+
+ /**
+ * Stores a {@link PowerStatsSpan} containing a single section for the supplied
+ * battery usage stats.
+ */
+ public void storeBatteryUsageStats(long monotonicStartTime,
+ BatteryUsageStats batteryUsageStats) {
+ PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime);
+ span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(),
+ batteryUsageStats.getStatsDuration());
+ span.addSection(new BatteryUsageStatsSection(batteryUsageStats));
+ storePowerStatsSpan(span);
+ }
+
+ /**
+ * Creates a file name by formatting the span ID as a 19-digit zero-padded number.
+ * This ensures that the lexicographically sorted directory follows the chronological order.
+ */
+ private File makePowerStatsSpanFilename(long id) {
+ return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", id)
+ + POWER_STATS_SPAN_FILE_EXTENSION);
+ }
+
+ private void maybeClearLegacyStore() {
+ File legacyStoreDir = new File(mSystemDir, "battery-usage-stats");
+ if (legacyStoreDir.exists()) {
+ FileUtils.deleteContentsAndDir(legacyStoreDir);
+ }
+ }
+
+ private void lockStoreDirectory() {
+ mFileLock.lock();
+
+ // Lock the directory from access by other JVMs
+ try {
+ mLockFile.getParentFile().mkdirs();
+ mLockFile.createNewFile();
+ mJvmLock = FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock();
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot lock snapshot directory", e);
+ }
+ }
+
+ private void unlockStoreDirectory() {
+ try {
+ mJvmLock.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot unlock snapshot directory", e);
+ } finally {
+ mFileLock.unlock();
+ }
+ }
+
+ private void removeOldSpansLocked() {
+ // Read the directory list into a _sorted_ map. The alphanumeric ordering
+ // corresponds to the historical order of snapshots because the file names
+ // are timestamps zero-padded to the same length.
+ long totalSize = 0;
+ TreeMap<File, Long> mFileSizes = new TreeMap<>();
+ for (File file : mStoreDir.listFiles()) {
+ final long fileSize = file.length();
+ totalSize += fileSize;
+ if (file.getName().endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) {
+ mFileSizes.put(file, fileSize);
+ }
+ }
+
+ while (totalSize > mMaxStorageBytes) {
+ final Map.Entry<File, Long> entry = mFileSizes.firstEntry();
+ if (entry == null) {
+ break;
+ }
+
+ File file = entry.getKey();
+ if (!file.delete()) {
+ Slog.e(TAG, "Cannot delete power stats span " + file);
+ }
+ totalSize -= entry.getValue();
+ mFileSizes.remove(file);
+ mTableOfContentsLoaded = false;
+ }
+ }
+
+ /**
+ * Deletes all contents from the store.
+ */
+ public void reset() {
+ lockStoreDirectory();
+ try {
+ for (File file : mStoreDir.listFiles()) {
+ if (file.getName().endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) {
+ if (!file.delete()) {
+ Slog.e(TAG, "Cannot delete power stats span " + file);
+ }
+ }
+ }
+ mTableOfContents.clear();
+ mTableOfContentsLoaded = false;
+ } finally {
+ unlockStoreDirectory();
+ }
+ }
+
+ /**
+ * Prints the summary of contents of the store: only metadata, but not the actual stored
+ * objects.
+ */
+ public void dumpTableOfContents(IndentingPrintWriter ipw) {
+ ipw.println("Power stats store TOC");
+ ipw.increaseIndent();
+ List<PowerStatsSpan.Metadata> contents = getTableOfContents();
+ for (PowerStatsSpan.Metadata metadata : contents) {
+ metadata.dump(ipw);
+ }
+ ipw.decreaseIndent();
+ }
+
+ /**
+ * Prints the contents of the store.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ ipw.println("Power stats store");
+ ipw.increaseIndent();
+ List<PowerStatsSpan.Metadata> contents = getTableOfContents();
+ for (PowerStatsSpan.Metadata metadata : contents) {
+ PowerStatsSpan span = loadPowerStatsSpan(metadata.getId());
+ if (span != null) {
+ span.dump(ipw);
+ }
+ }
+ ipw.decreaseIndent();
+ }
+
+ private static class DefaultSectionReader implements PowerStatsSpan.SectionReader {
+ @Override
+ public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ if (BatteryUsageStatsSection.TYPE.equals(sectionType)) {
+ return new BatteryUsageStatsSection(BatteryUsageStats.createFromXml(parser));
+ }
+ return null;
+ }
+ }
+}
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 5df0acb65249..0e14aff66e6d 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
@@ -40,6 +40,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.os.BatteryStatsHistoryIterator;
+import com.android.internal.os.MonotonicClock;
import com.android.internal.os.PowerProfile;
import org.junit.Rule;
@@ -64,6 +65,8 @@ public class BatteryUsageStatsProviderTest {
new BatteryUsageStatsRule(12345, mHistoryDir)
.setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0)
.setAveragePower(PowerProfile.POWER_AUDIO, 720.0);
+ private MockClock mMockClock = mStatsRule.getMockClock();
+
@Test
public void test_getBatteryUsageStats() {
BatteryStatsImpl batteryStats = prepareBatteryStats();
@@ -369,17 +372,23 @@ public class BatteryUsageStatsProviderTest {
public void testAggregateBatteryStats() {
Context context = InstrumentationRegistry.getContext();
BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
- mStatsRule.setCurrentTime(5 * MINUTE_IN_MS);
+ MonotonicClock monotonicClock = new MonotonicClock(0, mStatsRule.getMockClock());
+
+ setTime(5 * MINUTE_IN_MS);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
- BatteryUsageStatsStore batteryUsageStatsStore = new BatteryUsageStatsStore(context,
- batteryStats, new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
- new TestHandler(), Integer.MAX_VALUE);
- batteryUsageStatsStore.onSystemReady();
+
+ PowerStatsStore powerStatsStore = new PowerStatsStore(
+ new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
+ Integer.MAX_VALUE, new TestHandler());
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context,
- batteryStats, batteryUsageStatsStore);
+ batteryStats, powerStatsStore);
+
+ batteryStats.setBatteryResetListener(reason ->
+ powerStatsStore.storeBatteryUsageStats(monotonicClock.monotonicTime(),
+ provider.getBatteryUsageStats(BatteryUsageStatsQuery.DEFAULT)));
synchronized (batteryStats) {
batteryStats.noteFlashlightOnLocked(APP_UID,
@@ -389,7 +398,7 @@ public class BatteryUsageStatsProviderTest {
batteryStats.noteFlashlightOffLocked(APP_UID,
20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
}
- mStatsRule.setCurrentTime(25 * MINUTE_IN_MS);
+ setTime(25 * MINUTE_IN_MS);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
@@ -402,7 +411,7 @@ public class BatteryUsageStatsProviderTest {
batteryStats.noteFlashlightOffLocked(APP_UID,
50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
}
- mStatsRule.setCurrentTime(55 * MINUTE_IN_MS);
+ setTime(55 * MINUTE_IN_MS);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
@@ -416,7 +425,7 @@ public class BatteryUsageStatsProviderTest {
batteryStats.noteFlashlightOffLocked(APP_UID,
70 * MINUTE_IN_MS, 70 * MINUTE_IN_MS);
}
- mStatsRule.setCurrentTime(75 * MINUTE_IN_MS);
+ setTime(75 * MINUTE_IN_MS);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
@@ -430,7 +439,7 @@ public class BatteryUsageStatsProviderTest {
batteryStats.noteFlashlightOffLocked(APP_UID,
90 * MINUTE_IN_MS, 90 * MINUTE_IN_MS);
}
- mStatsRule.setCurrentTime(95 * MINUTE_IN_MS);
+ setTime(95 * MINUTE_IN_MS);
// Include the first and the second snapshot, but not the third or current
BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
@@ -457,29 +466,41 @@ public class BatteryUsageStatsProviderTest {
.of(180.0);
}
+ private void setTime(long timeMs) {
+ mMockClock.currentTime = timeMs;
+ mMockClock.realtime = timeMs;
+ }
+
@Test
public void testAggregateBatteryStats_incompatibleSnapshot() {
Context context = InstrumentationRegistry.getContext();
MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
- BatteryUsageStatsStore batteryUsageStatsStore = mock(BatteryUsageStatsStore.class);
-
- when(batteryUsageStatsStore.listBatteryUsageStatsTimestamps())
- .thenReturn(new long[]{1000, 2000});
+ PowerStatsStore powerStatsStore = mock(PowerStatsStore.class);
- when(batteryUsageStatsStore.loadBatteryUsageStats(1000)).thenReturn(
+ PowerStatsSpan span0 = new PowerStatsSpan(0);
+ span0.addTimeFrame(0, 1000, 1234);
+ span0.addSection(new BatteryUsageStatsSection(
new BatteryUsageStats.Builder(batteryStats.getCustomEnergyConsumerNames())
- .setStatsDuration(1234).build());
+ .setStatsDuration(1234).build()));
- // Add a snapshot, with a different set of custom power components. It should
- // be skipped by the aggregation.
- when(batteryUsageStatsStore.loadBatteryUsageStats(2000)).thenReturn(
+ PowerStatsSpan span1 = new PowerStatsSpan(1);
+ span1.addTimeFrame(0, 2000, 4321);
+ span1.addSection(new BatteryUsageStatsSection(
new BatteryUsageStats.Builder(new String[]{"different"})
- .setStatsDuration(4321).build());
+ .setStatsDuration(4321).build()));
+
+ when(powerStatsStore.getTableOfContents()).thenReturn(
+ List.of(span0.getMetadata(), span1.getMetadata()));
+
+ when(powerStatsStore.loadPowerStatsSpan(0, BatteryUsageStatsSection.TYPE))
+ .thenReturn(span0);
+ when(powerStatsStore.loadPowerStatsSpan(1, BatteryUsageStatsSection.TYPE))
+ .thenReturn(span1);
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context,
- batteryStats, batteryUsageStatsStore);
+ batteryStats, powerStatsStore);
BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
.aggregateSnapshots(0, 3000)
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 93cbea6125fa..3579fce11c8d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -88,6 +88,10 @@ public class BatteryUsageStatsRule implements TestRule {
mBatteryStats.onSystemReady();
}
+ public MockClock getMockClock() {
+ return mMockClock;
+ }
+
public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) {
mPowerProfile.forceInitForTesting(mContext, xmlId);
return this;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
deleted file mode 100644
index b846e3a36656..000000000000
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-
-import android.content.Context;
-import android.os.BatteryManager;
-import android.os.BatteryUsageStats;
-import android.os.BatteryUsageStatsQuery;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.util.Xml;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.os.PowerProfile;
-import com.android.modules.utils.TypedXmlSerializer;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-
-@RunWith(AndroidJUnit4.class)
-@SuppressWarnings("GuardedBy")
-public class BatteryUsageStatsStoreTest {
- private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024;
-
- private final MockClock mMockClock = new MockClock();
- private MockBatteryStatsImpl mBatteryStats;
- private BatteryUsageStatsStore mBatteryUsageStatsStore;
- private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
- private File mStoreDirectory;
-
- @Before
- public void setup() {
- mMockClock.currentTime = 123;
- mBatteryStats = new MockBatteryStatsImpl(mMockClock);
- mBatteryStats.setNoAutoReset(true);
- mBatteryStats.setPowerProfile(mock(PowerProfile.class));
- mBatteryStats.onSystemReady();
-
- Context context = InstrumentationRegistry.getContext();
-
- mStoreDirectory = new File(context.getCacheDir(), "BatteryUsageStatsStoreTest");
- clearDirectory(mStoreDirectory);
-
- mBatteryUsageStatsStore = new BatteryUsageStatsStore(context, mBatteryStats,
- mStoreDirectory, new TestHandler(), MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
- mBatteryUsageStatsStore.onSystemReady();
-
- mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats);
- }
-
- @Test
- public void testStoreSnapshot() {
- mMockClock.currentTime = 1_600_000;
- mMockClock.realtime = 1000;
- mMockClock.uptime = 1000;
-
- prepareBatteryStats();
-
- mMockClock.realtime = 1_000_000;
- mMockClock.uptime = 1_000_000;
- mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
-
- final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
- assertThat(timestamps).hasLength(1);
- assertThat(timestamps[0]).isEqualTo(1_600_000);
-
- final BatteryUsageStats batteryUsageStats = mBatteryUsageStatsStore.loadBatteryUsageStats(
- 1_600_000);
- assertThat(batteryUsageStats.getStatsStartTimestamp()).isEqualTo(123);
- assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(1_600_000);
- assertThat(batteryUsageStats.getBatteryCapacity()).isEqualTo(4000);
- assertThat(batteryUsageStats.getDischargePercentage()).isEqualTo(5);
- assertThat(batteryUsageStats.getDischargeDurationMs()).isEqualTo(1_000_000 - 1_000);
- assertThat(batteryUsageStats.getAggregateBatteryConsumer(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE).getConsumedPower())
- .isEqualTo(600); // (3_600_000 - 3_000_000) / 1000
- }
-
- @Test
- public void testGarbageCollectOldSnapshots() throws Exception {
- prepareBatteryStats();
-
- mMockClock.realtime = 10_000_000;
- mMockClock.uptime = 10_000_000;
- mMockClock.currentTime = 10_000_000;
-
- final int snapshotFileSize = getSnapshotFileSize();
- final int numberOfSnapshots =
- (int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize);
- for (int i = 0; i < numberOfSnapshots + 2; i++) {
- mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
-
- mMockClock.realtime += 10_000_000;
- mMockClock.uptime += 10_000_000;
- mMockClock.currentTime += 10_000_000;
- prepareBatteryStats();
- }
-
- final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
- Arrays.sort(timestamps);
- assertThat(timestamps).hasLength(numberOfSnapshots);
- // Two snapshots (10_000_000 and 20_000_000) should have been discarded
- assertThat(timestamps[0]).isEqualTo(30_000_000);
- assertThat(getDirectorySize(mStoreDirectory))
- .isAtMost(MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
- }
-
- @Test
- public void testRemoveAllSnapshots() throws Exception {
- prepareBatteryStats();
-
- for (int i = 0; i < 3; i++) {
- mMockClock.realtime += 10_000_000;
- mMockClock.uptime += 10_000_000;
- mMockClock.currentTime += 10_000_000;
- prepareBatteryStats();
-
- mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
- }
-
- assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);
-
- mBatteryUsageStatsStore.removeAllSnapshots();
-
- assertThat(getDirectorySize(mStoreDirectory)).isEqualTo(0);
- }
-
- @Test
- public void testSavingStatsdAtomPullTimestamp() {
- mBatteryUsageStatsStore.setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(1234);
- assertThat(mBatteryUsageStatsStore.getLastBatteryUsageStatsBeforeResetAtomPullTimestamp())
- .isEqualTo(1234);
- mBatteryUsageStatsStore.setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(5478);
- assertThat(mBatteryUsageStatsStore.getLastBatteryUsageStatsBeforeResetAtomPullTimestamp())
- .isEqualTo(5478);
- }
-
- private void prepareBatteryStats() {
- mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
- mMockClock.realtime, mMockClock.uptime, mMockClock.currentTime);
- mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
- mMockClock.realtime + 500_000, mMockClock.uptime + 500_000,
- mMockClock.currentTime + 500_000);
- }
-
- private void clearDirectory(File dir) {
- if (dir.exists()) {
- for (File child : dir.listFiles()) {
- if (child.isDirectory()) {
- clearDirectory(child);
- }
- child.delete();
- }
- }
- }
-
- private long getDirectorySize(File dir) {
- long size = 0;
- if (dir.exists()) {
- for (File child : dir.listFiles()) {
- if (child.isDirectory()) {
- size += getDirectorySize(child);
- } else {
- size += child.length();
- }
- }
- }
- return size;
- }
-
- private int getSnapshotFileSize() throws IOException {
- BatteryUsageStats stats = mBatteryUsageStatsProvider.getBatteryUsageStats(
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includePowerModels()
- .build());
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- TypedXmlSerializer serializer = Xml.newBinarySerializer();
- serializer.setOutput(out, StandardCharsets.UTF_8.name());
- serializer.startDocument(null, true);
- stats.writeXml(serializer);
- serializer.endDocument();
- return out.toByteArray().length;
- }
-
- private static class TestHandler extends Handler {
- TestHandler() {
- super(Looper.getMainLooper());
- }
-
- @Override
- public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
- msg.getCallback().run();
- return true;
- }
- }
-}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsSchedulerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsSchedulerTest.java
new file mode 100644
index 000000000000..87d09f33101d
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsSchedulerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 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 com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.BatteryConsumer;
+import android.os.BatteryManager;
+import android.os.BatteryUsageStats;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.MonotonicClock;
+import com.android.internal.os.PowerProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class PowerStatsSchedulerTest {
+ private PowerStatsStore mPowerStatsStore;
+ private Handler mHandler;
+ private MockClock mClock = new MockClock();
+ private MonotonicClock mMonotonicClock = new MonotonicClock(0, mClock);
+ private MockBatteryStatsImpl mBatteryStats;
+ private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
+ private PowerStatsScheduler mPowerStatsScheduler;
+ private PowerProfile mPowerProfile;
+
+ @Before
+ public void setup() {
+ final Context context = InstrumentationRegistry.getContext();
+
+ mClock.currentTime = 1234567;
+ mClock.realtime = 7654321;
+
+ HandlerThread bgThread = new HandlerThread("bg thread");
+ bgThread.start();
+ File systemDir = context.getCacheDir();
+ mHandler = new Handler(bgThread.getLooper());
+ mPowerStatsStore = new PowerStatsStore(systemDir, mHandler);
+ mPowerProfile = mock(PowerProfile.class);
+ when(mPowerProfile.getAveragePower(PowerProfile.POWER_FLASHLIGHT)).thenReturn(1000000.0);
+ mBatteryStats = new MockBatteryStatsImpl(mClock).setPowerProfile(mPowerProfile);
+ mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats);
+ mPowerStatsScheduler = new PowerStatsScheduler(mPowerStatsStore, mMonotonicClock, mHandler,
+ mBatteryStats, mBatteryUsageStatsProvider);
+ }
+
+ @Test
+ public void storeBatteryUsageStatsOnReset() {
+ mBatteryStats.forceRecordAllHistory();
+ synchronized (mBatteryStats) {
+ mBatteryStats.setOnBatteryLocked(mClock.realtime, mClock.uptime, true,
+ BatteryManager.BATTERY_STATUS_DISCHARGING, 50, 0);
+ }
+
+ mPowerStatsStore.reset();
+
+ assertThat(mPowerStatsStore.getTableOfContents()).isEmpty();
+
+ mPowerStatsScheduler.start();
+
+ synchronized (mBatteryStats) {
+ mBatteryStats.noteFlashlightOnLocked(42, mClock.realtime, mClock.uptime);
+ }
+
+ mClock.realtime += 60000;
+ mClock.currentTime += 60000;
+
+ synchronized (mBatteryStats) {
+ mBatteryStats.noteFlashlightOffLocked(42, mClock.realtime, mClock.uptime);
+ }
+
+ mClock.realtime += 60000;
+ mClock.currentTime += 60000;
+
+ // Battery stats reset should have the side-effect of saving accumulated battery usage stats
+ synchronized (mBatteryStats) {
+ mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ // Await completion
+ ConditionVariable done = new ConditionVariable();
+ mHandler.post(done::open);
+ done.block();
+
+ List<PowerStatsSpan.Metadata> contents = mPowerStatsStore.getTableOfContents();
+ assertThat(contents).hasSize(1);
+
+ PowerStatsSpan.Metadata metadata = contents.get(0);
+
+ PowerStatsSpan span = mPowerStatsStore.loadPowerStatsSpan(metadata.getId(),
+ BatteryUsageStatsSection.TYPE);
+ assertThat(span).isNotNull();
+
+ List<PowerStatsSpan.TimeFrame> timeFrames = span.getMetadata().getTimeFrames();
+ assertThat(timeFrames).hasSize(1);
+ assertThat(timeFrames.get(0).startTime).isEqualTo(1234567);
+ assertThat(timeFrames.get(0).duration).isEqualTo(120000);
+
+ List<PowerStatsSpan.Section> sections = span.getSections();
+ assertThat(sections).hasSize(1);
+
+ PowerStatsSpan.Section section = sections.get(0);
+ assertThat(section.getType()).isEqualTo(BatteryUsageStatsSection.TYPE);
+ BatteryUsageStats bus = ((BatteryUsageStatsSection) section).getBatteryUsageStats();
+ assertThat(bus.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isEqualTo(60000);
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsStoreTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsStoreTest.java
new file mode 100644
index 000000000000..d3628b5888c8
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsStoreTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 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 com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SuppressWarnings("GuardedBy")
+public class PowerStatsStoreTest {
+ private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024;
+
+ private PowerStatsStore mPowerStatsStore;
+ private File mStoreDirectory;
+
+ @Before
+ public void setup() {
+ Context context = InstrumentationRegistry.getContext();
+
+ mStoreDirectory = new File(context.getCacheDir(), "PowerStatsStoreTest");
+ clearDirectory(mStoreDirectory);
+
+ mPowerStatsStore = new PowerStatsStore(mStoreDirectory,
+ MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES,
+ new TestHandler(),
+ (sectionType, parser) -> {
+ if (sectionType.equals(TestSection.TYPE)) {
+ return TestSection.readXml(parser);
+ }
+ return null;
+ });
+ }
+
+ @Test
+ public void garbageCollectOldSpans() throws Exception {
+ int spanSize = 500;
+ final int numberOfSnaps =
+ (int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / spanSize);
+ for (int i = 0; i < numberOfSnaps + 2; i++) {
+ PowerStatsSpan span = new PowerStatsSpan(i);
+ span.addSection(new TestSection(i, spanSize));
+ mPowerStatsStore.storePowerStatsSpan(span);
+ }
+
+ assertThat(getDirectorySize(mStoreDirectory))
+ .isAtMost(MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
+
+ List<PowerStatsSpan.Metadata> toc = mPowerStatsStore.getTableOfContents();
+ assertThat(toc.size()).isLessThan(numberOfSnaps);
+ int minPreservedSpanId = numberOfSnaps - toc.size();
+ for (PowerStatsSpan.Metadata metadata : toc) {
+ assertThat(metadata.getId()).isAtLeast(minPreservedSpanId);
+ }
+ }
+
+ @Test
+ public void reset() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ PowerStatsSpan span = new PowerStatsSpan(i);
+ span.addSection(new TestSection(i, 42));
+ mPowerStatsStore.storePowerStatsSpan(span);
+ }
+
+ assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);
+
+ mPowerStatsStore.reset();
+
+ assertThat(getDirectorySize(mStoreDirectory)).isEqualTo(0);
+ }
+
+ private void clearDirectory(File dir) {
+ if (dir.exists()) {
+ for (File child : dir.listFiles()) {
+ if (child.isDirectory()) {
+ clearDirectory(child);
+ }
+ child.delete();
+ }
+ }
+ }
+
+ private long getDirectorySize(File dir) {
+ long size = 0;
+ if (dir.exists()) {
+ for (File child : dir.listFiles()) {
+ if (child.isDirectory()) {
+ size += getDirectorySize(child);
+ } else {
+ size += child.length();
+ }
+ }
+ }
+ return size;
+ }
+
+ private static class TestSection extends PowerStatsSpan.Section {
+ public static final String TYPE = "much-text";
+
+ private final int mSize;
+ private final int mValue;
+
+ TestSection(int value, int size) {
+ super(TYPE);
+ mSize = size;
+ mValue = value;
+ }
+
+ @Override
+ void write(TypedXmlSerializer serializer) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mSize; i++) {
+ sb.append("X");
+ }
+ serializer.startTag(null, "much-text");
+ serializer.attributeInt(null, "value", mValue);
+ serializer.text(sb.toString());
+ serializer.endTag(null, "much-text");
+ }
+
+ public static TestSection readXml(TypedXmlPullParser parser) throws XmlPullParserException {
+ TestSection section = null;
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && !(eventType == XmlPullParser.END_TAG
+ && parser.getName().equals("much-text"))) {
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("much-text")) {
+ section = new TestSection(parser.getAttributeInt(null, "value"), 0);
+ }
+ }
+ return section;
+ }
+ }
+
+ private static class TestHandler extends Handler {
+ TestHandler() {
+ super(Looper.getMainLooper());
+ }
+
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ msg.getCallback().run();
+ return true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
index 988cd818ca28..feb6bd930bf3 100644
--- a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.am;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertTrue;
import android.content.Context;
@@ -34,6 +36,7 @@ import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -49,8 +52,9 @@ public final class BatteryStatsServiceTest {
final Context context = InstrumentationRegistry.getContext();
mBgThread = new HandlerThread("bg thread");
mBgThread.start();
- mBatteryStatsService = new BatteryStatsService(context,
- context.getCacheDir(), new Handler(mBgThread.getLooper()));
+ File systemDir = context.getCacheDir();
+ Handler handler = new Handler(mBgThread.getLooper());
+ mBatteryStatsService = new BatteryStatsService(context, systemDir, handler);
}
@After
@@ -121,4 +125,14 @@ public final class BatteryStatsServiceTest {
waitThread.join(1000);
}
}
+
+ @Test
+ public void testSavingStatsdAtomPullTimestamp() {
+ mBatteryStatsService.setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(1234);
+ assertThat(mBatteryStatsService.getLastBatteryUsageStatsBeforeResetAtomPullTimestamp())
+ .isEqualTo(1234);
+ mBatteryStatsService.setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(5478);
+ assertThat(mBatteryStatsService.getLastBatteryUsageStatsBeforeResetAtomPullTimestamp())
+ .isEqualTo(5478);
+ }
}