1 /* 2 * Copyright (C) 2023 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.NonNull; 20 import android.annotation.Nullable; 21 import android.os.BatteryUsageStats; 22 import android.os.FileUtils; 23 import android.os.Handler; 24 import android.util.AtomicFile; 25 import android.util.IndentingPrintWriter; 26 import android.util.Slog; 27 import android.util.Xml; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.modules.utils.TypedXmlPullParser; 31 32 import org.xmlpull.v1.XmlPullParserException; 33 34 import java.io.BufferedInputStream; 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.nio.channels.Channel; 40 import java.nio.channels.FileChannel; 41 import java.nio.channels.FileLock; 42 import java.nio.charset.StandardCharsets; 43 import java.nio.file.StandardOpenOption; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Locale; 49 import java.util.Map; 50 import java.util.TreeMap; 51 import java.util.concurrent.locks.ReentrantLock; 52 53 /** 54 * A storage mechanism for aggregated power/battery stats. 55 */ 56 public class PowerStatsStore { 57 private static final String TAG = "PowerStatsStore"; 58 59 private static final String POWER_STATS_DIR = "power-stats"; 60 private static final String POWER_STATS_SPAN_FILE_EXTENSION = ".pss"; 61 private static final String DIR_LOCK_FILENAME = ".lock"; 62 private static final long MAX_POWER_STATS_SPAN_STORAGE_BYTES = 100 * 1024; 63 64 private final File mSystemDir; 65 private final File mStoreDir; 66 private final File mLockFile; 67 private final ReentrantLock mFileLock = new ReentrantLock(); 68 private FileLock mJvmLock; 69 private final long mMaxStorageBytes; 70 private final Handler mHandler; 71 private final Map<String, PowerStatsSpan.SectionReader> mSectionReaders = new HashMap<>(); 72 private volatile List<PowerStatsSpan.Metadata> mTableOfContents; 73 PowerStatsStore(@onNull File systemDir, Handler handler)74 public PowerStatsStore(@NonNull File systemDir, Handler handler) { 75 this(systemDir, MAX_POWER_STATS_SPAN_STORAGE_BYTES, handler); 76 } 77 78 @VisibleForTesting PowerStatsStore(@onNull File systemDir, long maxStorageBytes, Handler handler)79 public PowerStatsStore(@NonNull File systemDir, long maxStorageBytes, Handler handler) { 80 mSystemDir = systemDir; 81 mStoreDir = new File(systemDir, POWER_STATS_DIR); 82 mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME); 83 mHandler = handler; 84 mMaxStorageBytes = maxStorageBytes; 85 mHandler.post(this::maybeClearLegacyStore); 86 } 87 88 /** 89 * Registers a Reader for a section type, which is determined by `sectionReader.getType()` 90 */ addSectionReader(PowerStatsSpan.SectionReader sectionReader)91 public void addSectionReader(PowerStatsSpan.SectionReader sectionReader) { 92 mSectionReaders.put(sectionReader.getType(), sectionReader); 93 } 94 95 /** 96 * Returns the metadata for all {@link PowerStatsSpan}'s contained in the store. 97 */ 98 @NonNull getTableOfContents()99 public List<PowerStatsSpan.Metadata> getTableOfContents() { 100 List<PowerStatsSpan.Metadata> toc = mTableOfContents; 101 if (toc != null) { 102 return toc; 103 } 104 105 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 106 lockStoreDirectory(); 107 try { 108 toc = new ArrayList<>(); 109 for (File file : mStoreDir.listFiles()) { 110 String fileName = file.getName(); 111 if (!fileName.endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) { 112 continue; 113 } 114 try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { 115 parser.setInput(inputStream, StandardCharsets.UTF_8.name()); 116 PowerStatsSpan.Metadata metadata = PowerStatsSpan.Metadata.read(parser); 117 if (metadata != null) { 118 toc.add(metadata); 119 } else { 120 Slog.e(TAG, "Removing incompatible PowerStatsSpan file: " + fileName); 121 file.delete(); 122 } 123 } catch (IOException | XmlPullParserException e) { 124 Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + fileName); 125 } 126 } 127 toc.sort(PowerStatsSpan.Metadata.COMPARATOR); 128 mTableOfContents = Collections.unmodifiableList(toc); 129 } finally { 130 unlockStoreDirectory(); 131 } 132 133 return toc; 134 } 135 136 /** 137 * Schedules saving the specified span on the background thread. 138 */ storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete)139 public void storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete) { 140 mHandler.post(() -> { 141 try { 142 storePowerStatsSpan(span); 143 } finally { 144 onComplete.run(); 145 } 146 }); 147 } 148 149 /** 150 * Saves the specified span in the store. 151 */ storePowerStatsSpan(PowerStatsSpan span)152 public void storePowerStatsSpan(PowerStatsSpan span) { 153 lockStoreDirectory(); 154 try { 155 if (!mStoreDir.exists()) { 156 if (!mStoreDir.mkdirs()) { 157 Slog.e(TAG, "Could not create a directory for power stats store"); 158 return; 159 } 160 } 161 162 AtomicFile file = new AtomicFile(makePowerStatsSpanFilename(span.getId())); 163 file.write(out-> { 164 try { 165 span.writeXml(out, Xml.newBinarySerializer()); 166 } catch (Exception e) { 167 // AtomicFile will log the exception and delete the file. 168 throw new RuntimeException(e); 169 } 170 }); 171 mTableOfContents = null; 172 removeOldSpansLocked(); 173 } finally { 174 unlockStoreDirectory(); 175 } 176 } 177 178 /** 179 * Loads the PowerStatsSpan identified by its ID. Only loads the sections with 180 * the specified types. Loads all sections if no sectionTypes is empty. 181 */ 182 @Nullable loadPowerStatsSpan(long id, String... sectionTypes)183 public PowerStatsSpan loadPowerStatsSpan(long id, String... sectionTypes) { 184 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 185 lockStoreDirectory(); 186 try { 187 File file = makePowerStatsSpanFilename(id); 188 if (!file.exists()) { 189 return null; 190 } 191 try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { 192 return PowerStatsSpan.read(inputStream, parser, mSectionReaders, sectionTypes); 193 } catch (IOException | XmlPullParserException e) { 194 Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + file, e); 195 } 196 } finally { 197 unlockStoreDirectory(); 198 } 199 return null; 200 } 201 202 /** 203 * Stores a {@link PowerStatsSpan} containing a single section for the supplied 204 * battery usage stats. 205 */ storeBatteryUsageStatsAsync(long monotonicStartTime, BatteryUsageStats batteryUsageStats)206 public void storeBatteryUsageStatsAsync(long monotonicStartTime, 207 BatteryUsageStats batteryUsageStats) { 208 if (mHandler.getLooper().isCurrentThread()) { 209 storeBatteryUsageStats(monotonicStartTime, batteryUsageStats); 210 } else { 211 mHandler.post(() -> { 212 storeBatteryUsageStats(monotonicStartTime, batteryUsageStats); 213 }); 214 } 215 } 216 storeBatteryUsageStats(long monotonicStartTime, BatteryUsageStats batteryUsageStats)217 private void storeBatteryUsageStats(long monotonicStartTime, 218 BatteryUsageStats batteryUsageStats) { 219 try { 220 PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime); 221 span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(), 222 batteryUsageStats.getStatsDuration()); 223 span.addSection(new BatteryUsageStatsSection(batteryUsageStats)); 224 storePowerStatsSpan(span); 225 } finally { 226 try { 227 batteryUsageStats.close(); 228 } catch (IOException e) { 229 Slog.e(TAG, "Cannot close BatteryUsageStats", e); 230 } 231 } 232 } 233 234 /** 235 * Creates a file name by formatting the span ID as a 19-digit zero-padded number. 236 * This ensures that the lexicographically sorted directory follows the chronological order. 237 */ makePowerStatsSpanFilename(long id)238 private File makePowerStatsSpanFilename(long id) { 239 return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", id) 240 + POWER_STATS_SPAN_FILE_EXTENSION); 241 } 242 maybeClearLegacyStore()243 private void maybeClearLegacyStore() { 244 File legacyStoreDir = new File(mSystemDir, "battery-usage-stats"); 245 if (legacyStoreDir.exists()) { 246 FileUtils.deleteContentsAndDir(legacyStoreDir); 247 } 248 } 249 lockStoreDirectory()250 private void lockStoreDirectory() { 251 mFileLock.lock(); 252 253 // Lock the directory from access by other JVMs 254 try { 255 if (!mLockFile.exists()) { 256 mLockFile.getParentFile().mkdirs(); 257 mLockFile.createNewFile(); 258 } 259 mJvmLock = FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); 260 } catch (IOException e) { 261 Slog.e(TAG, "Cannot lock snapshot directory", e); 262 } 263 } 264 unlockStoreDirectory()265 private void unlockStoreDirectory() { 266 try { 267 Channel channel = mJvmLock.acquiredBy(); 268 mJvmLock.release(); 269 channel.close(); 270 } catch (IOException e) { 271 Slog.e(TAG, "Cannot unlock snapshot directory", e); 272 } finally { 273 mJvmLock = null; 274 mFileLock.unlock(); 275 } 276 } 277 removeOldSpansLocked()278 private void removeOldSpansLocked() { 279 // Read the directory list into a _sorted_ map. The alphanumeric ordering 280 // corresponds to the historical order of snapshots because the file names 281 // are timestamps zero-padded to the same length. 282 long totalSize = 0; 283 TreeMap<File, Long> mFileSizes = new TreeMap<>(); 284 for (File file : mStoreDir.listFiles()) { 285 final long fileSize = file.length(); 286 totalSize += fileSize; 287 if (file.getName().endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) { 288 mFileSizes.put(file, fileSize); 289 } 290 } 291 292 while (totalSize > mMaxStorageBytes) { 293 final Map.Entry<File, Long> entry = mFileSizes.firstEntry(); 294 if (entry == null) { 295 break; 296 } 297 298 File file = entry.getKey(); 299 if (!file.delete()) { 300 Slog.e(TAG, "Cannot delete power stats span " + file); 301 } 302 totalSize -= entry.getValue(); 303 mFileSizes.remove(file); 304 mTableOfContents = null; 305 } 306 } 307 308 /** 309 * Deletes all contents from the store. 310 */ reset()311 public void reset() { 312 lockStoreDirectory(); 313 try { 314 for (File file : mStoreDir.listFiles()) { 315 if (file.getName().endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) { 316 if (!file.delete()) { 317 Slog.e(TAG, "Cannot delete power stats span " + file); 318 } 319 } 320 } 321 mTableOfContents = List.of(); 322 } finally { 323 unlockStoreDirectory(); 324 } 325 } 326 327 /** 328 * Prints the summary of contents of the store: only metadata, but not the actual stored 329 * objects. 330 */ dumpTableOfContents(IndentingPrintWriter ipw)331 public void dumpTableOfContents(IndentingPrintWriter ipw) { 332 ipw.println("Power stats store TOC"); 333 ipw.increaseIndent(); 334 List<PowerStatsSpan.Metadata> contents = getTableOfContents(); 335 for (PowerStatsSpan.Metadata metadata : contents) { 336 metadata.dump(ipw); 337 } 338 ipw.decreaseIndent(); 339 } 340 341 /** 342 * Prints the contents of the store. 343 */ dump(IndentingPrintWriter ipw)344 public void dump(IndentingPrintWriter ipw) { 345 ipw.println("Power stats store"); 346 ipw.increaseIndent(); 347 List<PowerStatsSpan.Metadata> contents = getTableOfContents(); 348 for (PowerStatsSpan.Metadata metadata : contents) { 349 try (PowerStatsSpan span = loadPowerStatsSpan(metadata.getId())) { 350 if (span != null) { 351 span.dump(ipw); 352 } 353 } 354 } 355 ipw.decreaseIndent(); 356 } 357 } 358