Loading core/java/com/android/internal/os/BatteryStatsImpl.java +21 −0 Original line number Diff line number Diff line Loading @@ -341,6 +341,19 @@ public class BatteryStatsImpl extends BatteryStats { } } /** * Listener for the battery stats reset. */ public interface BatteryResetListener { /** * Callback invoked immediately prior to resetting battery stats. */ void prepareForBatteryStatsReset(); } private BatteryResetListener mBatteryResetListener; public interface BatteryCallback { public void batteryNeedsCpuUpdate(); public void batteryPowerChanged(boolean onBattery); Loading Loading @@ -11186,6 +11199,10 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter.reset(false, elapsedRealtimeUs); } public void setBatteryResetListener(BatteryResetListener batteryResetListener) { mBatteryResetListener = batteryResetListener; } public void resetAllStatsCmdLocked() { final long mSecUptime = mClocks.uptimeMillis(); long uptimeUs = mSecUptime * 1000; Loading Loading @@ -11221,6 +11238,10 @@ public class BatteryStatsImpl extends BatteryStats { } private void resetAllStatsLocked(long uptimeMillis, long elapsedRealtimeMillis) { if (mBatteryResetListener != null) { mBatteryResetListener.prepareForBatteryStatsReset(); } final long uptimeUs = uptimeMillis * 1000; final long elapsedRealtimeUs = elapsedRealtimeMillis * 1000; mStartCount = 0; Loading core/java/com/android/internal/os/BatteryUsageStatsStore.java 0 → 100644 +231 −0 Original line number Diff line number Diff line /* * 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.internal.os; import android.annotation.Nullable; import android.content.Context; import android.os.BatteryUsageStats; import android.os.BatteryUsageStatsQuery; import android.os.Handler; import android.util.LongArray; import android.util.Slog; import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; 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.TreeMap; /** * 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() .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 long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024; private final Context mContext; private final BatteryStatsImpl mBatteryStats; private final File mStoreDir; private final File mLockFile; 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); mHandler = handler; mMaxStorageBytes = maxStorageBytes; mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset); mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats); } private void prepareForBatteryStatsReset() { 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) { try (FileLock lock = lockSnapshotDirectory()) { 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(); } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } } /** * 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); try (FileLock lock = lockSnapshotDirectory()) { 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); } } } } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } return timestamps.toArray(); } /** * Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot * does not exist. */ @Nullable public BatteryUsageStats loadBatteryUsageStats(long timestamp) { try (FileLock lock = lockSnapshotDirectory()) { File file = makeSnapshotFilename(timestamp); try { return readXmlFileLocked(file); } catch (Exception e) { Slog.e(TAG, "Cannot read battery usage stats", e); } } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } return null; } private FileLock lockSnapshotDirectory() throws IOException { mLockFile.getParentFile().mkdirs(); mLockFile.createNewFile(); return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); } /** * 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); } } } core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java +1 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import org.junit.runners.Suite; BatteryStatsUidTest.class, BatteryUsageStatsProviderTest.class, BatteryUsageStatsTest.class, BatteryUsageStatsStoreTest.class, BatteryStatsUserLifecycleTests.class, BluetoothPowerCalculatorTest.class, BstatsCpuTimesValidationTest.class, Loading core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * 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.internal.os; import static com.google.common.truth.Truth.assertThat; 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.TypedXmlSerializer; import android.util.Xml; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; 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) public class BatteryUsageStatsStoreTest { private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024; private final MockClocks mMockClocks = new MockClocks(); private MockBatteryStatsImpl mBatteryStats; private BatteryUsageStatsStore mBatteryUsageStatsStore; private BatteryUsageStatsProvider mBatteryUsageStatsProvider; private File mStoreDirectory; @Before public void setup() { mMockClocks.currentTime = 123; mBatteryStats = new MockBatteryStatsImpl(mMockClocks); mBatteryStats.setNoAutoReset(true); 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); mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats); } @Test public void testStoreSnapshot() { mMockClocks.currentTime = 1_600_000; prepareBatteryStats(); mBatteryStats.resetAllStatsCmdLocked(); 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.getAggregateBatteryConsumer( BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE).getConsumedPower()) .isEqualTo(600); // (3_600_000 - 3_000_000) / 1000 } @Test public void testGarbageCollectOldSnapshots() throws Exception { prepareBatteryStats(); mMockClocks.realtime = 10_000_000; mMockClocks.uptime = 10_000_000; mMockClocks.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.resetAllStatsCmdLocked(); mMockClocks.realtime += 10_000_000; mMockClocks.uptime += 10_000_000; mMockClocks.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); } private void prepareBatteryStats() { mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, mMockClocks.realtime, mMockClocks.uptime, mMockClocks.currentTime); mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0, mMockClocks.realtime + 500_000, mMockClocks.uptime + 500_000, mMockClocks.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; } } } services/core/java/com/android/server/am/BatteryStatsService.java +9 −0 Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.BatteryStatsImpl; import com.android.internal.os.BatteryUsageStatsProvider; import com.android.internal.os.BatteryUsageStatsStore; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.PowerProfile; import com.android.internal.os.RailStats; Loading Loading @@ -124,10 +125,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub Watchdog.Monitor { static final String TAG = "BatteryStatsService"; static final boolean DBG = false; private static final boolean BATTERY_USAGE_STORE_ENABLED = false; private static IBatteryStats sService; final BatteryStatsImpl mStats; private final BatteryUsageStatsStore mBatteryUsageStatsStore; private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider; private final Context mContext; private final BatteryExternalStatsWorker mWorker; Loading Loading @@ -341,6 +344,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub mStats = new BatteryStatsImpl(systemDir, handler, this, this, mUserManagerUserInfoProvider); if (BATTERY_USAGE_STORE_ENABLED) { mBatteryUsageStatsStore = new BatteryUsageStatsStore(context, mStats, systemDir, mHandler); } else { mBatteryUsageStatsStore = null; } mWorker = new BatteryExternalStatsWorker(context, mStats); mStats.setExternalStatsSyncLocked(mWorker); mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger( Loading Loading
core/java/com/android/internal/os/BatteryStatsImpl.java +21 −0 Original line number Diff line number Diff line Loading @@ -341,6 +341,19 @@ public class BatteryStatsImpl extends BatteryStats { } } /** * Listener for the battery stats reset. */ public interface BatteryResetListener { /** * Callback invoked immediately prior to resetting battery stats. */ void prepareForBatteryStatsReset(); } private BatteryResetListener mBatteryResetListener; public interface BatteryCallback { public void batteryNeedsCpuUpdate(); public void batteryPowerChanged(boolean onBattery); Loading Loading @@ -11186,6 +11199,10 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter.reset(false, elapsedRealtimeUs); } public void setBatteryResetListener(BatteryResetListener batteryResetListener) { mBatteryResetListener = batteryResetListener; } public void resetAllStatsCmdLocked() { final long mSecUptime = mClocks.uptimeMillis(); long uptimeUs = mSecUptime * 1000; Loading Loading @@ -11221,6 +11238,10 @@ public class BatteryStatsImpl extends BatteryStats { } private void resetAllStatsLocked(long uptimeMillis, long elapsedRealtimeMillis) { if (mBatteryResetListener != null) { mBatteryResetListener.prepareForBatteryStatsReset(); } final long uptimeUs = uptimeMillis * 1000; final long elapsedRealtimeUs = elapsedRealtimeMillis * 1000; mStartCount = 0; Loading
core/java/com/android/internal/os/BatteryUsageStatsStore.java 0 → 100644 +231 −0 Original line number Diff line number Diff line /* * 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.internal.os; import android.annotation.Nullable; import android.content.Context; import android.os.BatteryUsageStats; import android.os.BatteryUsageStatsQuery; import android.os.Handler; import android.util.LongArray; import android.util.Slog; import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; 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.TreeMap; /** * 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() .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 long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024; private final Context mContext; private final BatteryStatsImpl mBatteryStats; private final File mStoreDir; private final File mLockFile; 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); mHandler = handler; mMaxStorageBytes = maxStorageBytes; mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset); mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats); } private void prepareForBatteryStatsReset() { 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) { try (FileLock lock = lockSnapshotDirectory()) { 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(); } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } } /** * 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); try (FileLock lock = lockSnapshotDirectory()) { 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); } } } } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } return timestamps.toArray(); } /** * Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot * does not exist. */ @Nullable public BatteryUsageStats loadBatteryUsageStats(long timestamp) { try (FileLock lock = lockSnapshotDirectory()) { File file = makeSnapshotFilename(timestamp); try { return readXmlFileLocked(file); } catch (Exception e) { Slog.e(TAG, "Cannot read battery usage stats", e); } } catch (IOException e) { Slog.e(TAG, "Cannot lock battery usage stats directory", e); } return null; } private FileLock lockSnapshotDirectory() throws IOException { mLockFile.getParentFile().mkdirs(); mLockFile.createNewFile(); return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); } /** * 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); } } }
core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java +1 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import org.junit.runners.Suite; BatteryStatsUidTest.class, BatteryUsageStatsProviderTest.class, BatteryUsageStatsTest.class, BatteryUsageStatsStoreTest.class, BatteryStatsUserLifecycleTests.class, BluetoothPowerCalculatorTest.class, BstatsCpuTimesValidationTest.class, Loading
core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * 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.internal.os; import static com.google.common.truth.Truth.assertThat; 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.TypedXmlSerializer; import android.util.Xml; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; 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) public class BatteryUsageStatsStoreTest { private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024; private final MockClocks mMockClocks = new MockClocks(); private MockBatteryStatsImpl mBatteryStats; private BatteryUsageStatsStore mBatteryUsageStatsStore; private BatteryUsageStatsProvider mBatteryUsageStatsProvider; private File mStoreDirectory; @Before public void setup() { mMockClocks.currentTime = 123; mBatteryStats = new MockBatteryStatsImpl(mMockClocks); mBatteryStats.setNoAutoReset(true); 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); mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats); } @Test public void testStoreSnapshot() { mMockClocks.currentTime = 1_600_000; prepareBatteryStats(); mBatteryStats.resetAllStatsCmdLocked(); 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.getAggregateBatteryConsumer( BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE).getConsumedPower()) .isEqualTo(600); // (3_600_000 - 3_000_000) / 1000 } @Test public void testGarbageCollectOldSnapshots() throws Exception { prepareBatteryStats(); mMockClocks.realtime = 10_000_000; mMockClocks.uptime = 10_000_000; mMockClocks.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.resetAllStatsCmdLocked(); mMockClocks.realtime += 10_000_000; mMockClocks.uptime += 10_000_000; mMockClocks.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); } private void prepareBatteryStats() { mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, mMockClocks.realtime, mMockClocks.uptime, mMockClocks.currentTime); mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0, mMockClocks.realtime + 500_000, mMockClocks.uptime + 500_000, mMockClocks.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; } } }
services/core/java/com/android/server/am/BatteryStatsService.java +9 −0 Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.BatteryStatsImpl; import com.android.internal.os.BatteryUsageStatsProvider; import com.android.internal.os.BatteryUsageStatsStore; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.PowerProfile; import com.android.internal.os.RailStats; Loading Loading @@ -124,10 +125,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub Watchdog.Monitor { static final String TAG = "BatteryStatsService"; static final boolean DBG = false; private static final boolean BATTERY_USAGE_STORE_ENABLED = false; private static IBatteryStats sService; final BatteryStatsImpl mStats; private final BatteryUsageStatsStore mBatteryUsageStatsStore; private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider; private final Context mContext; private final BatteryExternalStatsWorker mWorker; Loading Loading @@ -341,6 +344,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub mStats = new BatteryStatsImpl(systemDir, handler, this, this, mUserManagerUserInfoProvider); if (BATTERY_USAGE_STORE_ENABLED) { mBatteryUsageStatsStore = new BatteryUsageStatsStore(context, mStats, systemDir, mHandler); } else { mBatteryUsageStatsStore = null; } mWorker = new BatteryExternalStatsWorker(context, mStats); mStats.setExternalStatsSyncLocked(mWorker); mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger( Loading