Initial commit of PowerStatsService

Bug: 164465661
Bug: 164466995
Bug: 167280723
Test: Verified PowerStatsService is functional on targets that
have ODPM data.

Change-Id: Ic555b380c566ea26bc2214374f142c5448ea2ee7
This commit is contained in:
Mat Bevilacqua 2020-07-30 17:26:45 -07:00
parent 969e194325
commit 8160121220
20 changed files with 1727 additions and 0 deletions

View File

@ -3384,6 +3384,7 @@ public abstract class Context {
/** @hide */
@StringDef(suffix = { "_SERVICE" }, value = {
POWER_SERVICE,
//@hide: POWER_STATS_SERVICE,
WINDOW_SERVICE,
LAYOUT_INFLATER_SERVICE,
ACCOUNT_SERVICE,
@ -3724,6 +3725,16 @@ public abstract class Context {
*/
public static final String POWER_SERVICE = "power";
/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.PowerStatsService} for accessing power stats
* service.
*
* @see #getSystemService(String)
* @hide
*/
public static final String POWER_STATS_SERVICE = "power_stats";
/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.RecoverySystem} for accessing the recovery system

View File

@ -40,6 +40,7 @@ import "frameworks/base/core/proto/android/server/fingerprint.proto";
import "frameworks/base/core/proto/android/server/jobscheduler.proto";
import "frameworks/base/core/proto/android/server/location/context_hub.proto";
import "frameworks/base/core/proto/android/server/powermanagerservice.proto";
import "frameworks/base/core/proto/android/server/powerstatsservice.proto";
import "frameworks/base/core/proto/android/server/rolemanagerservice.proto";
import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
import "frameworks/base/core/proto/android/service/appwidget.proto";
@ -510,6 +511,11 @@ message IncidentProto {
(section).args = "sensorservice --proto"
];
optional com.android.server.powerstats.PowerStatsServiceProto powerstats = 3054 [
(section).type = SECTION_DUMPSYS,
(section).args = "power_stats --proto"
];
// Dumps in text format (on userdebug and eng builds only): 4000 ~ 4999
optional android.util.TextDumpProto textdump_wifi = 4000 [
(section).type = SECTION_TEXT_DUMPSYS,

View File

@ -0,0 +1,67 @@
/*
* 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.
*/
syntax = "proto2";
package com.android.server.powerstats;
option java_multiple_files = true;
message IncidentReportProto {
/** Section number matches that in incident.proto */
optional PowerStatsServiceProto incident_report = 3054;
}
message PowerStatsServiceProto {
repeated RailInfoProto rail_info = 1;
repeated EnergyDataProto energy_data = 2;
}
/**
* Rail information:
* Reports information related to the rails being monitored.
*/
message RailInfoProto {
/** Index corresponding to the rail */
optional int32 index = 1;
/** Name of the rail (opaque to the framework) */
optional string rail_name = 2;
/** Name of the subsystem to which this rail belongs (opaque to the framework) */
optional string subsys_name = 3;
/** Hardware sampling rate */
optional int32 sampling_rate = 4;
}
/**
* Rail level energy measurements:
* Reports accumulated energy since boot on each rail.
*/
message EnergyDataProto {
/**
* Index corresponding to the rail. This index matches
* the index returned in RailInfo
*/
optional int32 index = 1;
/** Time since device boot(CLOCK_BOOTTIME) in milli-seconds */
optional int64 timestamp_ms = 2;
/** Accumulated energy since device boot in microwatt-seconds (uWs) */
optional int64 energy_uws = 3;
}

View File

@ -0,0 +1,65 @@
/*
* 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.util.Log;
/**
* BatteryTrigger instantiates a BroadcastReceiver that listens for changes
* to the battery. When the battery level drops by 1% a message is sent to
* the PowerStatsLogger to log the rail energy data to on-device storage.
*/
public final class BatteryTrigger extends PowerStatsLogTrigger {
private static final String TAG = BatteryTrigger.class.getSimpleName();
private static final boolean DEBUG = false;
private int mBatteryLevel = 0;
private final BroadcastReceiver mBatteryLevelReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Intent.ACTION_BATTERY_CHANGED:
int newBatteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
if (newBatteryLevel < mBatteryLevel) {
if (DEBUG) Log.d(TAG, "Battery level dropped. Log rail data");
logPowerStatsData();
}
mBatteryLevel = newBatteryLevel;
break;
}
}
};
public BatteryTrigger(Context context, PowerStatsLogger powerStatsLogger,
boolean triggerEnabled) {
super(context, powerStatsLogger);
if (triggerEnabled) {
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = mContext.registerReceiver(mBatteryLevelReceiver, filter);
mBatteryLevel = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
}
}
}

View File

