blob: fd54b32bd12ff5588ac7327d0660928203773064 [file] [log] [blame]
/*
* 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.AtomicFile;
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.Properties;
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 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 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) {
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;
}
/**
* 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();
try (FileLock lock = lockSnapshotDirectory()) {
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);
}
} catch (IOException e) {
Slog.e(TAG, "Cannot lock battery usage stats directory", e);
}
}
/**
* Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET
* statsd atom pull.
*/
public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() {
Properties props = new Properties();
try (FileLock lock = lockSnapshotDirectory()) {
try (InputStream in = mConfigFile.openRead()) {
props.load(in);
} catch (IOException e) {
Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
}
} catch (IOException e) {
Slog.e(TAG, "Cannot lock battery usage stats directory", e);
}
return Long.parseLong(
props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0"));
}
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);
}
}
}