diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 98f78878ec15..bd11bc804c9c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -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 diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto index 5609b36703ae..9fed1b95f6c3 100644 --- a/core/proto/android/os/incident.proto +++ b/core/proto/android/os/incident.proto @@ -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, diff --git a/core/proto/android/server/powerstatsservice.proto b/core/proto/android/server/powerstatsservice.proto new file mode 100644 index 000000000000..c80524402335 --- /dev/null +++ b/core/proto/android/server/powerstatsservice.proto @@ -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; +} diff --git a/services/core/java/com/android/server/powerstats/BatteryTrigger.java b/services/core/java/com/android/server/powerstats/BatteryTrigger.java new file mode 100644 index 000000000000..c1f97f2685b5 --- /dev/null +++ b/services/core/java/com/android/server/powerstats/BatteryTrigger.java @@ -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); + } + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsData.java b/services/core/java/com/android/server/powerstats/PowerStatsData.java new file mode 100644 index 000000000000..755bd5fce45e --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsData.java @@ -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 mDataList; + + public PowerStatsData(ProtoInputStream pis) throws IOException { + mDataList = new ArrayList(); + unpackProto(pis); + } + + public PowerStatsData(Data[] data) { + mDataList = new ArrayList(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); + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java new file mode 100644 index 000000000000..84a6fc94598e --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java @@ -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); + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java new file mode 100644 index 000000000000..dc996a3e2d2e --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsHALWrapper.java @@ -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(); + } + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java new file mode 100644 index 000000000000..1754185ea71b --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsLogTrigger.java @@ -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; + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java new file mode 100644 index 000000000000..71a34a4174e5 --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java @@ -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); + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java new file mode 100644 index 000000000000..fd609c14276c --- /dev/null +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -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; + } +} diff --git a/services/core/java/com/android/server/powerstats/TEST_MAPPING b/services/core/java/com/android/server/powerstats/TEST_MAPPING new file mode 100644 index 000000000000..79224a580cd8 --- /dev/null +++ b/services/core/java/com/android/server/powerstats/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.powerstats" + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/powerstats/TimerTrigger.java b/services/core/java/com/android/server/powerstats/TimerTrigger.java new file mode 100644 index 000000000000..a9bee8bec77f --- /dev/null +++ b/services/core/java/com/android/server/powerstats/TimerTrigger.java @@ -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(); + } + } +} diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index d84f9d1a7dad..1b649fda1976 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -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", diff --git a/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp new file mode 100644 index 000000000000..5eb6b7343945 --- /dev/null +++ b/services/core/jni/com_android_server_powerstats_PowerStatsService.cpp @@ -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 +#include +#include + +#include + +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 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 &who) override { + // The HAL just died. Reset all handles to HAL services. + std::lock_guard lock(gPowerStatsHalMutex); + deinitPowerStats(); + } +}; + +sp 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 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 &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 lock(gPowerStatsHalMutex); + + if (!connectToPowerStatsHal()) { + ALOGE("nativeGetRailInfo failed to connect to power.stats HAL"); + return nullptr; + } + + hidl_vec list; + Return 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 lock(gPowerStatsHalMutex); + + if (!connectToPowerStatsHal()) { + ALOGE("nativeGetEnergy failed to connect to power.stats HAL"); + } + + hidl_vec list; + Return 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 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, "", "(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, "", "(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 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 diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 5df1adafed5e..e7f6db959060 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -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); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 97ae505b4fcf..ddd23778884a 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -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. diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java new file mode 100644 index 000000000000..3221a4d4f12c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -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); + } +} diff --git a/tools/powerstats/Android.bp b/tools/powerstats/Android.bp new file mode 100644 index 000000000000..af41144167a9 --- /dev/null +++ b/tools/powerstats/Android.bp @@ -0,0 +1,10 @@ +java_binary_host { + name: "PowerStatsServiceProtoParser", + manifest: "PowerStatsServiceProtoParser_manifest.txt", + srcs: [ + "*.java", + ], + static_libs: [ + "platformprotos", + ], +} diff --git a/tools/powerstats/PowerStatsServiceProtoParser.java b/tools/powerstats/PowerStatsServiceProtoParser.java new file mode 100644 index 000000000000..8ab302a50662 --- /dev/null +++ b/tools/powerstats/PowerStatsServiceProtoParser.java @@ -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 "); + System.err.println("Missing path to incident_report.proto. Exiting."); + System.exit(1); + } + } +} diff --git a/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt new file mode 100644 index 000000000000..5df12118ce80 --- /dev/null +++ b/tools/powerstats/PowerStatsServiceProtoParser_manifest.txt @@ -0,0 +1 @@ +Main-class: com.android.server.powerstats.PowerStatsServiceProtoParser