@ -0,0 +1,286 @@
/*
* 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.util.Log;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoUtils;
import android.util.proto.WireTypeMismatchException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* PowerStatsData is a class that performs two operations:
* 1) Unpacks serialized protobuf byte arrays, as defined in powerstatsservice.proto,
* into RailInfo or EnergyData object arrays.
*
* 2) Packs RailInfo or EnergyData object arrays in protobuf byte arrays as
* defined in powerstatsservice.proto.
*
* Inside frameworks, proto source is generated with the genstream option
* and therefore the getter/setter helper functions are not available.
* The protos need to be packed/unpacked in a more manual way using
* ProtoOutputStream/ProtoInputStream.
*/
public class PowerStatsData {
private static final String TAG = PowerStatsData.class.getSimpleName();
private List<Data> mDataList;
public PowerStatsData(ProtoInputStream pis) throws IOException {
mDataList = new ArrayList<Data>();
unpackProto(pis);
}
public PowerStatsData(Data[] data) {
mDataList = new ArrayList<Data>(Arrays.asList(data));
}
private void unpackProto(ProtoInputStream pis) throws IOException {
long token;
while (true) {
try {
switch (pis.nextField()) {
case (int) PowerStatsServiceProto.RAIL_INFO:
token = pis.start(PowerStatsServiceProto.RAIL_INFO);
mDataList.add(new RailInfo(pis));
pis.end(token);
break;
case (int) PowerStatsServiceProto.ENERGY_DATA:
token = pis.start(PowerStatsServiceProto.ENERGY_DATA);
mDataList.add(new EnergyData(pis));
pis.end(token);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
default:
Log.e(TAG, "Unhandled field in proto: "
+ ProtoUtils.currentFieldToString(pis));
break;
}
} catch (WireTypeMismatchException wtme) {
Log.e(TAG, "Wire Type mismatch in proto: " + ProtoUtils.currentFieldToString(pis));
}
}
}
/**
* Write this object to an output stream in protobuf format.
*
* @param pos ProtoOutputStream of file where data is to be written. Data is
* written in protobuf format as defined by powerstatsservice.proto.
*/
public void toProto(ProtoOutputStream pos) {
long token;
for (Data data : mDataList) {
if (data instanceof RailInfo) {
token = pos.start(PowerStatsServiceProto.RAIL_INFO);
} else {
token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
}
data.toProto(pos);
pos.end(token);
}
}
/**
* Convert mDataList to proto format and return the serialized byte array.
*
* @return byte array containing a serialized protobuf of mDataList.
*/
public byte[] getProtoBytes() {
ProtoOutputStream pos = new ProtoOutputStream();
long token;
for (Data data : mDataList) {
if (data instanceof RailInfo) {
token = pos.start(PowerStatsServiceProto.RAIL_INFO);
} else {
token = pos.start(PowerStatsServiceProto.ENERGY_DATA);
}
data.toProto(pos);
pos.end(token);
}
return pos.getBytes();
}
/**
* Print this object to logcat.
*/
public void print() {
for (Data data : mDataList) {
Log.d(TAG, data.toString());
}
}
/**
* RailInfo is a class that stores a description for an individual ODPM
* rail. It provides functionality to unpack a RailInfo object from a
* serialized protobuf byte array, and to pack a RailInfo object into
* a ProtoOutputStream.
*/
public static class RailInfo extends Data {
public String mRailName;
public String mSubSysName;
public long mSamplingRate;
public RailInfo(ProtoInputStream pis) throws IOException {
unpackProto(pis);
}
public RailInfo(long index, String railName, String subSysName, long samplingRate) {
mIndex = index;
mRailName = railName;
mSubSysName = subSysName;
mSamplingRate = samplingRate;
}
@Override
protected void unpackProto(ProtoInputStream pis) throws IOException {
while (true) {
try {
switch (pis.nextField()) {
case (int) RailInfoProto.INDEX:
mIndex = pis.readInt(RailInfoProto.INDEX);
break;
case (int) RailInfoProto.RAIL_NAME:
mRailName = pis.readString(RailInfoProto.RAIL_NAME);
break;
case (int) RailInfoProto.SUBSYS_NAME:
mSubSysName = pis.readString(RailInfoProto.SUBSYS_NAME);
break;
case (int) RailInfoProto.SAMPLING_RATE:
mSamplingRate = pis.readInt(RailInfoProto.SAMPLING_RATE);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
default:
Log.e(TAG, "Unhandled field in RailInfoProto: "
+ ProtoUtils.currentFieldToString(pis));
break;
}
} catch (WireTypeMismatchException wtme) {
Log.e(TAG, "Wire Type mismatch in RailInfoProto: "
+ ProtoUtils.currentFieldToString(pis));
}
}
}
@Override
public void toProto(ProtoOutputStream pos) {
pos.write(RailInfoProto.INDEX, mIndex);
pos.write(RailInfoProto.RAIL_NAME, mRailName);
pos.write(RailInfoProto.SUBSYS_NAME, mSubSysName);
pos.write(RailInfoProto.SAMPLING_RATE, mSamplingRate);
}
@Override
public String toString() {
return String.format("Index = " + mIndex
+ ", RailName = " + mRailName
+ ", SubSysName = " + mSubSysName
+ ", SamplingRate = " + mSamplingRate);
}
}
/**
* EnergyData is a class that stores an energy (uWs) data reading for an
* individual ODPM rail. It provides functionality to unpack an EnergyData
* object from a serialized protobuf byte array, and to pack an EnergyData
* object into a ProtoOutputStream.
*/
public static class EnergyData extends Data {
public long mTimestampMs;
public long mEnergyUWs;
public EnergyData(ProtoInputStream pis) throws IOException {
unpackProto(pis);
}
public EnergyData(long index, long timestampMs, long energyUWs) {
mIndex = index;
mTimestampMs = timestampMs;
mEnergyUWs = energyUWs;
}
@Override
protected void unpackProto(ProtoInputStream pis) throws IOException {
while (true) {
try {
switch (pis.nextField()) {
case (int) EnergyDataProto.INDEX:
mIndex = pis.readInt(EnergyDataProto.INDEX);
break;
case (int) EnergyDataProto.TIMESTAMP_MS:
mTimestampMs = pis.readLong(EnergyDataProto.TIMESTAMP_MS);
break;
case (int) EnergyDataProto.ENERGY_UWS:
mEnergyUWs = pis.readLong(EnergyDataProto.ENERGY_UWS);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
default:
Log.e(TAG, "Unhandled field in EnergyDataProto: "
+ ProtoUtils.currentFieldToString(pis));
break;
}
} catch (WireTypeMismatchException wtme) {
Log.e(TAG, "Wire Type mismatch in EnergyDataProto: "
+ ProtoUtils.currentFieldToString(pis));
}
}
}
@Override
protected void toProto(ProtoOutputStream pos) {
pos.write(EnergyDataProto.INDEX, mIndex);
pos.write(EnergyDataProto.TIMESTAMP_MS, mTimestampMs);
pos.write(EnergyDataProto.ENERGY_UWS, mEnergyUWs);
}
@Override
public String toString() {
return String.format("Index = " + mIndex
+ ", Timestamp (ms) = " + mTimestampMs
+ ", Energy (uWs) = " + mEnergyUWs);
}
}
private abstract static class Data {
public long mIndex;
protected abstract void unpackProto(ProtoInputStream pis) throws IOException;
protected abstract void toProto(ProtoOutputStream pos);
}
}

