• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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