• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.internal.os;
18 
19 import android.os.BatteryStats;
20 import android.os.Parcel;
21 import android.os.StatFs;
22 import android.os.SystemClock;
23 import android.util.ArraySet;
24 import android.util.AtomicFile;
25 import android.util.Slog;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.util.ParseUtils;
29 
30 import java.io.File;
31 import java.io.FilenameFilter;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.List;
35 import java.util.Set;
36 
37 /**
38  * BatteryStatsHistory encapsulates battery history files.
39  * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
40  * {@link #mActiveFile}.
41  * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
42  * current mActiveFile is closed and a new mActiveFile is open.
43  * History files are under directory /data/system/battery-history/.
44  * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
45  * grows sequentially.
46  * The mActiveFile is always the highest numbered history file.
47  * The lowest number file is always the oldest file.
48  * The highest number file is always the newest file.
49  * The file number grows sequentially and we never skip number.
50  * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
51  * the lowest numbered file is deleted and a new file is open.
52  *
53  * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
54  * locks on BatteryStatsImpl object.
55  */
56 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
57 public class BatteryStatsHistory {
58     private static final boolean DEBUG = false;
59     private static final String TAG = "BatteryStatsHistory";
60     public static final String HISTORY_DIR = "battery-history";
61     public static final String FILE_SUFFIX = ".bin";
62     private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
63 
64     private final BatteryStatsImpl mStats;
65     private final Parcel mHistoryBuffer;
66     private final File mHistoryDir;
67     /**
68      * The active history file that the history buffer is backed up into.
69      */
70     private AtomicFile mActiveFile;
71     /**
72      * A list of history files with incremental indexes.
73      */
74     private final List<Integer> mFileNumbers = new ArrayList<>();
75 
76     /**
77      * A list of small history parcels, used when BatteryStatsImpl object is created from
78      * deserialization of a parcel, such as Settings app or checkin file.
79      */
80     private List<Parcel> mHistoryParcels = null;
81 
82     /**
83      * When iterating history files, the current file index.
84      */
85     private int mCurrentFileIndex;
86     /**
87      * When iterating history files, the current file parcel.
88      */
89     private Parcel mCurrentParcel;
90     /**
91      * When iterating history file, the current parcel's Parcel.dataSize().
92      */
93     private int mCurrentParcelEnd;
94     /**
95      * When iterating history files, the current record count.
96      */
97     private int mRecordCount = 0;
98     /**
99      * Used when BatteryStatsImpl object is created from deserialization of a parcel,
100      * such as Settings app or checkin file, to iterate over history parcels.
101      */
102     private int mParcelIndex = 0;
103 
104     /**
105      * Constructor
106      * @param stats BatteryStatsImpl object.
107      * @param systemDir typically /data/system
108      * @param historyBuffer The in-memory history buffer.
109      */
BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer)110     public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) {
111         mStats = stats;
112         mHistoryBuffer = historyBuffer;
113         mHistoryDir = new File(systemDir, HISTORY_DIR);
114         mHistoryDir.mkdirs();
115         if (!mHistoryDir.exists()) {
116             Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
117         }
118 
119         final Set<Integer> dedup = new ArraySet<>();
120         // scan directory, fill mFileNumbers and mActiveFile.
121         mHistoryDir.listFiles(new FilenameFilter() {
122             @Override
123             public boolean accept(File dir, String name) {
124                 final int b = name.lastIndexOf(FILE_SUFFIX);
125                 if (b <= 0) {
126                     return false;
127                 }
128                 final Integer c =
129                         ParseUtils.parseInt(name.substring(0, b), -1);
130                 if (c != -1) {
131                     dedup.add(c);
132                     return true;
133                 } else {
134                     return false;
135                 }
136             }
137         });
138         if (!dedup.isEmpty()) {
139             mFileNumbers.addAll(dedup);
140             Collections.sort(mFileNumbers);
141             setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1));
142         } else {
143             // No file found, default to have file 0.
144             mFileNumbers.add(0);
145             setActiveFile(0);
146         }
147     }
148 
149     /**
150      * Used when BatteryStatsImpl object is created from deserialization of a parcel,
151      * such as Settings app or checkin file.
152      * @param stats BatteryStatsImpl object.
153      * @param historyBuffer the history buffer inside BatteryStatsImpl
154      */
BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer)155     public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) {
156         mStats = stats;
157         mHistoryDir = null;
158         mHistoryBuffer = historyBuffer;
159     }
160     /**
161      * Set the active file that mHistoryBuffer is backed up into.
162      *
163      * @param fileNumber the history file that mHistoryBuffer is backed up into.
164      */
setActiveFile(int fileNumber)165     private void setActiveFile(int fileNumber) {
166         mActiveFile = getFile(fileNumber);
167         if (DEBUG) {
168             Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath());
169         }
170     }
171 
172     /**
173      * Create history AtomicFile from file number.
174      * @param num file number.
175      * @return AtomicFile object.
176      */
getFile(int num)177     private AtomicFile getFile(int num) {
178         return new AtomicFile(
179                 new File(mHistoryDir,  num + FILE_SUFFIX));
180     }
181 
182     /**
183      * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
184      * create next history file.
185      */
startNextFile()186     public void startNextFile() {
187         if (mFileNumbers.isEmpty()) {
188             Slog.wtf(TAG, "mFileNumbers should never be empty");
189             return;
190         }
191         // The last number in mFileNumbers is the highest number. The next file number is highest
192         // number plus one.
193         final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
194         mFileNumbers.add(next);
195         setActiveFile(next);
196 
197         // if free disk space is less than 100MB, delete oldest history file.
198         if (!hasFreeDiskSpace()) {
199             int oldest = mFileNumbers.remove(0);
200             getFile(oldest).delete();
201         }
202 
203         // if there are more history files than allowed, delete oldest history files.
204         // MAX_HISTORY_FILES can be updated by GService config at run time.
205         while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
206             int oldest = mFileNumbers.get(0);
207             getFile(oldest).delete();
208             mFileNumbers.remove(0);
209         }
210     }
211 
212     /**
213      * Delete all existing history files. Active history file start from number 0 again.
214      */
resetAllFiles()215     public void resetAllFiles() {
216         for (Integer i : mFileNumbers) {
217             getFile(i).delete();
218         }
219         mFileNumbers.clear();
220         mFileNumbers.add(0);
221         setActiveFile(0);
222     }
223 
224     /**
225      * Start iterating history files and history buffer.
226      * @return always return true.
227      */
startIteratingHistory()228     public boolean startIteratingHistory() {
229         mRecordCount = 0;
230         mCurrentFileIndex = 0;
231         mCurrentParcel = null;
232         mCurrentParcelEnd = 0;
233         mParcelIndex = 0;
234         return true;
235     }
236 
237     /**
238      * Finish iterating history files and history buffer.
239      */
finishIteratingHistory()240     public void finishIteratingHistory() {
241         // setDataPosition so mHistoryBuffer Parcel can be written.
242         mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
243         if (DEBUG) {
244             Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
245         }
246     }
247 
248     /**
249      * When iterating history files and history buffer, always start from the lowest numbered
250      * history file, when reached the mActiveFile (highest numbered history file), do not read from
251      * mActiveFile, read from history buffer instead because the buffer has more updated data.
252      * @param out a history item.
253      * @return The parcel that has next record. null if finished all history files and history
254      *         buffer
255      */
getNextParcel(BatteryStats.HistoryItem out)256     public Parcel getNextParcel(BatteryStats.HistoryItem out) {
257         if (mRecordCount == 0) {
258             // reset out if it is the first record.
259             out.clear();
260         }
261         ++mRecordCount;
262 
263         // First iterate through all records in current parcel.
264         if (mCurrentParcel != null)
265         {
266             if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
267                 // There are more records in current parcel.
268                 return mCurrentParcel;
269             } else if (mHistoryBuffer == mCurrentParcel) {
270                 // finished iterate through all history files and history buffer.
271                 return null;
272             } else if (mHistoryParcels == null
273                     || !mHistoryParcels.contains(mCurrentParcel)) {
274                 // current parcel is from history file.
275                 mCurrentParcel.recycle();
276             }
277         }
278 
279         // Try next available history file.
280         // skip the last file because its data is in history buffer.
281         while (mCurrentFileIndex < mFileNumbers.size() - 1) {
282             mCurrentParcel = null;
283             mCurrentParcelEnd = 0;
284             final Parcel p = Parcel.obtain();
285             AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
286             if (readFileToParcel(p, file)) {
287                 int bufSize = p.readInt();
288                 int curPos = p.dataPosition();
289                 mCurrentParcelEnd = curPos + bufSize;
290                 mCurrentParcel = p;
291                 if (curPos < mCurrentParcelEnd) {
292                     return mCurrentParcel;
293                 }
294             } else {
295                 p.recycle();
296             }
297         }
298 
299         // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
300         // of a parcel, such as Settings app or checkin file.
301         if (mHistoryParcels != null) {
302             while (mParcelIndex < mHistoryParcels.size()) {
303                 final Parcel p = mHistoryParcels.get(mParcelIndex++);
304                 if (!skipHead(p)) {
305                     continue;
306                 }
307                 final int bufSize = p.readInt();
308                 final int curPos = p.dataPosition();
309                 mCurrentParcelEnd = curPos + bufSize;
310                 mCurrentParcel = p;
311                 if (curPos < mCurrentParcelEnd) {
312                     return mCurrentParcel;
313                 }
314             }
315         }
316 
317         // finished iterator through history files (except the last one), now history buffer.
318         if (mHistoryBuffer.dataSize() <= 0) {
319             // buffer is empty.
320             return null;
321         }
322         mHistoryBuffer.setDataPosition(0);
323         mCurrentParcel = mHistoryBuffer;
324         mCurrentParcelEnd = mCurrentParcel.dataSize();
325         return mCurrentParcel;
326     }
327 
328     /**
329      * Read history file into a parcel.
330      * @param out the Parcel read into.
331      * @param file the File to read from.
332      * @return true if success, false otherwise.
333      */
readFileToParcel(Parcel out, AtomicFile file)334     public boolean readFileToParcel(Parcel out, AtomicFile file) {
335         byte[] raw = null;
336         try {
337             final long start = SystemClock.uptimeMillis();
338             raw = file.readFully();
339             if (DEBUG) {
340                 Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
341                         + " duration ms:" + (SystemClock.uptimeMillis() - start));
342             }
343         } catch(Exception e) {
344             Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
345             return false;
346         }
347         out.unmarshall(raw, 0, raw.length);
348         out.setDataPosition(0);
349         return skipHead(out);
350     }
351 
352     /**
353      * Skip the header part of history parcel.
354      * @param p history parcel to skip head.
355      * @return true if version match, false if not.
356      */
skipHead(Parcel p)357     private boolean skipHead(Parcel p) {
358         p.setDataPosition(0);
359         final int version = p.readInt();
360         if (version != mStats.VERSION) {
361             return false;
362         }
363         // skip historyBaseTime field.
364         p.readLong();
365         return true;
366     }
367 
368     /**
369      * Read all history files and serialize into a big Parcel. This is to send history files to
370      * Settings app since Settings app can not access /data/system directory.
371      * Checkin file also call this method.
372      * @param out the output parcel
373      */
writeToParcel(Parcel out)374     public void writeToParcel(Parcel out) {
375         final long start = SystemClock.uptimeMillis();
376         out.writeInt(mFileNumbers.size() - 1);
377         for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
378             AtomicFile file = getFile(mFileNumbers.get(i));
379             byte[] raw = new byte[0];
380             try {
381                 raw = file.readFully();
382             } catch(Exception e) {
383                 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
384             }
385             out.writeByteArray(raw);
386         }
387         if (DEBUG) {
388             Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
389         }
390     }
391 
392     /**
393      * This is for Settings app, when Settings app receives big history parcel, it call
394      * this method to parse it into list of parcels.
395      * Checkin file also call this method.
396      * @param in the input parcel.
397      */
readFromParcel(Parcel in)398     public void readFromParcel(Parcel in) {
399         final long start = SystemClock.uptimeMillis();
400         mHistoryParcels = new ArrayList<>();
401         final int count = in.readInt();
402         for(int i = 0; i < count; i++) {
403             byte[] temp = in.createByteArray();
404             if (temp.length == 0) {
405                 continue;
406             }
407             Parcel p = Parcel.obtain();
408             p.unmarshall(temp, 0, temp.length);
409             p.setDataPosition(0);
410             mHistoryParcels.add(p);
411         }
412         if (DEBUG) {
413             Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
414         }
415     }
416 
417     /**
418      * @return true if there is more than 100MB free disk space left.
419      */
hasFreeDiskSpace()420     private boolean hasFreeDiskSpace() {
421         final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
422         return stats.getAvailableBytes() > MIN_FREE_SPACE;
423     }
424 
getFilesNumbers()425     public List<Integer> getFilesNumbers() {
426         return mFileNumbers;
427     }
428 
getActiveFile()429     public AtomicFile getActiveFile() {
430         return mActiveFile;
431     }
432 
433     /**
434      * @return the total size of all history files and history buffer.
435      */
getHistoryUsedSize()436     public int getHistoryUsedSize() {
437         int ret = 0;
438         for(int i = 0; i < mFileNumbers.size() - 1; i++) {
439             ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
440         }
441         ret += mHistoryBuffer.dataSize();
442         if (mHistoryParcels != null) {
443             for(int i = 0; i < mHistoryParcels.size(); i++) {
444                 ret += mHistoryParcels.get(i).dataSize();
445             }
446         }
447         return ret;
448     }
449 }
450