View File

@ -0,0 +1,223 @@
/*
* 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.Log;
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 File mDataStorageDir;
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) {
Log.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;
if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) {
Log.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++) {
if (!files[i].getName().matches(dataStorageFilename + "(.*)")) {
files[i].delete();
}
}
mFileRotator = new FileRotator(mDataStorageDir,
dataStorageFilename,
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) {
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) {
Log.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);
}
}

View File

@ -0,0 +1,105 @@
/*
* 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;
/**
* PowerStatsHALWrapper is a wrapper class for the PowerStats HAL API calls.
*/
public final class PowerStatsHALWrapper {
private static final String TAG = PowerStatsHALWrapper.class.getSimpleName();
/**
* IPowerStatsHALWrapper defines the interface to the PowerStatsHAL.
*/
public interface IPowerStatsHALWrapper {
/**
* Returns rail info for all available ODPM rails.
*
* @return array of RailInfo objects containing rail info for all
* available rails.
*/
PowerStatsData.RailInfo[] readRailInfo();
/**
* Returns energy data for all available ODPM rails. Available rails can
* be retrieved by calling nativeGetRailInfo. Energy data and
* rail info can be linked through the index field.
*
* @return array of EnergyData objects containing energy data for all
* available rails.
*/
PowerStatsData.EnergyData[] readEnergyData();
/**
* Returns boolean indicating if connection to power stats HAL was
* established.
*
* @return true if connection to power stats HAL was correctly established
* and that energy data and rail info can be read from the interface.
* false otherwise
*/
boolean initialize();
}
/**
* PowerStatsHALWrapperImpl is the implementation of the IPowerStatsHALWrapper
* used by the PowerStatsService. Other implementations will be used by the testing
* framework and will be passed into the PowerStatsService through an injector.
*/
public static final class PowerStatsHALWrapperImpl implements IPowerStatsHALWrapper {
private static native boolean nativeInit();
private static native PowerStatsData.RailInfo[] nativeGetRailInfo();
private static native PowerStatsData.EnergyData[] nativeGetEnergyData();
/**
* Returns rail info for all available ODPM rails.
*
* @return array of RailInfo objects containing rail info for all
* available rails.
*/
@Override
public PowerStatsData.RailInfo[] readRailInfo() {
return nativeGetRailInfo();
}
/**
* Returns energy data for all available ODPM rails. Available rails can
* be retrieved by calling nativeGetRailInfo. Energy data and
* rail info can be linked through the index field.
*
* @return array of EnergyData objects containing energy data for all
* available rails.
*/
@Override
public PowerStatsData.EnergyData[] readEnergyData() {
return nativeGetEnergyData();
}
/**
* Returns boolean indicating if connection to power stats HAL was
* established.
*
* @return true if connection to power stats HAL was correctly established
* and that energy data and rail info can be read from the interface.
* false otherwise
*/
@Override
public boolean initialize() {
return nativeInit();
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.os.Message;
/**
* PowerStatsLogTrigger is the base class for other trigger classes.
* It provides the logPowerStatsData() function which sends a message
* to the PowerStatsLogger to read the rail energy data and log it to
* on-device storage. This class is abstract and cannot be instantiated.
*/
public abstract class PowerStatsLogTrigger {
private static final String TAG = PowerStatsLogTrigger.class.getSimpleName();
protected Context mContext;
private PowerStatsLogger mPowerStatsLogger;
protected void logPowerStatsData() {
Message.obtain(
mPowerStatsLogger,
PowerStatsLogger.MSG_LOG_TO_DATA_STORAGE).sendToTarget();
}
public PowerStatsLogTrigger(Context context, PowerStatsLogger powerStatsLogger) {
mContext = context;
mPowerStatsLogger = powerStatsLogger;
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
/**
* PowerStatsLogger is responsible for logging energy data to on-device
* storage. Messages are sent to its message handler to request that energy
* data be logged, at which time it queries the PowerStats HAL and logs the
* data to on-device storage. The on-device storage is dumped to file by
* calling writeToFile with a file descriptor that points to the output file.
*/
public final class PowerStatsLogger extends Handler {
private static final String TAG = PowerStatsLogger.class.getSimpleName();
private static final boolean DEBUG = false;
protected static final int MSG_LOG_TO_DATA_STORAGE = 0;
private final PowerStatsDataStorage mPowerStatsDataStorage;
private final IPowerStatsHALWrapper mPowerStatsHALWrapper;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOG_TO_DATA_STORAGE:
if (DEBUG) Log.d(TAG, "Logging to data storage");
PowerStatsData energyData =
new PowerStatsData(mPowerStatsHALWrapper.readEnergyData());
mPowerStatsDataStorage.write(energyData.getProtoBytes());
break;
}
}
/**
* Writes data stored in PowerStatsDataStorage to a file descriptor.
*
* @param fd FileDescriptor where data stored in PowerStatsDataStorage is
* written. Data is written in protobuf format as defined by
* powerstatsservice.proto.
*/
public void writeToFile(FileDescriptor fd) {
if (DEBUG) Log.d(TAG, "Writing to file");
final ProtoOutputStream pos = new ProtoOutputStream(fd);
try {
PowerStatsData railInfo = new PowerStatsData(mPowerStatsHALWrapper.readRailInfo());
railInfo.toProto(pos);
if (DEBUG) railInfo.print();
mPowerStatsDataStorage.read(new PowerStatsDataStorage.DataElementReadCallback() {
@Override
public void onReadDataElement(byte[] data) {
try {
final ProtoInputStream pis =
new ProtoInputStream(new ByteArrayInputStream(data));
// TODO(b/166535853): ProtoOutputStream doesn't provide a method to write
// a byte array that already contains a serialized proto, so I have to
// deserialize, then re-serialize. This is computationally inefficient.
final PowerStatsData energyData = new PowerStatsData(pis);
energyData.toProto(pos);
if (DEBUG) energyData.print();
} catch (IOException e) {
Log.e(TAG, "Failed to write energy data to incident report.");
}
}
});
} catch (IOException e) {
Log.e(TAG, "Failed to write rail info to incident report.");
}
pos.flush();
}
public PowerStatsLogger(Context context, File dataStoragePath, String dataStorageFilename,
IPowerStatsHALWrapper powerStatsHALWrapper) {
super(Looper.getMainLooper());
mPowerStatsHALWrapper = powerStatsHALWrapper;
mPowerStatsDataStorage = new PowerStatsDataStorage(context, dataStoragePath,
dataStorageFilename);
}
}

View File

@ -0,0 +1,135 @@
/*
* 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.os.Binder;
import android.os.Environment;
import android.os.UserHandle;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;
import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
import com.android.server.powerstats.PowerStatsHALWrapper.PowerStatsHALWrapperImpl;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
/**
* This class provides a system service that estimates system power usage
* per subsystem (modem, wifi, gps, display, etc) and provides those power
* estimates to subscribers.
*/
public class PowerStatsService extends SystemService {
private static final String TAG = PowerStatsService.class.getSimpleName();
private static final boolean DEBUG = false;
private static final String DATA_STORAGE_SUBDIR = "powerstats";
private static final int DATA_STORAGE_VERSION = 0;
private static final String DATA_STORAGE_FILENAME = "log.powerstats." + DATA_STORAGE_VERSION;
private final Injector mInjector;
private Context mContext;
private IPowerStatsHALWrapper mPowerStatsHALWrapper;
private PowerStatsLogger mPowerStatsLogger;
private BatteryTrigger mBatteryTrigger;
private TimerTrigger mTimerTrigger;
@VisibleForTesting
static class Injector {
File createDataStoragePath() {
return new File(Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
DATA_STORAGE_SUBDIR);
}
String createDataStorageFilename() {
return DATA_STORAGE_FILENAME;
}
IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
return new PowerStatsHALWrapperImpl();
}
PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
return new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
powerStatsHALWrapper);
}
BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
return new BatteryTrigger(context, powerStatsLogger, true /* trigger enabled */);
}
TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
return new TimerTrigger(context, powerStatsLogger, true /* trigger enabled */);
}
}
private final class BinderService extends Binder {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
if (args.length > 0 && "--proto".equals(args[0])) {
mPowerStatsLogger.writeToFile(fd);
}
}
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_BOOT_COMPLETED) {
onSystemServiceReady();
}
}
@Override
public void onStart() {
publishBinderService(Context.POWER_STATS_SERVICE, new BinderService());
}
private void onSystemServiceReady() {
mPowerStatsHALWrapper = mInjector.createPowerStatsHALWrapperImpl();
if (mPowerStatsHALWrapper.initialize()) {
if (DEBUG) Log.d(TAG, "Starting PowerStatsService");
// Only start logger and triggers if initialization is successful.
mPowerStatsLogger = mInjector.createPowerStatsLogger(mContext,
mInjector.createDataStoragePath(), mInjector.createDataStorageFilename(),
mPowerStatsHALWrapper);
mBatteryTrigger = mInjector.createBatteryTrigger(mContext, mPowerStatsLogger);
mTimerTrigger = mInjector.createTimerTrigger(mContext, mPowerStatsLogger);
} else {
Log.e(TAG, "Initialization of PowerStatsHAL wrapper failed");
}
}
public PowerStatsService(Context context) {
this(context, new Injector());
}
@VisibleForTesting
public PowerStatsService(Context context, Injector injector) {
super(context);
mContext = context;
mInjector = injector;
}
}

