1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.os; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.os.BatteryUsageStats; 22 import android.os.BatteryUsageStatsQuery; 23 import android.os.Handler; 24 import android.util.AtomicFile; 25 import android.util.LongArray; 26 import android.util.Slog; 27 import android.util.TypedXmlPullParser; 28 import android.util.TypedXmlSerializer; 29 import android.util.Xml; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.OutputStream; 41 import java.nio.channels.FileChannel; 42 import java.nio.channels.FileLock; 43 import java.nio.charset.StandardCharsets; 44 import java.nio.file.StandardOpenOption; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.Map; 48 import java.util.Properties; 49 import java.util.TreeMap; 50 51 /** 52 * A storage mechanism for BatteryUsageStats snapshots. 53 */ 54 public class BatteryUsageStatsStore { 55 private static final String TAG = "BatteryUsageStatsStore"; 56 57 private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of( 58 new BatteryUsageStatsQuery.Builder() 59 .setMaxStatsAgeMs(0) 60 .includePowerModels() 61 .build()); 62 private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats"; 63 private static final String SNAPSHOT_FILE_EXTENSION = ".bus"; 64 private static final String DIR_LOCK_FILENAME = ".lock"; 65 private static final String CONFIG_FILENAME = "config"; 66 private static final String BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY = 67 "BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP"; 68 private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024; 69 70 private final Context mContext; 71 private final BatteryStatsImpl mBatteryStats; 72 private boolean mSystemReady; 73 private final File mStoreDir; 74 private final File mLockFile; 75 private final AtomicFile mConfigFile; 76 private final long mMaxStorageBytes; 77 private final Handler mHandler; 78 private final BatteryUsageStatsProvider mBatteryUsageStatsProvider; 79 BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir, Handler handler)80 public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir, 81 Handler handler) { 82 this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES); 83 } 84 85 @VisibleForTesting BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir, Handler handler, long maxStorageBytes)86 public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir, 87 Handler handler, long maxStorageBytes) { 88 mContext = context; 89 mBatteryStats = batteryStats; 90 mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR); 91 mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME); 92 mConfigFile = new AtomicFile(new File(mStoreDir, CONFIG_FILENAME)); 93 mHandler = handler; 94 mMaxStorageBytes = maxStorageBytes; 95 mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset); 96 mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats); 97 } 98 99 /** 100 * Notifies BatteryUsageStatsStore that the system server is ready. 101 */ onSystemReady()102 public void onSystemReady() { 103 mSystemReady = true; 104 } 105 prepareForBatteryStatsReset(int resetReason)106 private void prepareForBatteryStatsReset(int resetReason) { 107 if (resetReason == BatteryStatsImpl.RESET_REASON_CORRUPT_FILE || !mSystemReady) { 108 return; 109 } 110 111 final List<BatteryUsageStats> stats = 112 mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY); 113 if (stats.isEmpty()) { 114 Slog.wtf(TAG, "No battery usage stats generated"); 115 return; 116 } 117 118 mHandler.post(() -> storeBatteryUsageStats(stats.get(0))); 119 } 120 storeBatteryUsageStats(BatteryUsageStats stats)121 private void storeBatteryUsageStats(BatteryUsageStats stats) { 122 try (FileLock lock = lockSnapshotDirectory()) { 123 if (!mStoreDir.exists()) { 124 if (!mStoreDir.mkdirs()) { 125 Slog.e(TAG, 126 "Could not create a directory for battery usage stats snapshots"); 127 return; 128 } 129 } 130 File file = makeSnapshotFilename(stats.getStatsEndTimestamp()); 131 try { 132 writeXmlFileLocked(stats, file); 133 } catch (Exception e) { 134 Slog.e(TAG, "Cannot save battery usage stats", e); 135 } 136 137 removeOldSnapshotsLocked(); 138 } catch (IOException e) { 139 Slog.e(TAG, "Cannot lock battery usage stats directory", e); 140 } 141 } 142 143 /** 144 * Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds 145 * to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}. 146 */ listBatteryUsageStatsTimestamps()147 public long[] listBatteryUsageStatsTimestamps() { 148 LongArray timestamps = new LongArray(100); 149 try (FileLock lock = lockSnapshotDirectory()) { 150 for (File file : mStoreDir.listFiles()) { 151 String fileName = file.getName(); 152 if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) { 153 try { 154 String fileNameWithoutExtension = fileName.substring(0, 155 fileName.length() - SNAPSHOT_FILE_EXTENSION.length()); 156 timestamps.add(Long.parseLong(fileNameWithoutExtension)); 157 } catch (NumberFormatException e) { 158 Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: " 159 + fileName); 160 } 161 } 162 } 163 } catch (IOException e) { 164 Slog.e(TAG, "Cannot lock battery usage stats directory", e); 165 } 166 return timestamps.toArray(); 167 } 168 169 /** 170 * Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot 171 * does not exist. 172 */ 173 @Nullable loadBatteryUsageStats(long timestamp)174 public BatteryUsageStats loadBatteryUsageStats(long timestamp) { 175 try (FileLock lock = lockSnapshotDirectory()) { 176 File file = makeSnapshotFilename(timestamp); 177 try { 178 return readXmlFileLocked(file); 179 } catch (Exception e) { 180 Slog.e(TAG, "Cannot read battery usage stats", e); 181 } 182 } catch (IOException e) { 183 Slog.e(TAG, "Cannot lock battery usage stats directory", e); 184 } 185 return null; 186 } 187 188 /** 189 * Saves the supplied timestamp of the BATTERY_USAGE_STATS_BEFORE_RESET statsd atom pull 190 * in persistent file. 191 */ setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp)192 public void setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp) { 193 Properties props = new Properties(); 194 try (FileLock lock = lockSnapshotDirectory()) { 195 try (InputStream in = mConfigFile.openRead()) { 196 props.load(in); 197 } catch (IOException e) { 198 Slog.e(TAG, "Cannot load config file " + mConfigFile, e); 199 } 200 props.put(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, 201 String.valueOf(timestamp)); 202 FileOutputStream out = null; 203 try { 204 out = mConfigFile.startWrite(); 205 props.store(out, "Statsd atom pull timestamps"); 206 mConfigFile.finishWrite(out); 207 } catch (IOException e) { 208 mConfigFile.failWrite(out); 209 Slog.e(TAG, "Cannot save config file " + mConfigFile, e); 210 } 211 } catch (IOException e) { 212 Slog.e(TAG, "Cannot lock battery usage stats directory", e); 213 } 214 } 215 216 /** 217 * Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET 218 * statsd atom pull. 219 */ getLastBatteryUsageStatsBeforeResetAtomPullTimestamp()220 public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() { 221 Properties props = new Properties(); 222 try (FileLock lock = lockSnapshotDirectory()) { 223 try (InputStream in = mConfigFile.openRead()) { 224 props.load(in); 225 } catch (IOException e) { 226 Slog.e(TAG, "Cannot load config file " + mConfigFile, e); 227 } 228 } catch (IOException e) { 229 Slog.e(TAG, "Cannot lock battery usage stats directory", e); 230 } 231 return Long.parseLong( 232 props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0")); 233 } 234 lockSnapshotDirectory()235 private FileLock lockSnapshotDirectory() throws IOException { 236 mLockFile.getParentFile().mkdirs(); 237 mLockFile.createNewFile(); 238 return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); 239 } 240 241 /** 242 * Creates a file name by formatting the timestamp as 19-digit zero-padded number. 243 * This ensures that sorted directory list follows the chronological order. 244 */ makeSnapshotFilename(long statsEndTimestamp)245 private File makeSnapshotFilename(long statsEndTimestamp) { 246 return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp) 247 + SNAPSHOT_FILE_EXTENSION); 248 } 249 writeXmlFileLocked(BatteryUsageStats stats, File file)250 private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException { 251 try (OutputStream out = new FileOutputStream(file)) { 252 TypedXmlSerializer serializer = Xml.newBinarySerializer(); 253 serializer.setOutput(out, StandardCharsets.UTF_8.name()); 254 serializer.startDocument(null, true); 255 stats.writeXml(serializer); 256 serializer.endDocument(); 257 } 258 } 259 readXmlFileLocked(File file)260 private BatteryUsageStats readXmlFileLocked(File file) 261 throws IOException, XmlPullParserException { 262 try (InputStream in = new FileInputStream(file)) { 263 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 264 parser.setInput(in, StandardCharsets.UTF_8.name()); 265 return BatteryUsageStats.createFromXml(parser); 266 } 267 } 268 removeOldSnapshotsLocked()269 private void removeOldSnapshotsLocked() { 270 // Read the directory list into a _sorted_ map. The alphanumeric ordering 271 // corresponds to the historical order of snapshots because the file names 272 // are timestamps zero-padded to the same length. 273 long totalSize = 0; 274 TreeMap<File, Long> mFileSizes = new TreeMap<>(); 275 for (File file : mStoreDir.listFiles()) { 276 final long fileSize = file.length(); 277 totalSize += fileSize; 278 if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) { 279 mFileSizes.put(file, fileSize); 280 } 281 } 282 283 while (totalSize > mMaxStorageBytes) { 284 final Map.Entry<File, Long> entry = mFileSizes.firstEntry(); 285 if (entry == null) { 286 break; 287 } 288 289 File file = entry.getKey(); 290 if (!file.delete()) { 291 Slog.e(TAG, "Cannot delete battery usage stats " + file); 292 } 293 totalSize -= entry.getValue(); 294 mFileSizes.remove(file); 295 } 296 } 297 } 298