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