View File

@ -0,0 +1,12 @@
{
"presubmit": [
{
"name": "FrameworksServicesTests",
"options": [
{
"include-filter": "com.android.server.powerstats"
}
]
}
]
}

View File

@ -0,0 +1,56 @@
/*
* 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.os.Handler;
import android.util.Log;
/**
* TimerTrigger sets a 60 second opportunistic timer using postDelayed.
* When the timer expires a message is sent to the PowerStatsLogger to
* read the rail energy data and log it to on-device storage.
*/
public final class TimerTrigger extends PowerStatsLogTrigger {
private static final String TAG = TimerTrigger.class.getSimpleName();
private static final boolean DEBUG = false;
// TODO(b/166689029): Make configurable through global settings.
private static final long LOG_PERIOD_MS = 60 * 1000;
private final Handler mHandler;
private Runnable mLogData = new Runnable() {
@Override
public void run() {
// Do not wake the device for these messages. Opportunistically log rail data every
// LOG_PERIOD_MS.
mHandler.postDelayed(mLogData, LOG_PERIOD_MS);
if (DEBUG) Log.d(TAG, "Received delayed message. Log rail data");
logPowerStatsData();
}
};
public TimerTrigger(Context context, PowerStatsLogger powerStatsLogger,
boolean triggerEnabled) {
super(context, powerStatsLogger);
mHandler = mContext.getMainThreadHandler();
if (triggerEnabled) {
mLogData.run();
}
}
}

