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.FileChannel; 40 import java.nio.channels.FileLock; 41 import java.nio.charset.StandardCharsets; 42 import java.nio.file.StandardOpenOption; 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.Map; 48 import java.util.TreeMap; 49 import java.util.concurrent.locks.ReentrantLock; 50 51 /** 52 * A storage mechanism for aggregated power/battery stats. 53 */ 54 public class PowerStatsStore { 55 private static final String TAG = "PowerStatsStore"; 56 57 private static final String POWER_STATS_DIR = "power-stats"; 58 private static final String POWER_STATS_SPAN_FILE_EXTENSION = ".pss"; 59 private static final String DIR_LOCK_FILENAME = ".lock"; 60 private static final long MAX_POWER_STATS_SPAN_STORAGE_BYTES = 100 * 1024; 61 62 private final File mSystemDir; 63 private final File mStoreDir; 64 private final File mLockFile; 65 private final ReentrantLock mFileLock = new ReentrantLock(); 66 private FileLock mJvmLock; 67 private final long mMaxStorageBytes; 68 private final Handler mHandler; 69 private final PowerStatsSpan.SectionReader mSectionReader; 70 private volatile List<PowerStatsSpan.Metadata> mTableOfContents; 71 PowerStatsStore(@onNull File systemDir, Handler handler, AggregatedPowerStatsConfig aggregatedPowerStatsConfig)72 public PowerStatsStore(@NonNull File systemDir, Handler handler, 73 AggregatedPowerStatsConfig aggregatedPowerStatsConfig) { 74 this(systemDir, MAX_POWER_STATS_SPAN_STORAGE_BYTES, handler, 75 new DefaultSectionReader(aggregatedPowerStatsConfig)); 76 } 77 78 @VisibleForTesting PowerStatsStore(@onNull File systemDir, long maxStorageBytes, Handler handler, @NonNull PowerStatsSpan.SectionReader sectionReader)79 public PowerStatsStore(@NonNull File systemDir, long maxStorageBytes, Handler handler, 80 @NonNull PowerStatsSpan.SectionReader sectionReader) { 81 mSystemDir = systemDir; 82 mStoreDir = new File(systemDir, POWER_STATS_DIR); 83 mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME); 84 mHandler = handler; 85 mMaxStorageBytes = maxStorageBytes; 86 mSectionReader = sectionReader; 87 mHandler.post(this::maybeClearLegacyStore); 88 } 89 90 /** 91 * Returns the metadata for all {@link PowerStatsSpan}'s contained in the store. 92 */ 93 @NonNull getTableOfContents()94 public List<PowerStatsSpan.Metadata> getTableOfContents() { 95 List<PowerStatsSpan.Metadata> toc = mTableOfContents; 96 if (toc != null) { 97 return toc; 98 } 99 100 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 101 lockStoreDirectory(); 102 try { 103 toc = new ArrayList<>(); 104 for (File file : mStoreDir.listFiles()) { 105 String fileName = file.getName(); 106 if (!fileName.endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) { 107 continue; 108 } 109 try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { 110 parser.setInput(inputStream, StandardCharsets.UTF_8.name()); 111 PowerStatsSpan.Metadata metadata = PowerStatsSpan.Metadata.read(parser); 112 if (metadata != null) { 113 toc.add(metadata); 114 } else { 115 Slog.e(TAG, "Removing incompatible PowerStatsSpan file: " + fileName); 116 file.delete(); 117 } 118 } catch (IOException | XmlPullParserException e) { 119 Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + fileName); 120 } 121 } 122 toc.sort(PowerStatsSpan.Metadata.COMPARATOR); 123 mTableOfContents = Collections.unmodifiableList(toc); 124 } finally { 125 unlockStoreDirectory(); 126 } 127 128 return toc; 129 } 130 131 /** 132 * Saves the specified span in the store. 133 */ storePowerStatsSpan(PowerStatsSpan span)134 public void storePowerStatsSpan(PowerStatsSpan span) { 135 maybeClearLegacyStore(); 136 lockStoreDirectory(); 137 try { 138 if (!mStoreDir.exists()) { 139 if (!mStoreDir.mkdirs()) { 140 Slog.e(TAG, "Could not create a directory for power stats store"); 141 return; 142 } 143 } 144 145 AtomicFile file = new AtomicFile(makePowerStatsSpanFilename(span.getId())); 146 file.write(out-> { 147 try { 148 span.writeXml(out, Xml.newBinarySerializer()); 149 } catch (Exception e) { 150 // AtomicFile will log the exception and delete the file. 151 throw new RuntimeException(e); 152 } 153 }); 154 mTableOfContents = null; 155 removeOldSpansLocked(); 156 } finally { 157 unlockStoreDirectory(); 158 } 159 } 160 161 /** 162 * Loads the PowerStatsSpan identified by its ID. Only loads the sections with 163 * the specified types. Loads all sections if no sectionTypes is empty. 164 */ 165 @Nullable loadPowerStatsSpan(long id, String... sectionTypes)166 public PowerStatsSpan loadPowerStatsSpan(long id, String... sectionTypes) { 167 TypedXmlPullParser parser = Xml.newBinaryPullParser(); 168 lockStoreDirectory(); 169 try { 170 File file = makePowerStatsSpanFilename(id); 171 try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { 172 return PowerStatsSpan.read(inputStream, parser, mSectionReader, sectionTypes); 173 } catch (IOException | XmlPullParserException e) { 174 Slog.wtf(TAG, "Cannot read PowerStatsSpan file: " + file, e); 175 } 176 } finally { 177 unlockStoreDirectory(); 178 } 179 return null; 180 } 181 storeAggregatedPowerStats(AggregatedPowerStats stats)182 void storeAggregatedPowerStats(AggregatedPowerStats stats) { 183 PowerStatsSpan span = createPowerStatsSpan(stats); 184 if (span == null) { 185 return; 186 } 187 storePowerStatsSpan(span); 188 } 189 createPowerStatsSpan(AggregatedPowerStats stats)190 static PowerStatsSpan createPowerStatsSpan(AggregatedPowerStats stats) { 191 List<AggregatedPowerStats.ClockUpdate> clockUpdates = stats.getClockUpdates(); 192 if (clockUpdates.isEmpty()) { 193 Slog.w(TAG, "No clock updates in aggregated power stats " + stats); 194 return null; 195 } 196 197 long monotonicTime = clockUpdates.get(0).monotonicTime; 198 long durationSum = 0; 199 PowerStatsSpan span = new PowerStatsSpan(monotonicTime); 200 for (int i = 0; i < clockUpdates.size(); i++) { 201 AggregatedPowerStats.ClockUpdate clockUpdate = clockUpdates.get(i); 202 long duration; 203 if (i == clockUpdates.size() - 1) { 204 duration = stats.getDuration() - durationSum; 205 } else { 206 duration = clockUpdate.monotonicTime - monotonicTime; 207 } 208 span.addTimeFrame(clockUpdate.monotonicTime, clockUpdate.currentTime, duration); 209 monotonicTime = clockUpdate.monotonicTime; 210 durationSum += duration; 211 } 212 213 span.addSection(new AggregatedPowerStatsSection(stats)); 214 return span; 215 } 216 217 /** 218 * Stores a {@link PowerStatsSpan} containing a single section for the supplied 219 * battery usage stats. 220 */ storeBatteryUsageStats(long monotonicStartTime, BatteryUsageStats batteryUsageStats)221 public void storeBatteryUsageStats(long monotonicStartTime, 222 BatteryUsageStats batteryUsageStats) { 223 PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime); 224 span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(), 225 batteryUsageStats.getStatsDuration()); 226 span.addSection(new BatteryUsageStatsSection(batteryUsageStats)); 227 storePowerStatsSpan(span); 228 } 229 230 /** 231 * Creates a file name by formatting the span ID as a 19-digit zero-padded number. 232 * This ensures that the lexicographically sorted directory follows the chronological order. 233 */ makePowerStatsSpanFilename(long id)234 private File makePowerStatsSpanFilename(long id) { 235 return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", id) 236 + POWER_STATS_SPAN_FILE_EXTENSION); 237 } 238 maybeClearLegacyStore()239 private void maybeClearLegacyStore() { 240 File legacyStoreDir = new File(mSystemDir, "battery-usage-stats"); 241 if (legacyStoreDir.exists()) { 242 FileUtils.deleteContentsAndDir(legacyStoreDir); 243 } 244 } 245 lockStoreDirectory()246 private void lockStoreDirectory() { 247 mFileLock.lock(); 248 249 // Lock the directory from access by other JVMs 250 try { 251 mLockFile.getParentFile().mkdirs(); 252 mLockFile.createNewFile(); 253 mJvmLock = FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock(); 254 } catch (IOException e) { 255 Slog.e(TAG, "Cannot lock snapshot directory", e); 256 } 257 } 258 unlockStoreDirectory()259 private void unlockStoreDirectory() { 260 try { 261 mJvmLock.close(); 262 } catch (IOException e) { 263 Slog.e(TAG, "Cannot unlock snapshot directory", e); 264 } finally { 265 mFileLock.unlock(); 266 } 267 } 268 removeOldSpansLocked()269 private void removeOldSpansLocked() { 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(POWER_STATS_SPAN_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 power stats span " + file); 292 } 293 totalSize -= entry.getValue(); 294 mFileSizes.remove(file); 295 mTableOfContents = null; 296 } 297 } 298 299 /** 300 * Deletes all contents from the store. 301 */ reset()302 public void reset() { 303 lockStoreDirectory(); 304 try { 305 for (File file : mStoreDir.listFiles()) { 306 if (file.getName().endsWith(POWER_STATS_SPAN_FILE_EXTENSION)) { 307 if (!file.delete()) { 308 Slog.e(TAG, "Cannot delete power stats span " + file); 309 } 310 } 311 } 312 mTableOfContents = List.of(); 313 } finally { 314 unlockStoreDirectory(); 315 } 316 } 317 318 /** 319 * Prints the summary of contents of the store: only metadata, but not the actual stored 320 * objects. 321 */ dumpTableOfContents(IndentingPrintWriter ipw)322 public void dumpTableOfContents(IndentingPrintWriter ipw) { 323 ipw.println("Power stats store TOC"); 324 ipw.increaseIndent(); 325 List<PowerStatsSpan.Metadata> contents = getTableOfContents(); 326 for (PowerStatsSpan.Metadata metadata : contents) { 327 metadata.dump(ipw); 328 } 329 ipw.decreaseIndent(); 330 } 331 332 /** 333 * Prints the contents of the store. 334 */ dump(IndentingPrintWriter ipw)335 public void dump(IndentingPrintWriter ipw) { 336 ipw.println("Power stats store"); 337 ipw.increaseIndent(); 338 List<PowerStatsSpan.Metadata> contents = getTableOfContents(); 339 for (PowerStatsSpan.Metadata metadata : contents) { 340 PowerStatsSpan span = loadPowerStatsSpan(metadata.getId()); 341 if (span != null) { 342 span.dump(ipw); 343 } 344 } 345 ipw.decreaseIndent(); 346 } 347 348 private static class DefaultSectionReader implements PowerStatsSpan.SectionReader { 349 private final AggregatedPowerStatsConfig mAggregatedPowerStatsConfig; 350 DefaultSectionReader(AggregatedPowerStatsConfig aggregatedPowerStatsConfig)351 DefaultSectionReader(AggregatedPowerStatsConfig aggregatedPowerStatsConfig) { 352 mAggregatedPowerStatsConfig = aggregatedPowerStatsConfig; 353 } 354 355 @Override read(String sectionType, TypedXmlPullParser parser)356 public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser) 357 throws IOException, XmlPullParserException { 358 switch (sectionType) { 359 case AggregatedPowerStatsSection.TYPE: 360 return new AggregatedPowerStatsSection( 361 AggregatedPowerStats.createFromXml(parser, 362 mAggregatedPowerStatsConfig)); 363 case BatteryUsageStatsSection.TYPE: 364 return new BatteryUsageStatsSection( 365 BatteryUsageStats.createFromXml(parser)); 366 default: 367 return null; 368 } 369 } 370 } 371 } 372