1 /* 2 * Copyright (C) 2020 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.server.power.stats; 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.Log; 26 import android.util.LongArray; 27 import android.util.Slog; 28 import android.util.Xml; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.modules.utils.TypedXmlPullParser; 32 import com.android.modules.utils.TypedXmlSerializer; 33 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.io.OutputStream; 42 import java.nio.channels.FileChannel; 43 import java.nio.channels.FileLock; 44 import java.nio.charset.StandardCharsets; 45 import java.nio.file.StandardOpenOption; 46 import java.util.List; 47 import java.util.Locale; 48 import java.util.Map; 49 import java.util.Properties; 50 import java.util.TreeMap; 51 import java.util.concurrent.locks.ReentrantLock; 52 53 /** 54 * A storage mechanism for BatteryUsageStats snapshots. 55 */ 56 public class BatteryUsageStatsStore { 57 private static final String TAG = "BatteryUsageStatsStore"; 58 59 private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of( 60 new BatteryUsageStatsQuery.Builder() 61 .setMaxStatsAgeMs(0) 62 .includePowerModels() 63 .includeProcessStateData() 64 .build()); 65 private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats"; 66 private static final String SNAPSHOT_FILE_EXTENSION = ".bus"; 67 private static final String DIR_LOCK_FILENAME = ".lock"; 68 private static final String CONFIG_FILENAME = "config"; 69 private static final String BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY = 70 "BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP"; 71 private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024; 72 73 private final Context mContext; 74 private final BatteryStatsImpl mBatteryStats; 75 private boolean mSystemReady; 76 private final File mStoreDir; 77 private final File mLockFile; 78 private final ReentrantLock mFileLock = new ReentrantLock(); 79 private FileLock mJvmLock; 80 private final AtomicFile mConfigFile; 81 private final long mMaxStorageBytes; 82 private final Handler mHandler; 83 private final BatteryUsageStatsProvider mBatteryUsageStatsProvider; 84 BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir, Handler handler)85 public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir, 86 Handler handler) { 87 this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES); 88 } 89 90 @VisibleForTesting BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir, Handler handler, long maxStorageBytes)91 public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir, 92 Handler handler, long maxStorageBytes) { 93 mContext = context; 94 mBatteryStats = batteryStats; 95 mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR); 96 mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME); 97 mConfigFile = new AtomicFile(new File(mStoreDir, CONFIG_FILENAME)); 98 mHandler = handler; 99 mMaxStorageBytes = maxStorageBytes; 100 mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset); 101 mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats); 102 } 103 104 /** 105 * Notifies BatteryUsageStatsStore that the system server is ready. 106 */ onSystemReady()107 public void onSystemReady() { 108 mSystemReady = true; 109 } 110 prepareForBatteryStatsReset(int resetReason)111 private void prepareForBatteryStatsReset(int resetReason) { 112 if (resetReason == BatteryStatsImpl.RESET_REASON_CORRUPT_FILE || !mSystemReady) { 113 return; 114 } 115 116 final List<BatteryUsageStats> stats = 117 mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY); 118 if (stats.isEmpty()) { 119 Slog.wtf(TAG, "No battery usage stats generated"); 120 return; 121 } 122 123 mHandler.post(() -> storeBatteryUsageStats(stats.get(0))); 124 } 125 storeBatteryUsageStats(BatteryUsageStats stats)126 private void storeBatteryUsageStats(BatteryUsageStats stats) { 127 lockSnapshotDirectory(); 128 try { 129 if (!mStoreDir.exists()) { 130 if (!mStoreDir.mkdirs()) { 131 Slog.e(TAG, "Could not create a directory for battery usage stats snapshots"); 132 return; 133 } 134 } 135 File file = makeSnapshotFilename(stats.getStatsEndTimestamp()); 136 try { 137 writeXmlFileLocked(stats, file); 138 } catch (Exception e) { 139 Slog.e(TAG, "Cannot save battery usage stats", e); 140 } 141 142 removeOldSnapshotsLocked(); 143 } finally { 144 unlockSnapshotDirectory(); 145 } 146 } 147 148 /** 149 * Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds 150 * to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}. 151 */ listBatteryUsageStatsTimestamps()152 public long[] listBatteryUsageStatsTimestamps() { 153 LongArray timestamps = new LongArray(100); 154 lockSnapshotDirectory(); 155 try { 156 for (File file : mStoreDir.listFiles()) { 157 String fileName = file.getName(); 158 if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) { 159 try { 160 String fileNameWithoutExtension = fileName.substring(0, 161 fileName.length() - SNAPSHOT_FILE_EXTENSION.length()); 162 timestamps.add(Long.parseLong(fileNameWithoutExtension)); 163 } catch (NumberFormatException e) { 164 Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: " 165 + fileName); 166 } 167 } 168 } 169 } finally { 170 unlockSnapshotDirectory(); 171 } 172 return timestamps.toArray(); 173 } 174 175 /** 176 * Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot 177 * does not exist. 178 */ 179 @Nullable loadBatteryUsageStats(long timestamp)180 public BatteryUsageStats loadBatteryUsageStats(long timestamp) { 181 lockSnapshotDirectory(); 182 try { 183 File file = makeSnapshotFilename(timestamp); 184 try { 185 return readXmlFileLocked(file); 186 } catch (Exception e) { 187 Slog.e(TAG, "Cannot read battery usage stats", e); 188 } 189 } finally { 190 unlockSnapshotDirectory(); 191 } 192 return null; 193 } 194 195 /** 196 * Saves the supplied timestamp of the BATTERY_USAGE_STATS_BEFORE_RESET statsd atom pull 197 * in persistent file. 198 */ setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp)199 public void setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp) { 200 Properties props = new Properties(); 201 lockSnapshotDirectory(); 202 try { 203 try (InputStream in = mConfigFile.openRead()) { 204 props.load(in); 205 } catch (IOException e) { 206 Slog.e(TAG, "Cannot load config file " + mConfigFile, e); 207 } 208 props.put(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, 209 String.valueOf(timestamp)); 210 FileOutputStream out = null; 211 try { 212 out = mConfigFile.startWrite(); 213 props.store(out, "Statsd atom pull timestamps"); 214 mConfigFile.finishWrite(out); 215 } catch (IOException e) { 216 mConfigFile.failWrite(out); 217 Slog.e(TAG, "Cannot save config file " + mConfigFile, e); 218 } 219 } finally { 220 unlockSnapshotDirectory(); 221 } 222 } 223 224 /** 225 * Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET 226 * statsd atom pull. 227 */ getLastBatteryUsageStatsBeforeResetAtomPullTimestamp()228 public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() { 229 Properties props = new Properties(); 230 lockSnapshotDirectory(); 231 try { 232 try (InputStream in = mConfigFile.openRead()) { 233 props.load(in); 234 } catch (IOException e) { 235 Slog.e(TAG, "Cannot load config file " + mConfigFile, e); 236 } 237 } finally { 238 unlockSnapshotDirectory(); 239 } 240 return Long.parseLong( 241 props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0")); 242 } 243 lockSnapshotDirectory()244 private void lockSnapshotDirectory() { 245 mFileLock.lock(); 246 247 // Lock the directory from access by other JVMs 248 try { 249 mLockFile.getParentFile().mkdirs(); 250 mLockFile.createNewFile(); 251 mJvmLock = FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); 252 } catch (IOException e) { 253 Log.e(TAG, "Cannot lock snapshot directory", e); 254 } 255 } 256 unlockSnapshotDirectory()257 private void unlockSnapshotDirectory() { 258 try { 259 mJvmLock.close(); 260 } catch (IOException e) { 261 Log.e(TAG, "Cannot unlock snapshot directory", e); 262 } finally { 263 mFileLock.unlock(); 264 } 265 } 266 267 /** 268 * Creates a file name by formatting the timestamp as 19-digit zero-padded number. 269 * This ensures that sorted directory list follows the chronological order. 270 */ makeSnapshotFilename(long statsEndTimestamp)271 private File makeSnapshotFilename(long statsEndTimestamp) { 272 return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp) 273 + SNAPSHOT_FILE_EXTENSION); 274 } 275 writeXmlFileLocked(BatteryUsageStats stats, File file)276 private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException { 277 try (OutputStream out = new FileOutputStream(file)) { 278 TypedXmlSerializer serializer = Xml.newBinarySerializer(); 279 serializer.setOutput(out, StandardCharsets.UTF_8.name()); 280 serializer.startDocument(null, true); 281 stats.writeXml(serializer); 282 serializer.endDocument(); 283 } 284 } 285 readXmlFileLocked(File file)286 private BatteryUsageStats readXmlFileLocked(File file) 287 throws IOException, XmlPullParserException { 288 try (InputStream in = new FileInputStream(file)) { 289 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 290 parser.setInput(in, StandardCharsets.UTF_8.name()); 291 return BatteryUsageStats.createFromXml(parser); 292 } 293 } 294 removeOldSnapshotsLocked()295 private void removeOldSnapshotsLocked() { 296 // Read the directory list into a _sorted_ map. The alphanumeric ordering 297 // corresponds to the historical order of snapshots because the file names 298 // are timestamps zero-padded to the same length. 299 long totalSize = 0; 300 TreeMap<File, Long> mFileSizes = new TreeMap<>(); 301 for (File file : mStoreDir.listFiles()) { 302 final long fileSize = file.length(); 303 totalSize += fileSize; 304 if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) { 305 mFileSizes.put(file, fileSize); 306 } 307 } 308 309 while (totalSize > mMaxStorageBytes) { 310 final Map.Entry<File, Long> entry = mFileSizes.firstEntry(); 311 if (entry == null) { 312 break; 313 } 314 315 File file = entry.getKey(); 316 if (!file.delete()) { 317 Slog.e(TAG, "Cannot delete battery usage stats " + file); 318 } 319 totalSize -= entry.getValue(); 320 mFileSizes.remove(file); 321 } 322 } 323 removeAllSnapshots()324 public void removeAllSnapshots() { 325 lockSnapshotDirectory(); 326 try { 327 for (File file : mStoreDir.listFiles()) { 328 if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) { 329 if (!file.delete()) { 330 Slog.e(TAG, "Cannot delete battery usage stats " + file); 331 } 332 } 333 } 334 } finally { 335 unlockSnapshotDirectory(); 336 } 337 } 338 } 339