• 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.annotation.NonNull;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.os.BatteryStats;
23 import android.os.BatteryStats.BitDescription;
24 import android.os.BatteryStats.HistoryItem;
25 import android.os.BatteryStats.HistoryTag;
26 import android.os.Build;
27 import android.os.Parcel;
28 import android.os.Process;
29 import android.os.StatFs;
30 import android.os.SystemClock;
31 import android.os.SystemProperties;
32 import android.os.Trace;
33 import android.util.ArraySet;
34 import android.util.AtomicFile;
35 import android.util.Slog;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.internal.util.ParseUtils;
39 
40 import java.io.File;
41 import java.io.FilenameFilter;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.Set;
46 
47 /**
48  * BatteryStatsHistory encapsulates battery history files.
49  * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
50  * {@link #mActiveFile}.
51  * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
52  * current mActiveFile is closed and a new mActiveFile is open.
53  * History files are under directory /data/system/battery-history/.
54  * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
55  * grows sequentially.
56  * The mActiveFile is always the highest numbered history file.
57  * The lowest number file is always the oldest file.
58  * The highest number file is always the newest file.
59  * The file number grows sequentially and we never skip number.
60  * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
61  * the lowest numbered file is deleted and a new file is open.
62  *
63  * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
64  * locks on BatteryStatsImpl object.
65  */
66 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
67 public class BatteryStatsHistory {
68     private static final boolean DEBUG = false;
69     private static final String TAG = "BatteryStatsHistory";
70     public static final String HISTORY_DIR = "battery-history";
71     public static final String FILE_SUFFIX = ".bin";
72     private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
73 
74     @Nullable
75     private final BatteryStatsImpl mStats;
76     private final Parcel mHistoryBuffer;
77     private final File mHistoryDir;
78     /**
79      * The active history file that the history buffer is backed up into.
80      */
81     private AtomicFile mActiveFile;
82     /**
83      * A list of history files with incremental indexes.
84      */
85     private final List<Integer> mFileNumbers = new ArrayList<>();
86 
87     /**
88      * A list of small history parcels, used when BatteryStatsImpl object is created from
89      * deserialization of a parcel, such as Settings app or checkin file.
90      */
91     private List<Parcel> mHistoryParcels = null;
92 
93     /**
94      * When iterating history files, the current file index.
95      */
96     private int mCurrentFileIndex;
97     /**
98      * When iterating history files, the current file parcel.
99      */
100     private Parcel mCurrentParcel;
101     /**
102      * When iterating history file, the current parcel's Parcel.dataSize().
103      */
104     private int mCurrentParcelEnd;
105     /**
106      * When iterating history files, the current record count.
107      */
108     private int mRecordCount = 0;
109     /**
110      * Used when BatteryStatsImpl object is created from deserialization of a parcel,
111      * such as Settings app or checkin file, to iterate over history parcels.
112      */
113     private int mParcelIndex = 0;
114 
115     /**
116      * A delegate for android.os.Trace to allow testing static calls. Due to
117      * limitations in Android Tracing (b/153319140), the delegate also records
118      * counter values in system properties which allows reading the value at the
119      * start of a tracing session. This overhead is limited to userdebug builds.
120      * On user builds, tracing still occurs but the counter value will be missing
121      * until the first change occurs.
122      */
123     @VisibleForTesting
124     public static class TraceDelegate {
125         // Note: certain tests currently run as platform_app which is not allowed
126         // to set debug system properties. To ensure that system properties are set
127         // only when allowed, we check the current UID.
128         private final boolean mShouldSetProperty =
129                 Build.IS_USERDEBUG && (Process.myUid() == Process.SYSTEM_UID);
130 
131         /**
132          * Returns true if trace counters should be recorded.
133          */
tracingEnabled()134         public boolean tracingEnabled() {
135             return Trace.isTagEnabled(Trace.TRACE_TAG_POWER) || mShouldSetProperty;
136         }
137 
138         /**
139          * Records the counter value with the given name.
140          */
traceCounter(@onNull String name, int value)141         public void traceCounter(@NonNull String name, int value) {
142             Trace.traceCounter(Trace.TRACE_TAG_POWER, name, value);
143             if (mShouldSetProperty) {
144                 SystemProperties.set("debug.tracing." + name, Integer.toString(value));
145             }
146         }
147 
148         /**
149          * Records an instant event (one with no duration).
150          */
traceInstantEvent(@onNull String track, @NonNull String name)151         public void traceInstantEvent(@NonNull String track, @NonNull String name) {
152             Trace.instantForTrack(Trace.TRACE_TAG_POWER, track, name);
153         }
154     }
155 
156     private TraceDelegate mTracer = new TraceDelegate();
157 
158     /**
159      * Constructor
160      * @param stats BatteryStatsImpl object.
161      * @param systemDir typically /data/system
162      * @param historyBuffer The in-memory history buffer.
163      */
BatteryStatsHistory(@onNull BatteryStatsImpl stats, File systemDir, Parcel historyBuffer)164     public BatteryStatsHistory(@NonNull BatteryStatsImpl stats, File systemDir,
165             Parcel historyBuffer) {
166         mStats = stats;
167         mHistoryBuffer = historyBuffer;
168         mHistoryDir = new File(systemDir, HISTORY_DIR);
169         mHistoryDir.mkdirs();
170         if (!mHistoryDir.exists()) {
171             Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
172         }
173 
174         final Set<Integer> dedup = new ArraySet<>();
175         // scan directory, fill mFileNumbers and mActiveFile.
176         mHistoryDir.listFiles(new FilenameFilter() {
177             @Override
178             public boolean accept(File dir, String name) {
179                 final int b = name.lastIndexOf(FILE_SUFFIX);
180                 if (b <= 0) {
181                     return false;
182                 }
183                 final Integer c =
184                         ParseUtils.parseInt(name.substring(0, b), -1);
185                 if (c != -1) {
186                     dedup.add(c);
187                     return true;
188                 } else {
189                     return false;
190                 }
191             }
192         });
193         if (!dedup.isEmpty()) {
194             mFileNumbers.addAll(dedup);
195             Collections.sort(mFileNumbers);
196             setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1));
197         } else {
198             // No file found, default to have file 0.
199             mFileNumbers.add(0);
200             setActiveFile(0);
201         }
202     }
203 
204     /**
205      * Used when BatteryStatsImpl object is created from deserialization of a parcel,
206      * such as Settings app or checkin file.
207      * @param historyBuffer the history buffer
208      */
BatteryStatsHistory(Parcel historyBuffer)209     public BatteryStatsHistory(Parcel historyBuffer) {
210         mStats = null;
211         mHistoryDir = null;
212         mHistoryBuffer = historyBuffer;
213     }
214 
getHistoryDirectory()215     public File getHistoryDirectory() {
216         return mHistoryDir;
217     }
218 
219     /**
220      * Set the active file that mHistoryBuffer is backed up into.
221      *
222      * @param fileNumber the history file that mHistoryBuffer is backed up into.
223      */
setActiveFile(int fileNumber)224     private void setActiveFile(int fileNumber) {
225         mActiveFile = getFile(fileNumber);
226         if (DEBUG) {
227             Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath());
228         }
229     }
230 
231     /**
232      * Create history AtomicFile from file number.
233      * @param num file number.
234      * @return AtomicFile object.
235      */
getFile(int num)236     private AtomicFile getFile(int num) {
237         return new AtomicFile(
238                 new File(mHistoryDir,  num + FILE_SUFFIX));
239     }
240 
241     /**
242      * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
243      * create next history file.
244      */
startNextFile()245     public void startNextFile() {
246         if (mStats == null) {
247             Slog.wtf(TAG, "mStats should not be null when writing history");
248             return;
249         }
250 
251         if (mFileNumbers.isEmpty()) {
252             Slog.wtf(TAG, "mFileNumbers should never be empty");
253             return;
254         }
255 
256         // The last number in mFileNumbers is the highest number. The next file number is highest
257         // number plus one.
258         final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
259         mFileNumbers.add(next);
260         setActiveFile(next);
261 
262         // if free disk space is less than 100MB, delete oldest history file.
263         if (!hasFreeDiskSpace()) {
264             int oldest = mFileNumbers.remove(0);
265             getFile(oldest).delete();
266         }
267 
268         // if there are more history files than allowed, delete oldest history files.
269         // MAX_HISTORY_FILES can be updated by GService config at run time.
270         while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
271             int oldest = mFileNumbers.get(0);
272             getFile(oldest).delete();
273             mFileNumbers.remove(0);
274         }
275     }
276 
277     /**
278      * Delete all existing history files. Active history file start from number 0 again.
279      */
resetAllFiles()280     public void resetAllFiles() {
281         for (Integer i : mFileNumbers) {
282             getFile(i).delete();
283         }
284         mFileNumbers.clear();
285         mFileNumbers.add(0);
286         setActiveFile(0);
287     }
288 
289     /**
290      * Start iterating history files and history buffer.
291      * @return always return true.
292      */
startIteratingHistory()293     public boolean startIteratingHistory() {
294         mRecordCount = 0;
295         mCurrentFileIndex = 0;
296         mCurrentParcel = null;
297         mCurrentParcelEnd = 0;
298         mParcelIndex = 0;
299         return true;
300     }
301 
302     /**
303      * Finish iterating history files and history buffer.
304      */
finishIteratingHistory()305     public void finishIteratingHistory() {
306         // setDataPosition so mHistoryBuffer Parcel can be written.
307         mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
308         if (DEBUG) {
309             Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
310         }
311     }
312 
313     /**
314      * When iterating history files and history buffer, always start from the lowest numbered
315      * history file, when reached the mActiveFile (highest numbered history file), do not read from
316      * mActiveFile, read from history buffer instead because the buffer has more updated data.
317      * @param out a history item.
318      * @return The parcel that has next record. null if finished all history files and history
319      *         buffer
320      */
getNextParcel(BatteryStats.HistoryItem out)321     public Parcel getNextParcel(BatteryStats.HistoryItem out) {
322         if (mRecordCount == 0) {
323             // reset out if it is the first record.
324             out.clear();
325         }
326         ++mRecordCount;
327 
328         // First iterate through all records in current parcel.
329         if (mCurrentParcel != null)
330         {
331             if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
332                 // There are more records in current parcel.
333                 return mCurrentParcel;
334             } else if (mHistoryBuffer == mCurrentParcel) {
335                 // finished iterate through all history files and history buffer.
336                 return null;
337             } else if (mHistoryParcels == null
338                     || !mHistoryParcels.contains(mCurrentParcel)) {
339                 // current parcel is from history file.
340                 mCurrentParcel.recycle();
341             }
342         }
343 
344         // Try next available history file.
345         // skip the last file because its data is in history buffer.
346         while (mCurrentFileIndex < mFileNumbers.size() - 1) {
347             mCurrentParcel = null;
348             mCurrentParcelEnd = 0;
349             final Parcel p = Parcel.obtain();
350             AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
351             if (readFileToParcel(p, file)) {
352                 int bufSize = p.readInt();
353                 int curPos = p.dataPosition();
354                 mCurrentParcelEnd = curPos + bufSize;
355                 mCurrentParcel = p;
356                 if (curPos < mCurrentParcelEnd) {
357                     return mCurrentParcel;
358                 }
359             } else {
360                 p.recycle();
361             }
362         }
363 
364         // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
365         // of a parcel, such as Settings app or checkin file.
366         if (mHistoryParcels != null) {
367             while (mParcelIndex < mHistoryParcels.size()) {
368                 final Parcel p = mHistoryParcels.get(mParcelIndex++);
369                 if (!skipHead(p)) {
370                     continue;
371                 }
372                 final int bufSize = p.readInt();
373                 final int curPos = p.dataPosition();
374                 mCurrentParcelEnd = curPos + bufSize;
375                 mCurrentParcel = p;
376                 if (curPos < mCurrentParcelEnd) {
377                     return mCurrentParcel;
378                 }
379             }
380         }
381 
382         // finished iterator through history files (except the last one), now history buffer.
383         if (mHistoryBuffer.dataSize() <= 0) {
384             // buffer is empty.
385             return null;
386         }
387         mHistoryBuffer.setDataPosition(0);
388         mCurrentParcel = mHistoryBuffer;
389         mCurrentParcelEnd = mCurrentParcel.dataSize();
390         return mCurrentParcel;
391     }
392 
393     /**
394      * Read history file into a parcel.
395      * @param out the Parcel read into.
396      * @param file the File to read from.
397      * @return true if success, false otherwise.
398      */
readFileToParcel(Parcel out, AtomicFile file)399     public boolean readFileToParcel(Parcel out, AtomicFile file) {
400         byte[] raw = null;
401         try {
402             final long start = SystemClock.uptimeMillis();
403             raw = file.readFully();
404             if (DEBUG) {
405                 Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
406                         + " duration ms:" + (SystemClock.uptimeMillis() - start));
407             }
408         } catch(Exception e) {
409             Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
410             return false;
411         }
412         out.unmarshall(raw, 0, raw.length);
413         out.setDataPosition(0);
414         return skipHead(out);
415     }
416 
417     /**
418      * Skip the header part of history parcel.
419      * @param p history parcel to skip head.
420      * @return true if version match, false if not.
421      */
skipHead(Parcel p)422     private boolean skipHead(Parcel p) {
423         p.setDataPosition(0);
424         final int version = p.readInt();
425         if (version != BatteryStatsImpl.VERSION) {
426             return false;
427         }
428         // skip historyBaseTime field.
429         p.readLong();
430         return true;
431     }
432 
433     /**
434      * Read all history files and serialize into a big Parcel.
435      * Checkin file calls this method.
436      *
437      * @param out the output parcel
438      */
writeToParcel(Parcel out)439     public void writeToParcel(Parcel out) {
440         writeToParcel(out, false /* useBlobs */);
441     }
442 
443     /**
444      * This is for Settings app, when Settings app receives big history parcel, it call
445      * this method to parse it into list of parcels.
446      * @param out the output parcel
447      */
writeToBatteryUsageStatsParcel(Parcel out)448     public void writeToBatteryUsageStatsParcel(Parcel out) {
449         out.writeBlob(mHistoryBuffer.marshall());
450         writeToParcel(out, true /* useBlobs */);
451     }
452 
writeToParcel(Parcel out, boolean useBlobs)453     private void writeToParcel(Parcel out, boolean useBlobs) {
454         final long start = SystemClock.uptimeMillis();
455         out.writeInt(mFileNumbers.size() - 1);
456         for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
457             AtomicFile file = getFile(mFileNumbers.get(i));
458             byte[] raw = new byte[0];
459             try {
460                 raw = file.readFully();
461             } catch(Exception e) {
462                 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
463             }
464             if (useBlobs) {
465                 out.writeBlob(raw);
466             } else {
467                 // Avoiding blobs in the check-in file for compatibility
468                 out.writeByteArray(raw);
469             }
470         }
471         if (DEBUG) {
472             Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
473         }
474     }
475 
476     /**
477      * Reads a BatteryStatsHistory from a parcel written with
478      * the {@link #writeToBatteryUsageStatsParcel} method.
479      */
createFromBatteryUsageStatsParcel(Parcel in)480     public static BatteryStatsHistory createFromBatteryUsageStatsParcel(Parcel in) {
481         final byte[] historyBlob = in.readBlob();
482 
483         Parcel historyBuffer = Parcel.obtain();
484         historyBuffer.unmarshall(historyBlob, 0, historyBlob.length);
485 
486         BatteryStatsHistory history = new BatteryStatsHistory(historyBuffer);
487         history.readFromParcel(in, true /* useBlobs */);
488         return history;
489     }
490 
491     /**
492      * This is for the check-in file, which has all history files embedded.
493      *
494      * @param in the input parcel.
495      */
readFromParcel(Parcel in)496     public void readFromParcel(Parcel in) {
497         readFromParcel(in, false /* useBlobs */);
498     }
499 
readFromParcel(Parcel in, boolean useBlobs)500     private void readFromParcel(Parcel in, boolean useBlobs) {
501         final long start = SystemClock.uptimeMillis();
502         mHistoryParcels = new ArrayList<>();
503         final int count = in.readInt();
504         for(int i = 0; i < count; i++) {
505             byte[] temp = useBlobs ? in.readBlob() : in.createByteArray();
506             if (temp == null || temp.length == 0) {
507                 continue;
508             }
509             Parcel p = Parcel.obtain();
510             p.unmarshall(temp, 0, temp.length);
511             p.setDataPosition(0);
512             mHistoryParcels.add(p);
513         }
514         if (DEBUG) {
515             Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
516         }
517     }
518 
519     /**
520      * @return true if there is more than 100MB free disk space left.
521      */
hasFreeDiskSpace()522     private boolean hasFreeDiskSpace() {
523         final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
524         return stats.getAvailableBytes() > MIN_FREE_SPACE;
525     }
526 
getFilesNumbers()527     public List<Integer> getFilesNumbers() {
528         return mFileNumbers;
529     }
530 
getActiveFile()531     public AtomicFile getActiveFile() {
532         return mActiveFile;
533     }
534 
535     /**
536      * @return the total size of all history files and history buffer.
537      */
getHistoryUsedSize()538     public int getHistoryUsedSize() {
539         int ret = 0;
540         for(int i = 0; i < mFileNumbers.size() - 1; i++) {
541             ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
542         }
543         ret += mHistoryBuffer.dataSize();
544         if (mHistoryParcels != null) {
545             for(int i = 0; i < mHistoryParcels.size(); i++) {
546                 ret += mHistoryParcels.get(i).dataSize();
547             }
548         }
549         return ret;
550     }
551 
552     /**
553      * Writes event details into Atrace.
554      */
recordTraceEvents(int code, HistoryTag tag)555     public void recordTraceEvents(int code, HistoryTag tag) {
556         if (code == HistoryItem.EVENT_NONE) return;
557         if (!mTracer.tracingEnabled()) return;
558 
559         final int idx = code & HistoryItem.EVENT_TYPE_MASK;
560         final String prefix = (code & HistoryItem.EVENT_FLAG_START) != 0 ? "+" :
561                   (code & HistoryItem.EVENT_FLAG_FINISH) != 0 ? "-" : "";
562 
563         final String[] names = BatteryStats.HISTORY_EVENT_NAMES;
564         if (idx < 0 || idx >= BatteryStats.HISTORY_EVENT_NAMES.length) return;
565 
566         final String track = "battery_stats." + names[idx];
567         final String name = prefix + names[idx] + "=" + tag.uid + ":\"" + tag.string + "\"";
568         mTracer.traceInstantEvent(track, name);
569     }
570 
571     /**
572      * Writes changes to a HistoryItem state bitmap to Atrace.
573      */
recordTraceCounters(int oldval, int newval, BitDescription[] descriptions)574     public void recordTraceCounters(int oldval, int newval, BitDescription[] descriptions) {
575         if (!mTracer.tracingEnabled()) return;
576 
577         int diff = oldval ^ newval;
578         if (diff == 0) return;
579 
580         for (int i = 0; i < descriptions.length; i++) {
581             BitDescription bd = descriptions[i];
582             if ((diff & bd.mask) == 0) continue;
583 
584             int value;
585             if (bd.shift < 0) {
586                 value = (newval & bd.mask) != 0 ? 1 : 0;
587             } else {
588                 value = (newval & bd.mask) >> bd.shift;
589             }
590 
591             mTracer.traceCounter("battery_stats." + bd.name, value);
592         }
593     }
594 }
595