blob: 392f0748ca0dcd7c84b561ef281e9695e01fabfc [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.os;
import android.os.BatteryStats;
import android.os.Parcel;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ParseUtils;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* BatteryStatsHistory encapsulates battery history files.
* Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
* {@link #mActiveFile}.
* When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
* current mActiveFile is closed and a new mActiveFile is open.
* History files are under directory /data/system/battery-history/.
* History files have name battery-history-<num>.bin. The file number <num> starts from zero and
* grows sequentially.
* The mActiveFile is always the highest numbered history file.
* The lowest number file is always the oldest file.
* The highest number file is always the newest file.
* The file number grows sequentially and we never skip number.
* When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
* the lowest numbered file is deleted and a new file is open.
*
* All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
* locks on BatteryStatsImpl object.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class BatteryStatsHistory {
private static final boolean DEBUG = false;
private static final String TAG = "BatteryStatsHistory";
public static final String HISTORY_DIR = "battery-history";
public static final String FILE_SUFFIX = ".bin";
private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
private final BatteryStatsImpl mStats;
private final Parcel mHistoryBuffer;
private final File mHistoryDir;
/**
* The active history file that the history buffer is backed up into.
*/
private AtomicFile mActiveFile;
/**
* A list of history files with incremental indexes.
*/
private final List<Integer> mFileNumbers = new ArrayList<>();
/**
* A list of small history parcels, used when BatteryStatsImpl object is created from
* deserialization of a parcel, such as Settings app or checkin file.
*/
private List<Parcel> mHistoryParcels = null;
/**
* When iterating history files, the current file index.
*/
private int mCurrentFileIndex;
/**
* When iterating history files, the current file parcel.
*/
private Parcel mCurrentParcel;
/**
* When iterating history file, the current parcel's Parcel.dataSize().
*/
private int mCurrentParcelEnd;
/**
* When iterating history files, the current record count.
*/
private int mRecordCount = 0;
/**
* Used when BatteryStatsImpl object is created from deserialization of a parcel,
* such as Settings app or checkin file, to iterate over history parcels.
*/
private int mParcelIndex = 0;
/**
* Constructor
* @param stats BatteryStatsImpl object.
* @param systemDir typically /data/system
* @param historyBuffer The in-memory history buffer.
*/
public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) {
mStats = stats;
mHistoryBuffer = historyBuffer;
mHistoryDir = new File(systemDir, HISTORY_DIR);
mHistoryDir.mkdirs();
if (!mHistoryDir.exists()) {
Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
}
final Set<Integer> dedup = new ArraySet<>();
// scan directory, fill mFileNumbers and mActiveFile.
mHistoryDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
final int b = name.lastIndexOf(FILE_SUFFIX);
if (b <= 0) {
return false;
}
final Integer c =
ParseUtils.parseInt(name.substring(0, b), -1);
if (c != -1) {
dedup.add(c);
return true;
} else {
return false;
}
}
});
if (!dedup.isEmpty()) {
mFileNumbers.addAll(dedup);
Collections.sort(mFileNumbers);
setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1));
} else {
// No file found, default to have file 0.
mFileNumbers.add(0);
setActiveFile(0);
}
}
/**
* Used when BatteryStatsImpl object is created from deserialization of a parcel,
* such as Settings app or checkin file.
* @param stats BatteryStatsImpl object.
* @param historyBuffer the history buffer inside BatteryStatsImpl
*/
public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) {
mStats = stats;
mHistoryDir = null;
mHistoryBuffer = historyBuffer;
}
/**
* Set the active file that mHistoryBuffer is backed up into.
*
* @param fileNumber the history file that mHistoryBuffer is backed up into.
*/
private void setActiveFile(int fileNumber) {
mActiveFile = getFile(fileNumber);
if (DEBUG) {
Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath());
}
}
/**
* Create history AtomicFile from file number.
* @param num file number.
* @return AtomicFile object.
*/
private AtomicFile getFile(int num) {
return new AtomicFile(
new File(mHistoryDir, num + FILE_SUFFIX));
}
/**
* When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
* create next history file.
*/
public void startNextFile() {
if (mFileNumbers.isEmpty()) {
Slog.wtf(TAG, "mFileNumbers should never be empty");
return;
}
// The last number in mFileNumbers is the highest number. The next file number is highest
// number plus one.
final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
mFileNumbers.add(next);
setActiveFile(next);
// if free disk space is less than 100MB, delete oldest history file.
if (!hasFreeDiskSpace()) {
int oldest = mFileNumbers.remove(0);
getFile(oldest).delete();
}
// if there are more history files than allowed, delete oldest history files.
// MAX_HISTORY_FILES can be updated by GService config at run time.
while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
int oldest = mFileNumbers.get(0);
getFile(oldest).delete();
mFileNumbers.remove(0);
}
}
/**
* Delete all existing history files. Active history file start from number 0 again.
*/
public void resetAllFiles() {
for (Integer i : mFileNumbers) {
getFile(i).delete();
}
mFileNumbers.clear();
mFileNumbers.add(0);
setActiveFile(0);
}
/**
* Start iterating history files and history buffer.
* @return always return true.
*/
public boolean startIteratingHistory() {
mRecordCount = 0;
mCurrentFileIndex = 0;
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
return true;
}
/**
* Finish iterating history files and history buffer.
*/
public void finishIteratingHistory() {
// setDataPosition so mHistoryBuffer Parcel can be written.
mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
if (DEBUG) {
Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
}
}
/**
* When iterating history files and history buffer, always start from the lowest numbered
* history file, when reached the mActiveFile (highest numbered history file), do not read from
* mActiveFile, read from history buffer instead because the buffer has more updated data.
* @param out a history item.
* @return The parcel that has next record. null if finished all history files and history
* buffer
*/
public Parcel getNextParcel(BatteryStats.HistoryItem out) {
if (mRecordCount == 0) {
// reset out if it is the first record.
out.clear();
}
++mRecordCount;
// First iterate through all records in current parcel.
if (mCurrentParcel != null)
{
if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
// There are more records in current parcel.
return mCurrentParcel;
} else if (mHistoryBuffer == mCurrentParcel) {
// finished iterate through all history files and history buffer.
return null;
} else if (mHistoryParcels == null
|| !mHistoryParcels.contains(mCurrentParcel)) {
// current parcel is from history file.
mCurrentParcel.recycle();
}
}
// Try next available history file.
// skip the last file because its data is in history buffer.
while (mCurrentFileIndex < mFileNumbers.size() - 1) {
mCurrentParcel = null;
mCurrentParcelEnd = 0;
final Parcel p = Parcel.obtain();
AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
if (readFileToParcel(p, file)) {
int bufSize = p.readInt();
int curPos = p.dataPosition();
mCurrentParcelEnd = curPos + bufSize;
mCurrentParcel = p;
if (curPos < mCurrentParcelEnd) {
return mCurrentParcel;
}
} else {
p.recycle();
}
}
// mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
// of a parcel, such as Settings app or checkin file.
if (mHistoryParcels != null) {
while (mParcelIndex < mHistoryParcels.size()) {
final Parcel p = mHistoryParcels.get(mParcelIndex++);
if (!skipHead(p)) {
continue;
}
final int bufSize = p.readInt();
final int curPos = p.dataPosition();
mCurrentParcelEnd = curPos + bufSize;
mCurrentParcel = p;
if (curPos < mCurrentParcelEnd) {
return mCurrentParcel;
}
}
}
// finished iterator through history files (except the last one), now history buffer.
if (mHistoryBuffer.dataSize() <= 0) {
// buffer is empty.
return null;
}
mHistoryBuffer.setDataPosition(0);
mCurrentParcel = mHistoryBuffer;
mCurrentParcelEnd = mCurrentParcel.dataSize();
return mCurrentParcel;
}
/**
* Read history file into a parcel.
* @param out the Parcel read into.
* @param file the File to read from.
* @return true if success, false otherwise.
*/
public boolean readFileToParcel(Parcel out, AtomicFile file) {
byte[] raw = null;
try {
final long start = SystemClock.uptimeMillis();
raw = file.readFully();
if (DEBUG) {
Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
+ " duration ms:" + (SystemClock.uptimeMillis() - start));
}
} catch(Exception e) {
Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
return false;
}
out.unmarshall(raw, 0, raw.length);
out.setDataPosition(0);
return skipHead(out);
}
/**
* Skip the header part of history parcel.
* @param p history parcel to skip head.
* @return true if version match, false if not.
*/
private boolean skipHead(Parcel p) {
p.setDataPosition(0);
final int version = p.readInt();
if (version != mStats.VERSION) {
return false;
}
// skip historyBaseTime field.
p.readLong();
return true;
}
/**
* Read all history files and serialize into a big Parcel. This is to send history files to
* Settings app since Settings app can not access /data/system directory.
* Checkin file also call this method.
* @param out the output parcel
*/
public void writeToParcel(Parcel out) {
final long start = SystemClock.uptimeMillis();
out.writeInt(mFileNumbers.size() - 1);
for(int i = 0; i < mFileNumbers.size() - 1; i++) {
AtomicFile file = getFile(mFileNumbers.get(i));
byte[] raw = new byte[0];
try {
raw = file.readFully();
} catch(Exception e) {
Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
}
out.writeByteArray(raw);
}
if (DEBUG) {
Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
}
}
/**
* This is for Settings app, when Settings app receives big history parcel, it call
* this method to parse it into list of parcels.
* Checkin file also call this method.
* @param in the input parcel.
*/
public void readFromParcel(Parcel in) {
final long start = SystemClock.uptimeMillis();
mHistoryParcels = new ArrayList<>();
final int count = in.readInt();
for(int i = 0; i < count; i++) {
byte[] temp = in.createByteArray();
if (temp.length == 0) {
continue;
}
Parcel p = Parcel.obtain();
p.unmarshall(temp, 0, temp.length);
p.setDataPosition(0);
mHistoryParcels.add(p);
}
if (DEBUG) {
Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
}
}
/**
* @return true if there is more than 100MB free disk space left.
*/
private boolean hasFreeDiskSpace() {
final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
return stats.getAvailableBytes() > MIN_FREE_SPACE;
}
public List<Integer> getFilesNumbers() {
return mFileNumbers;
}
public AtomicFile getActiveFile() {
return mActiveFile;
}
/**
* @return the total size of all history files and history buffer.
*/
public int getHistoryUsedSize() {
int ret = 0;
for(int i = 0; i < mFileNumbers.size() - 1; i++) {
ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
}
ret += mHistoryBuffer.dataSize();
if (mHistoryParcels != null) {
for(int i = 0; i < mHistoryParcels.size(); i++) {
ret += mHistoryParcels.get(i).dataSize();
}
}
return ret;
}
}