diff options
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 <metadata> 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); + } } |