View File

@ -37,6 +37,7 @@ cc_library_static {
"com_android_server_locksettings_SyntheticPasswordManager.cpp",
"com_android_server_net_NetworkStatsService.cpp",
"com_android_server_power_PowerManagerService.cpp",
"com_android_server_powerstats_PowerStatsService.cpp",
"com_android_server_security_VerityUtils.cpp",
"com_android_server_SerialService.cpp",
"com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",

View File

@ -0,0 +1,222 @@
/*
* 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.
*/
#define LOG_TAG "PowerStatsService"
#include <android/hardware/power/stats/1.0/IPowerStats.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
#include <log/log.h>
using android::hardware::hidl_vec;
using android::hardware::Return;
using android::hardware::power::stats::V1_0::EnergyData;
using android::hardware::power::stats::V1_0::RailInfo;
using android::hardware::power::stats::V1_0::Status;
static jclass class_railInfo;
static jmethodID method_railInfoInit;
static jclass class_energyData;
static jmethodID method_energyDataInit;
namespace android {
static std::mutex gPowerStatsHalMutex;
static sp<android::hardware::power::stats::V1_0::IPowerStats> gPowerStatsHalV1_0_ptr = nullptr;
static void deinitPowerStats() {
gPowerStatsHalV1_0_ptr = nullptr;
}
struct PowerStatsHalDeathRecipient : virtual public hardware::hidl_death_recipient {
virtual void serviceDied(uint64_t cookie,
const wp<android::hidl::base::V1_0::IBase> &who) override {
// The HAL just died. Reset all handles to HAL services.
std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
deinitPowerStats();
}
};
sp<PowerStatsHalDeathRecipient> gPowerStatsHalDeathRecipient = new PowerStatsHalDeathRecipient();
static bool connectToPowerStatsHal() {
if (gPowerStatsHalV1_0_ptr == nullptr) {
gPowerStatsHalV1_0_ptr = android::hardware::power::stats::V1_0::IPowerStats::getService();
if (gPowerStatsHalV1_0_ptr == nullptr) {
ALOGE("Unable to get power.stats HAL service.");
return false;
}
// Link death recipient to power.stats service handle
hardware::Return<bool> linked =
gPowerStatsHalV1_0_ptr->linkToDeath(gPowerStatsHalDeathRecipient, 0);
if (!linked.isOk()) {
ALOGE("Transaction error in linking to power.stats HAL death: %s",
linked.description().c_str());
deinitPowerStats();
return false;
} else if (!linked) {
ALOGW("Unable to link to power.stats HAL death notifications");
return false;
}
}
return true;
}
static bool checkResult(const Return<void> &ret, const char *function) {
if (!ret.isOk()) {
ALOGE("%s failed: requested HAL service not available. Description: %s", function,
ret.description().c_str());
if (ret.isDeadObject()) {
deinitPowerStats();
}
return false;
}
return true;
}
static jobjectArray nativeGetRailInfo(JNIEnv *env, jclass clazz) {
std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
if (!connectToPowerStatsHal()) {
ALOGE("nativeGetRailInfo failed to connect to power.stats HAL");
return nullptr;
}
hidl_vec<RailInfo> list;
Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&list](auto rails, auto status) {
if (status != Status::SUCCESS) {
ALOGW("Rail information is not available");
} else {
list = std::move(rails);
}
});
if (!checkResult(ret, __func__)) {
ALOGE("getRailInfo failed");
return nullptr;
} else {
jobjectArray railInfoArray = env->NewObjectArray(list.size(), class_railInfo, nullptr);
for (int i = 0; i < list.size(); i++) {
jstring railName = env->NewStringUTF(list[i].railName.c_str());
jstring subsysName = env->NewStringUTF(list[i].subsysName.c_str());
jobject railInfo = env->NewObject(class_railInfo, method_railInfoInit, list[i].index,
railName, subsysName, list[i].samplingRate);
env->SetObjectArrayElement(railInfoArray, i, railInfo);
env->DeleteLocalRef(railName);
env->DeleteLocalRef(subsysName);
env->DeleteLocalRef(railInfo);
}
return railInfoArray;
}
}
static jobjectArray nativeGetEnergyData(JNIEnv *env, jclass clazz) {
std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
if (!connectToPowerStatsHal()) {
ALOGE("nativeGetEnergy failed to connect to power.stats HAL");
}
hidl_vec<EnergyData> list;
Return<void> ret =
gPowerStatsHalV1_0_ptr->getEnergyData({}, [&list](auto energyData, auto status) {
if (status != Status::SUCCESS) {
ALOGW("getEnergyData is not supported");
} else {
list = std::move(energyData);
}
});
if (!checkResult(ret, __func__)) {
ALOGE("getEnergyData failed");
return nullptr;
} else {
jobjectArray energyDataArray = env->NewObjectArray(list.size(), class_energyData, nullptr);
for (int i = 0; i < list.size(); i++) {
jobject energyData = env->NewObject(class_energyData, method_energyDataInit,
list[i].index, list[i].timestamp, list[i].energy);
env->SetObjectArrayElement(energyDataArray, i, energyData);
env->DeleteLocalRef(energyData);
}
return energyDataArray;
}
}
static jboolean nativeInit(JNIEnv *env, jclass clazz) {
std::lock_guard<std::mutex> lock(gPowerStatsHalMutex);
jclass temp = env->FindClass("com/android/server/powerstats/PowerStatsData$RailInfo");
if (temp == nullptr) return false;
class_railInfo = (jclass)env->NewGlobalRef(temp);
if (class_railInfo == nullptr) return false;
method_railInfoInit =
env->GetMethodID(class_railInfo, "<init>", "(JLjava/lang/String;Ljava/lang/String;J)V");
if (method_railInfoInit == nullptr) return false;
temp = env->FindClass("com/android/server/powerstats/PowerStatsData$EnergyData");
if (temp == nullptr) return false;
class_energyData = (jclass)env->NewGlobalRef(temp);
if (class_energyData == nullptr) return false;
method_energyDataInit = env->GetMethodID(class_energyData, "<init>", "(JJJ)V");
if (method_energyDataInit == nullptr) return false;
bool rv = true;
if (!connectToPowerStatsHal()) {
ALOGE("nativeInit failed to connect to power.stats HAL");
rv = false;
} else {
Return<void> ret = gPowerStatsHalV1_0_ptr->getRailInfo([&rv](auto rails, auto status) {
if (status != Status::SUCCESS) {
ALOGE("nativeInit RailInfo is unavailable");
rv = false;
}
});
ret = gPowerStatsHalV1_0_ptr->getEnergyData({}, [&rv](auto energyData, auto status) {
if (status != Status::SUCCESS) {
ALOGE("nativeInit EnergyData is unavailable");
rv = false;
}
});
}
return rv;
}
static const JNINativeMethod method_table[] = {
{"nativeInit", "()Z", (void *)nativeInit},
{"nativeGetRailInfo", "()[Lcom/android/server/powerstats/PowerStatsData$RailInfo;",
(void *)nativeGetRailInfo},
{"nativeGetEnergyData", "()[Lcom/android/server/powerstats/PowerStatsData$EnergyData;",
(void *)nativeGetEnergyData},
};
int register_android_server_PowerStatsService(JNIEnv *env) {
return jniRegisterNativeMethods(env,
"com/android/server/powerstats/"
"PowerStatsHALWrapper$PowerStatsHALWrapperImpl",
method_table, NELEM(method_table));
}
}; // namespace android

