blob: 2a95416747a6e90ee69d8adae4fff6b84e056de9 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.powerstats;
import android.content.Context;
import android.util.Slog;
import com.android.internal.util.FileRotator;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.ReentrantLock;
/**
* PowerStatsDataStorage implements the on-device storage cache for energy
* data. This data must be persisted across boot cycles so we store it
* on-device. Versioning of this data is handled by deleting any data that
* does not match the current version. The cache is implemented as a circular
* buffer using the FileRotator class in android.util. We maintain 48 hours
* worth of logs in 12 files (4 hours each).
*/
public class PowerStatsDataStorage {
private static final String TAG = PowerStatsDataStorage.class.getSimpleName();
private static final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
// Rotate files every 4 hours.
private static final long ROTATE_AGE_MILLIS = 4 * MILLISECONDS_PER_HOUR;
// Store 48 hours worth of data.
private static final long DELETE_AGE_MILLIS = 48 * MILLISECONDS_PER_HOUR;
private final ReentrantLock mLock = new ReentrantLock();
private final File mDataStorageDir;
private final String mDataStorageFilename;
private final FileRotator mFileRotator;
private static class DataElement {
private static final int LENGTH_FIELD_WIDTH = 4;
private static final int MAX_DATA_ELEMENT_SIZE = 1000;
private byte[] mData;
private byte[] toByteArray() throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(ByteBuffer.allocate(LENGTH_FIELD_WIDTH).putInt(mData.length).array());
data.write(mData);
return data.toByteArray();
}
protected byte[] getData() {
return mData;
}
private DataElement(byte[] data) {
mData = data;
}
private DataElement(InputStream in) throws IOException {
byte[] lengthBytes = new byte[LENGTH_FIELD_WIDTH];
int bytesRead = in.read(lengthBytes);
mData = new byte[0];
if (bytesRead == LENGTH_FIELD_WIDTH) {
int length = ByteBuffer.wrap(lengthBytes).getInt();
if (0 < length && length < MAX_DATA_ELEMENT_SIZE) {
mData = new byte[length];
bytesRead = in.read(mData);
if (bytesRead != length) {
throw new IOException("Invalid bytes read, expected: " + length
+ ", actual: " + bytesRead);
}
} else {
throw new IOException("DataElement size is invalid: " + length);
}
} else {
throw new IOException("Did not read " + LENGTH_FIELD_WIDTH + " bytes (" + bytesRead
+ ")");
}
}
}
/**
* Used by external classes to read DataElements from on-device storage.
* This callback is passed in to the read() function and is called for
* each DataElement read from on-device storage.
*/
public interface DataElementReadCallback {
/**
* When performing a read of the on-device storage this callback
* must be passed in to the read function. The function will be
* called for each DataElement read from on-device storage.
*
* @param data Byte array containing a DataElement payload.
*/
void onReadDataElement(byte[] data);
}
private static class DataReader implements FileRotator.Reader {
private DataElementReadCallback mCallback;
DataReader(DataElementReadCallback callback) {
mCallback = callback;
}
@Override
public void read(InputStream in) throws IOException {
while (in.available() > 0) {
try {
DataElement dataElement = new DataElement(in);
mCallback.onReadDataElement(dataElement.getData());
} catch (IOException e) {
Slog.e(TAG, "Failed to read from storage. " + e.getMessage());
}
}
}
}
private static class DataRewriter implements FileRotator.Rewriter {
byte[] mActiveFileData;
byte[] mNewData;
DataRewriter(byte[] data) {
mActiveFileData = new byte[0];
mNewData = data;
}
@Override
public void reset() {
// ignored
}
@Override
public void read(InputStream in) throws IOException {
mActiveFileData = new byte[in.available()];
in.read(mActiveFileData);
}
@Override
public boolean shouldWrite() {
return true;
}
@Override
public void write(OutputStream out) throws IOException {
out.write(mActiveFileData);
out.write(mNewData);
}
}
public PowerStatsDataStorage(Context context, File dataStoragePath,
String dataStorageFilename) {
mDataStorageDir = dataStoragePath;
mDataStorageFilename = dataStorageFilename;
if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) {
Slog.wtf(TAG, "mDataStorageDir does not exist: " + mDataStorageDir.getPath());
mFileRotator = null;
} else {
// Delete files written with an old version number. The version is included in the
// filename, so any files that don't match the current version number can be deleted.
File[] files = mDataStorageDir.listFiles();
for (int i = 0; i < files.length; i++) {
// Meter, model, and residency files are stored in the same directory.
//
// The format of filenames on disk is:
// log.powerstats.meter.version.timestamp
// log.powerstats.model.version.timestamp
// log.powerstats.residency.version.timestamp
//
// The format of dataStorageFilenames is:
// log.powerstats.meter.version
// log.powerstats.model.version
// log.powerstats.residency.version
//
// A PowerStatsDataStorage object is created for meter, model, and residency data.
// Strip off the version and check that the current file we're checking starts with
// the stem (log.powerstats.meter, log.powerstats.model, log.powerstats.residency).
// If the stem matches and the version number is different, delete the old file.
int versionDot = mDataStorageFilename.lastIndexOf('.');
String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
// Check that the stems match.
if (files[i].getName().startsWith(beforeVersionDot)) {
// Check that the version number matches. If not, delete the old file.
if (!files[i].getName().startsWith(mDataStorageFilename)) {
files[i].delete();
}
}
}
mFileRotator = new FileRotator(mDataStorageDir,
mDataStorageFilename,
ROTATE_AGE_MILLIS,
DELETE_AGE_MILLIS);
}
}
/**
* Writes data stored in PowerStatsDataStorage to a file descriptor.
*
* @param data Byte array to write to on-device storage. Byte array is
* converted to a DataElement which prefixes the payload with
* the data length. The DataElement is then converted to a byte
* array and written to on-device storage.
*/
public void write(byte[] data) {
if (data != null && data.length > 0) {
mLock.lock();
long currentTimeMillis = System.currentTimeMillis();
try {
DataElement dataElement = new DataElement(data);
mFileRotator.rewriteActive(new DataRewriter(dataElement.toByteArray()),
currentTimeMillis);
mFileRotator.maybeRotate(currentTimeMillis);
} catch (IOException e) {
Slog.e(TAG, "Failed to write to on-device storage: " + e);
}
mLock.unlock();
}
}
/**
* Reads all DataElements stored in on-device storage. For each
* DataElement retrieved from on-device storage, callback is called.
*/
public void read(DataElementReadCallback callback) throws IOException {
mFileRotator.readMatching(new DataReader(callback), Long.MIN_VALUE, Long.MAX_VALUE);
}
/**
* Deletes all stored log data.
*/
public void deleteLogs() {
File[] files = mDataStorageDir.listFiles();
for (int i = 0; i < files.length; i++) {
int versionDot = mDataStorageFilename.lastIndexOf('.');
String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
// Check that the stems before the version match.
if (files[i].getName().startsWith(beforeVersionDot)) {
files[i].delete();
}
}
}
}