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