View File

@ -29,6 +29,7 @@ int register_android_server_ConsumerIrService(JNIEnv *env);
int register_android_server_InputManager(JNIEnv* env);
int register_android_server_LightsService(JNIEnv* env);
int register_android_server_PowerManagerService(JNIEnv* env);
int register_android_server_PowerStatsService(JNIEnv* env);
int register_android_server_storage_AppFuse(JNIEnv* env);
int register_android_server_SerialService(JNIEnv* env);
int register_android_server_SystemServer(JNIEnv* env);
@ -82,6 +83,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
register_android_server_broadcastradio_BroadcastRadioService(env);
register_android_server_broadcastradio_Tuner(vm, env);
register_android_server_PowerManagerService(env);
register_android_server_PowerStatsService(env);
register_android_server_SerialService(env);
register_android_server_InputManager(env);
register_android_server_LightsService(env);

View File

@ -150,6 +150,7 @@ import com.android.server.policy.role.LegacyRoleResolutionPolicy;
import com.android.server.power.PowerManagerService;
import com.android.server.power.ShutdownThread;
import com.android.server.power.ThermalManagerService;
import com.android.server.powerstats.PowerStatsService;
import com.android.server.profcollect.ProfcollectForwardingService;
import com.android.server.recoverysystem.RecoverySystemService;
import com.android.server.restrictions.RestrictionsManagerService;
@ -761,6 +762,11 @@ public final class SystemServer {
mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
t.traceEnd();
t.traceBegin("StartPowerStatsService");
// Tracks rail data to be used for power statistics.
mSystemServiceManager.startService(PowerStatsService.class);
t.traceEnd();
// Activity manager runs the show.
t.traceBegin("StartActivityManager");
// TODO: Might need to move after migration to WM.

View File

@ -0,0 +1,271 @@
/*
* 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 static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import com.android.server.SystemService;
import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
import com.android.server.powerstats.nano.PowerStatsServiceProto;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Random;
/**
* Tests for {@link com.android.server.powerstats.PowerStatsService}.
*
* Build/Install/Run:
* atest FrameworksServicesTests:PowerStatsServiceTest
*/
public class PowerStatsServiceTest {
private static final String TAG = PowerStatsServiceTest.class.getSimpleName();
private static final String DATA_STORAGE_SUBDIR = "powerstatstest";
private static final String DATA_STORAGE_FILENAME = "test";
private static final String PROTO_OUTPUT_FILENAME = "powerstats.proto";
private static final String RAIL_NAME = "railname";
private static final String SUBSYS_NAME = "subsysname";
private static final int POWER_RAIL_COUNT = 8;
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
private PowerStatsService mService;
private File mDataStorageDir;
private TimerTrigger mTimerTrigger;
private PowerStatsLogger mPowerStatsLogger;
private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() {
@Override
File createDataStoragePath() {
mDataStorageDir = null;
try {
mDataStorageDir = Files.createTempDirectory(DATA_STORAGE_SUBDIR).toFile();
} catch (IOException e) {
fail("Could not create temp directory.");
}
return mDataStorageDir;
}
@Override
String createDataStorageFilename() {
return DATA_STORAGE_FILENAME;
}
@Override
IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
return new TestPowerStatsHALWrapper();
}
@Override
PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
String dataStorageFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
mPowerStatsLogger = new PowerStatsLogger(context, dataStoragePath, dataStorageFilename,
powerStatsHALWrapper);
return mPowerStatsLogger;
}
@Override
BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
return new BatteryTrigger(context, powerStatsLogger, false /* trigger enabled */);
}
@Override
TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) {
mTimerTrigger = new TimerTrigger(context, powerStatsLogger,
false /* trigger enabled */);
return mTimerTrigger;
}
};
public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
@Override
public PowerStatsData.RailInfo[] readRailInfo() {
PowerStatsData.RailInfo[] railInfoArray = new PowerStatsData.RailInfo[POWER_RAIL_COUNT];
for (int i = 0; i < POWER_RAIL_COUNT; i++) {
railInfoArray[i] = new PowerStatsData.RailInfo(i, RAIL_NAME + i, SUBSYS_NAME + i,
i);
}
return railInfoArray;
}
@Override
public PowerStatsData.EnergyData[] readEnergyData() {
PowerStatsData.EnergyData[] energyDataArray =
new PowerStatsData.EnergyData[POWER_RAIL_COUNT];
for (int i = 0; i < POWER_RAIL_COUNT; i++) {
energyDataArray[i] = new PowerStatsData.EnergyData(i, i, i);
}
return energyDataArray;
}
@Override
public boolean initialize() {
return true;
}
}
@Before
public void setUp() {
mService = new PowerStatsService(mContext, mInjector);
}
@Test
public void testWrittenPowerStatsHALDataMatchesReadIncidentReportData()
throws InterruptedException, IOException {
mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
// Write data to on-device storage.
mTimerTrigger.logPowerStatsData();
// The above call puts a message on a handler. Wait for
// it to be processed.
Thread.sleep(100);
// Write on-device storage to an incident report.
File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
FileOutputStream fos = new FileOutputStream(incidentReport);
mPowerStatsLogger.writeToFile(fos.getFD());
// Read the incident report in to a byte array.
FileInputStream fis = new FileInputStream(incidentReport);
byte[] fileContent = new byte[(int) incidentReport.length()];
fis.read(fileContent);
// Parse the incident data into a PowerStatsServiceProto object.
PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
// Validate the railInfo array matches what was written to on-device storage.
assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
for (int i = 0; i < pssProto.railInfo.length; i++) {
assertTrue(pssProto.railInfo[i].index == i);
assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
assertTrue(pssProto.railInfo[i].samplingRate == i);
}
// Validate the energyData array matches what was written to on-device storage.
assertTrue(pssProto.energyData.length == POWER_RAIL_COUNT);
for (int i = 0; i < pssProto.energyData.length; i++) {
assertTrue(pssProto.energyData[i].index == i);
assertTrue(pssProto.energyData[i].timestampMs == i);
assertTrue(pssProto.energyData[i].energyUws == i);
}
}
@Test
public void testCorruptOnDeviceStorage() throws IOException {
mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
// Generate random array of bytes to emulate corrupt data.
Random rd = new Random();
byte[] bytes = new byte[100];
rd.nextBytes(bytes);
// Store corrupt data in on-device storage. Add fake timestamp to filename
// to match format expected by FileRotator.
File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
onDeviceStorageFos.write(bytes);
onDeviceStorageFos.close();
// Write on-device storage to an incident report.
File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
// Read the incident report in to a byte array.
FileInputStream fis = new FileInputStream(incidentReport);
byte[] fileContent = new byte[(int) incidentReport.length()];
fis.read(fileContent);
// Parse the incident data into a PowerStatsServiceProto object.
PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
// Valid railInfo data is written to the incident report in the call to
// mPowerStatsLogger.writeToFile().
assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
for (int i = 0; i < pssProto.railInfo.length; i++) {
assertTrue(pssProto.railInfo[i].index == i);
assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
assertTrue(pssProto.railInfo[i].samplingRate == i);
}
// No energyData should be written to the incident report since it
// is all corrupt (random bytes generated above).
assertTrue(pssProto.energyData.length == 0);
}
@Test
public void testNotEnoughBytesAfterLengthField() throws IOException {
mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
// Create corrupt data.
// Length field is correct, but there is no data following the length.
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(ByteBuffer.allocate(4).putInt(50).array());
byte[] test = data.toByteArray();
// Store corrupt data in on-device storage. Add fake timestamp to filename
// to match format expected by FileRotator.
File onDeviceStorageFile = new File(mDataStorageDir, DATA_STORAGE_FILENAME + ".1234-2234");
FileOutputStream onDeviceStorageFos = new FileOutputStream(onDeviceStorageFile);
onDeviceStorageFos.write(data.toByteArray());
onDeviceStorageFos.close();
// Write on-device storage to an incident report.
File incidentReport = new File(mDataStorageDir, PROTO_OUTPUT_FILENAME);
FileOutputStream incidentReportFos = new FileOutputStream(incidentReport);
mPowerStatsLogger.writeToFile(incidentReportFos.getFD());
// Read the incident report in to a byte array.
FileInputStream fis = new FileInputStream(incidentReport);
byte[] fileContent = new byte[(int) incidentReport.length()];
fis.read(fileContent);
// Parse the incident data into a PowerStatsServiceProto object.
PowerStatsServiceProto pssProto = PowerStatsServiceProto.parseFrom(fileContent);
// Valid railInfo data is written to the incident report in the call to
// mPowerStatsLogger.writeToFile().
assertTrue(pssProto.railInfo.length == POWER_RAIL_COUNT);
for (int i = 0; i < pssProto.railInfo.length; i++) {
assertTrue(pssProto.railInfo[i].index == i);
assertTrue(pssProto.railInfo[i].railName.equals(RAIL_NAME + i));
assertTrue(pssProto.railInfo[i].subsysName.equals(SUBSYS_NAME + i));
assertTrue(pssProto.railInfo[i].samplingRate == i);
}
// No energyData should be written to the incident report since the
// input buffer had only length and no data.
assertTrue(pssProto.energyData.length == 0);
}
}

