Implement BatteryUsageStatsStore

Bug: 187223764
Test: atest FrameworksCoreTests:BatteryUsageStatsStoreTest

Change-Id: I66bbb173f402e795c900ccf3bc82b8fb210142a4
This commit is contained in:
Dmitri Plotnikov
2021-05-19 13:52:35 -07:00
parent 054e2db802
commit 2a0460f701
5 changed files with 445 additions and 0 deletions

View File

@@ -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);
@@ -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;
@@ -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;

View File

@@ -0,0 +1,231 @@
/*
* 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);
}
}
}

View File

@@ -44,6 +44,7 @@ import org.junit.runners.Suite;
BatteryStatsUidTest.class,
BatteryUsageStatsProviderTest.class,
BatteryUsageStatsTest.class,
BatteryUsageStatsStoreTest.class,
BatteryStatsUserLifecycleTests.class,
BluetoothPowerCalculatorTest.class,
BstatsCpuTimesValidationTest.class,

View File

@@ -0,0 +1,183 @@
/*
* 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;
}
}
}

View File

@@ -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;
@@ -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;
@@ -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(