View File

@ -0,0 +1,10 @@
java_binary_host {
name: "PowerStatsServiceProtoParser",
manifest: "PowerStatsServiceProtoParser_manifest.txt",
srcs: [
"*.java",
],
static_libs: [
"platformprotos",
],
}

View File

@ -0,0 +1,95 @@
/*
* 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 java.io.FileInputStream;
import java.io.IOException;
/**
* This class implements a utility to parse ODPM data out
* of incident reports contained in bugreports. The data
* is output to STDOUT in csv format.
*/
public class PowerStatsServiceProtoParser {
private static void printRailInfo(PowerStatsServiceProto proto) {
String csvHeader = new String();
for (int i = 0; i < proto.getRailInfoCount(); i++) {
RailInfoProto railInfo = proto.getRailInfo(i);
csvHeader += "Index" + ","
+ "Timestamp" + ","
+ railInfo.getRailName() + "/" + railInfo.getSubsysName() + ",";
}
System.out.println(csvHeader);
}
private static void printEnergyData(PowerStatsServiceProto proto) {
int railInfoCount = proto.getRailInfoCount();
if (railInfoCount > 0) {
int energyDataCount = proto.getEnergyDataCount();
int energyDataSetCount = energyDataCount / railInfoCount;
for (int i = 0; i < energyDataSetCount; i++) {
String csvRow = new String();
for (int j = 0; j < railInfoCount; j++) {
EnergyDataProto energyData = proto.getEnergyData(i * railInfoCount + j);
csvRow += energyData.getIndex() + ","
+ energyData.getTimestampMs() + ","
+ energyData.getEnergyUws() + ",";
}
System.out.println(csvRow);
}
} else {
System.out.println("Error: railInfoCount is zero");
}
}
private static void generateCsvFile(String pathToIncidentReport) {
try {
IncidentReportProto irProto =
IncidentReportProto.parseFrom(new FileInputStream(pathToIncidentReport));
if (irProto.hasIncidentReport()) {
PowerStatsServiceProto pssProto = irProto.getIncidentReport();
printRailInfo(pssProto);
printEnergyData(pssProto);
} else {
System.out.println("Incident report not found. Exiting.");
}
} catch (IOException e) {
System.out.println("Unable to open incident report file: " + pathToIncidentReport);
System.out.println(e);
}
}
/**
* This is the entry point to parse the ODPM data out of incident reports.
* It requires one argument which is the path to the incident_report.proto
* file captured in a bugreport.
*
* @param args Path to incident_report.proto passed in from command line.
*/
public static void main(String[] args) {
if (args.length > 0) {
generateCsvFile(args[0]);
} else {
System.err.println("Usage: PowerStatsServiceProtoParser <incident_report.proto>");
System.err.println("Missing path to incident_report.proto. Exiting.");
System.exit(1);
}
}
}

View File

@ -0,0 +1 @@
Main-class: com.android.server.powerstats.PowerStatsServiceProtoParser