Add state residency logging to power stats
Test: atest FrameworksServicesTests:PowerStatsServiceTest Bug: 175724197 Change-Id: I4ba7af3b9a895ecdc9f8c16cd371e5582458d212
This commit is contained in:
parent
ff001a1523
commit
723784a055
@ -521,6 +521,11 @@ message IncidentProto {
|
||||
(section).args = "power_stats --proto model"
|
||||
];
|
||||
|
||||
optional com.android.server.powerstats.PowerStatsServiceResidencyProto powerstats_residency = 3056 [
|
||||
(section).type = SECTION_DUMPSYS,
|
||||
(section).args = "power_stats --proto residency"
|
||||
];
|
||||
|
||||
// Dumps in text format (on userdebug and eng builds only): 4000 ~ 4999
|
||||
optional android.util.TextDumpProto textdump_wifi = 4000 [
|
||||
(section).type = SECTION_TEXT_DUMPSYS,
|
||||
|
@ -40,6 +40,16 @@ message IncidentReportModelProto {
|
||||
optional PowerStatsServiceModelProto incident_report = 3055;
|
||||
}
|
||||
|
||||
/**
|
||||
* IncidentReportResidencyProto is used only in the parsing tool located
|
||||
* in frameworks/base/tools which is used to parse this data out of
|
||||
* incident reports.
|
||||
*/
|
||||
message IncidentReportResidencyProto {
|
||||
/** Section number matches that in incident.proto */
|
||||
optional PowerStatsServiceResidencyProto incident_report = 3056;
|
||||
}
|
||||
|
||||
/**
|
||||
* EnergyConsumer (model) data is exposed by the PowerStats HAL. This data
|
||||
* represents modeled energy consumption estimates and is provided per
|
||||
@ -62,6 +72,99 @@ message PowerStatsServiceMeterProto {
|
||||
repeated EnergyMeasurementProto energy_measurement = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* A PowerEntity is defined as a platform subsystem, peripheral, or power domain
|
||||
* that impacts the total device power consumption. PowerEntityInfo is
|
||||
* information related to each power entity. Each PowerEntity may reside in one
|
||||
* of multiple states. It may also transition from one state to another.
|
||||
* StateResidency is defined as an accumulation of time that a PowerEntity
|
||||
* resided in each of its possible states, the number of times that each state
|
||||
* was entered, and a timestamp corresponding to the last time that state was
|
||||
* entered.
|
||||
*/
|
||||
message PowerStatsServiceResidencyProto {
|
||||
repeated PowerEntityInfoProto power_entity_info = 1;
|
||||
repeated StateResidencyResultProto state_residency_result = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the possible states for a particular PowerEntity.
|
||||
*/
|
||||
message StateInfoProto {
|
||||
/**
|
||||
* Unique (for a given PowerEntityInfo) ID of this StateInfo
|
||||
*/
|
||||
optional int32 state_id = 1;
|
||||
/**
|
||||
* Unique (for a given PowerEntityInfo) name of the state. Vendor/device specific.
|
||||
* Opaque to framework
|
||||
*/
|
||||
optional string state_name = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* A PowerEntity is defined as a platform subsystem, peripheral, or power domain
|
||||
* that impacts the total device power consumption. PowerEntityInfo is
|
||||
* information about a PowerEntity. It includes an array of information about
|
||||
* each possible state of the PowerEntity.
|
||||
*/
|
||||
message PowerEntityInfoProto {
|
||||
/**
|
||||
* Unique ID of this PowerEntityInfo
|
||||
*/
|
||||
optional int32 power_entity_id = 1;
|
||||
/**
|
||||
* Unique name of the PowerEntity. Vendor/device specific. Opaque to framework
|
||||
*/
|
||||
optional string power_entity_name = 2;
|
||||
/**
|
||||
* List of states that the PowerEntity may reside in
|
||||
*/
|
||||
repeated StateInfoProto states = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* StateResidency is defined as an accumulation of time that a PowerEntity
|
||||
* resided in each of its possible states, the number of times that each state
|
||||
* was entered, and a timestamp corresponding to the last time that state was
|
||||
* entered. Data is accumulated starting at device boot.
|
||||
*/
|
||||
message StateResidencyProto {
|
||||
/**
|
||||
* ID of the state associated with this residency
|
||||
*/
|
||||
optional int32 state_id = 1;
|
||||
/**
|
||||
* Total time in milliseconds that the corresponding PowerEntity resided
|
||||
* in this state since boot
|
||||
*/
|
||||
optional int64 total_time_in_state_ms = 2;
|
||||
/**
|
||||
* Total number of times that the state was entered since boot
|
||||
*/
|
||||
optional int64 total_state_entry_count = 3;
|
||||
/**
|
||||
* Last time this state was entered. Time in milliseconds since boot
|
||||
*/
|
||||
optional int64 last_entry_timestamp_ms = 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* A StateResidencyResult is an array of StateResidencies for a particular
|
||||
* PowerEntity. The StateResidencyResult can be matched to its corresponding
|
||||
* PowerEntityInfo through the power_entity_id field.
|
||||
*/
|
||||
message StateResidencyResultProto {
|
||||
/**
|
||||
* ID of the PowerEntity associated with this result
|
||||
*/
|
||||
optional int32 power_entity_id = 1;
|
||||
/**
|
||||
* Residency for each state in the PowerEntity's state space
|
||||
*/
|
||||
repeated StateResidencyProto state_residency_data = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Energy consumer ID:
|
||||
* A list of default subsystems for which energy consumption estimates
|
||||
|
@ -218,7 +218,7 @@ public class PowerStatsDataStorage {
|
||||
* array and written to on-device storage.
|
||||
*/
|
||||
public void write(byte[] data) {
|
||||
if (data.length > 0) {
|
||||
if (data != null && data.length > 0) {
|
||||
mLock.lock();
|
||||
|
||||
long currentTimeMillis = System.currentTimeMillis();
|
||||
|
@ -20,6 +20,8 @@ import android.content.Context;
|
||||
import android.hardware.power.stats.ChannelInfo;
|
||||
import android.hardware.power.stats.EnergyConsumerResult;
|
||||
import android.hardware.power.stats.EnergyMeasurement;
|
||||
import android.hardware.power.stats.PowerEntityInfo;
|
||||
import android.hardware.power.stats.StateResidencyResult;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
@ -32,6 +34,8 @@ import com.android.server.powerstats.ProtoStreamUtils.ChannelInfoUtils;
|
||||
import com.android.server.powerstats.ProtoStreamUtils.EnergyConsumerIdUtils;
|
||||
import com.android.server.powerstats.ProtoStreamUtils.EnergyConsumerResultUtils;
|
||||
import com.android.server.powerstats.ProtoStreamUtils.EnergyMeasurementUtils;
|
||||
import com.android.server.powerstats.ProtoStreamUtils.PowerEntityInfoUtils;
|
||||
import com.android.server.powerstats.ProtoStreamUtils.StateResidencyResultUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
@ -42,8 +46,8 @@ import java.io.IOException;
|
||||
* PowerStatsLogger is responsible for logging model and meter 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 writeModelDataToFile or writeMeterDataToFile with a file descriptor
|
||||
* that points to the output file.
|
||||
* dumped to file by calling writeModelDataToFile, writeMeterDataToFile, or writeResidencyDataToFile
|
||||
* with a file descriptor that points to the output file.
|
||||
*/
|
||||
public final class PowerStatsLogger extends Handler {
|
||||
private static final String TAG = PowerStatsLogger.class.getSimpleName();
|
||||
@ -52,6 +56,7 @@ public final class PowerStatsLogger extends Handler {
|
||||
|
||||
private final PowerStatsDataStorage mPowerStatsMeterStorage;
|
||||
private final PowerStatsDataStorage mPowerStatsModelStorage;
|
||||
private final PowerStatsDataStorage mPowerStatsResidencyStorage;
|
||||
private final IPowerStatsHALWrapper mPowerStatsHALWrapper;
|
||||
|
||||
@Override
|
||||
@ -73,6 +78,13 @@ public final class PowerStatsLogger extends Handler {
|
||||
mPowerStatsModelStorage.write(
|
||||
EnergyConsumerResultUtils.getProtoBytes(energyConsumerResults));
|
||||
if (DEBUG) EnergyConsumerResultUtils.print(energyConsumerResults);
|
||||
|
||||
// Log state residency data.
|
||||
StateResidencyResult[] stateResidencyResults =
|
||||
mPowerStatsHALWrapper.getStateResidency(new int[0]);
|
||||
mPowerStatsResidencyStorage.write(
|
||||
StateResidencyResultUtils.getProtoBytes(stateResidencyResults));
|
||||
if (DEBUG) StateResidencyResultUtils.print(stateResidencyResults);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -159,13 +171,57 @@ public final class PowerStatsLogger extends Handler {
|
||||
pos.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes residency data stored in PowerStatsDataStorage to a file descriptor.
|
||||
*
|
||||
* @param fd FileDescriptor where residency data stored in PowerStatsDataStorage is written.
|
||||
* Data is written in protobuf format as defined by powerstatsservice.proto.
|
||||
*/
|
||||
public void writeResidencyDataToFile(FileDescriptor fd) {
|
||||
if (DEBUG) Slog.d(TAG, "Writing residency data to file");
|
||||
|
||||
final ProtoOutputStream pos = new ProtoOutputStream(fd);
|
||||
|
||||
try {
|
||||
PowerEntityInfo[] powerEntityInfo = mPowerStatsHALWrapper.getPowerEntityInfo();
|
||||
PowerEntityInfoUtils.packProtoMessage(powerEntityInfo, pos);
|
||||
if (DEBUG) PowerEntityInfoUtils.print(powerEntityInfo);
|
||||
|
||||
mPowerStatsResidencyStorage.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.
|
||||
StateResidencyResult[] stateResidencyResult =
|
||||
StateResidencyResultUtils.unpackProtoMessage(data);
|
||||
StateResidencyResultUtils.packProtoMessage(stateResidencyResult, pos);
|
||||
if (DEBUG) StateResidencyResultUtils.print(stateResidencyResult);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to write residency data to incident report.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to write residency data to incident report.");
|
||||
}
|
||||
|
||||
pos.flush();
|
||||
}
|
||||
|
||||
public PowerStatsLogger(Context context, File dataStoragePath, String meterFilename,
|
||||
String modelFilename, IPowerStatsHALWrapper powerStatsHALWrapper) {
|
||||
String modelFilename, String residencyFilename,
|
||||
IPowerStatsHALWrapper powerStatsHALWrapper) {
|
||||
super(Looper.getMainLooper());
|
||||
mPowerStatsHALWrapper = powerStatsHALWrapper;
|
||||
mPowerStatsMeterStorage = new PowerStatsDataStorage(context, dataStoragePath,
|
||||
meterFilename);
|
||||
mPowerStatsModelStorage = new PowerStatsDataStorage(context, dataStoragePath,
|
||||
modelFilename);
|
||||
mPowerStatsResidencyStorage = new PowerStatsDataStorage(context, dataStoragePath,
|
||||
residencyFilename);
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,8 @@ public class PowerStatsService extends SystemService {
|
||||
private static final int DATA_STORAGE_VERSION = 0;
|
||||
private static final String METER_FILENAME = "log.powerstats.meter." + DATA_STORAGE_VERSION;
|
||||
private static final String MODEL_FILENAME = "log.powerstats.model." + DATA_STORAGE_VERSION;
|
||||
private static final String RESIDENCY_FILENAME =
|
||||
"log.powerstats.residency." + DATA_STORAGE_VERSION;
|
||||
|
||||
private final Injector mInjector;
|
||||
|
||||
@ -76,15 +78,19 @@ public class PowerStatsService extends SystemService {
|
||||
return MODEL_FILENAME;
|
||||
}
|
||||
|
||||
String createResidencyFilename() {
|
||||
return RESIDENCY_FILENAME;
|
||||
}
|
||||
|
||||
IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
|
||||
return PowerStatsHALWrapper.getPowerStatsHalImpl();
|
||||
}
|
||||
|
||||
PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
|
||||
String meterFilename, String modelFilename,
|
||||
String meterFilename, String modelFilename, String residencyFilename,
|
||||
IPowerStatsHALWrapper powerStatsHALWrapper) {
|
||||
return new PowerStatsLogger(context, dataStoragePath, meterFilename,
|
||||
modelFilename, powerStatsHALWrapper);
|
||||
modelFilename, residencyFilename, powerStatsHALWrapper);
|
||||
}
|
||||
|
||||
BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) {
|
||||
@ -109,6 +115,8 @@ public class PowerStatsService extends SystemService {
|
||||
mPowerStatsLogger.writeModelDataToFile(fd);
|
||||
} else if ("meter".equals(args[1])) {
|
||||
mPowerStatsLogger.writeMeterDataToFile(fd);
|
||||
} else if ("residency".equals(args[1])) {
|
||||
mPowerStatsLogger.writeResidencyDataToFile(fd);
|
||||
}
|
||||
} else if (args.length == 0) {
|
||||
pw.println("PowerStatsService dumpsys: available PowerEntityInfos");
|
||||
@ -148,7 +156,8 @@ public class PowerStatsService extends SystemService {
|
||||
// Only start logger and triggers if initialization is successful.
|
||||
mPowerStatsLogger = mInjector.createPowerStatsLogger(mContext,
|
||||
mInjector.createDataStoragePath(), mInjector.createMeterFilename(),
|
||||
mInjector.createModelFilename(), mPowerStatsHALWrapper);
|
||||
mInjector.createModelFilename(), mInjector.createResidencyFilename(),
|
||||
mPowerStatsHALWrapper);
|
||||
mBatteryTrigger = mInjector.createBatteryTrigger(mContext, mPowerStatsLogger);
|
||||
mTimerTrigger = mInjector.createTimerTrigger(mContext, mPowerStatsLogger);
|
||||
} else {
|
||||
|
@ -20,6 +20,8 @@ import android.hardware.power.stats.ChannelInfo;
|
||||
import android.hardware.power.stats.EnergyConsumerResult;
|
||||
import android.hardware.power.stats.EnergyMeasurement;
|
||||
import android.hardware.power.stats.PowerEntityInfo;
|
||||
import android.hardware.power.stats.StateInfo;
|
||||
import android.hardware.power.stats.StateResidency;
|
||||
import android.hardware.power.stats.StateResidencyResult;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
@ -45,6 +47,29 @@ public class ProtoStreamUtils {
|
||||
private static final String TAG = ProtoStreamUtils.class.getSimpleName();
|
||||
|
||||
static class PowerEntityInfoUtils {
|
||||
public static void packProtoMessage(PowerEntityInfo[] powerEntityInfo,
|
||||
ProtoOutputStream pos) {
|
||||
if (powerEntityInfo == null) return;
|
||||
|
||||
for (int i = 0; i < powerEntityInfo.length; i++) {
|
||||
long peiToken = pos.start(PowerStatsServiceResidencyProto.POWER_ENTITY_INFO);
|
||||
pos.write(PowerEntityInfoProto.POWER_ENTITY_ID, powerEntityInfo[i].powerEntityId);
|
||||
pos.write(PowerEntityInfoProto.POWER_ENTITY_NAME,
|
||||
powerEntityInfo[i].powerEntityName);
|
||||
if (powerEntityInfo[i].states != null) {
|
||||
final int statesLength = powerEntityInfo[i].states.length;
|
||||
for (int j = 0; j < statesLength; j++) {
|
||||
final StateInfo state = powerEntityInfo[i].states[j];
|
||||
long siToken = pos.start(PowerEntityInfoProto.STATES);
|
||||
pos.write(StateInfoProto.STATE_ID, state.stateId);
|
||||
pos.write(StateInfoProto.STATE_NAME, state.stateName);
|
||||
pos.end(siToken);
|
||||
}
|
||||
}
|
||||
pos.end(peiToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static void print(PowerEntityInfo[] powerEntityInfo) {
|
||||
if (powerEntityInfo == null) return;
|
||||
|
||||
@ -77,6 +102,144 @@ public class ProtoStreamUtils {
|
||||
}
|
||||
|
||||
static class StateResidencyResultUtils {
|
||||
public static byte[] getProtoBytes(StateResidencyResult[] stateResidencyResult) {
|
||||
ProtoOutputStream pos = new ProtoOutputStream();
|
||||
packProtoMessage(stateResidencyResult, pos);
|
||||
return pos.getBytes();
|
||||
}
|
||||
|
||||
public static void packProtoMessage(StateResidencyResult[] stateResidencyResult,
|
||||
ProtoOutputStream pos) {
|
||||
if (stateResidencyResult == null) return;
|
||||
|
||||
for (int i = 0; i < stateResidencyResult.length; i++) {
|
||||
final int stateLength = stateResidencyResult[i].stateResidencyData.length;
|
||||
long srrToken = pos.start(PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT);
|
||||
pos.write(StateResidencyResultProto.POWER_ENTITY_ID,
|
||||
stateResidencyResult[i].powerEntityId);
|
||||
for (int j = 0; j < stateLength; j++) {
|
||||
final StateResidency stateResidencyData =
|
||||
stateResidencyResult[i].stateResidencyData[j];
|
||||
long srdToken = pos.start(StateResidencyResultProto.STATE_RESIDENCY_DATA);
|
||||
pos.write(StateResidencyProto.STATE_ID, stateResidencyData.stateId);
|
||||
pos.write(StateResidencyProto.TOTAL_TIME_IN_STATE_MS,
|
||||
stateResidencyData.totalTimeInStateMs);
|
||||
pos.write(StateResidencyProto.TOTAL_STATE_ENTRY_COUNT,
|
||||
stateResidencyData.totalStateEntryCount);
|
||||
pos.write(StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS,
|
||||
stateResidencyData.lastEntryTimestampMs);
|
||||
pos.end(srdToken);
|
||||
}
|
||||
pos.end(srrToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static StateResidencyResult[] unpackProtoMessage(byte[] data) throws IOException {
|
||||
final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
|
||||
List<StateResidencyResult> stateResidencyResultList =
|
||||
new ArrayList<StateResidencyResult>();
|
||||
while (true) {
|
||||
try {
|
||||
int nextField = pis.nextField();
|
||||
StateResidencyResult stateResidencyResult = new StateResidencyResult();
|
||||
|
||||
if (nextField == (int) PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT) {
|
||||
long token =
|
||||
pis.start(PowerStatsServiceResidencyProto.STATE_RESIDENCY_RESULT);
|
||||
stateResidencyResultList.add(unpackStateResidencyResultProto(pis));
|
||||
pis.end(token);
|
||||
} else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
|
||||
return stateResidencyResultList.toArray(
|
||||
new StateResidencyResult[stateResidencyResultList.size()]);
|
||||
} else {
|
||||
Slog.e(TAG, "Unhandled field in PowerStatsServiceResidencyProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
}
|
||||
} catch (WireTypeMismatchException wtme) {
|
||||
Slog.e(TAG, "Wire Type mismatch in PowerStatsServiceResidencyProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static StateResidencyResult unpackStateResidencyResultProto(ProtoInputStream pis)
|
||||
throws IOException {
|
||||
StateResidencyResult stateResidencyResult = new StateResidencyResult();
|
||||
List<StateResidency> stateResidencyList = new ArrayList<StateResidency>();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
switch (pis.nextField()) {
|
||||
case (int) StateResidencyResultProto.POWER_ENTITY_ID:
|
||||
stateResidencyResult.powerEntityId =
|
||||
pis.readInt(StateResidencyResultProto.POWER_ENTITY_ID);
|
||||
break;
|
||||
|
||||
case (int) StateResidencyResultProto.STATE_RESIDENCY_DATA:
|
||||
long token = pis.start(StateResidencyResultProto.STATE_RESIDENCY_DATA);
|
||||
stateResidencyList.add(unpackStateResidencyProto(pis));
|
||||
pis.end(token);
|
||||
break;
|
||||
|
||||
case ProtoInputStream.NO_MORE_FIELDS:
|
||||
stateResidencyResult.stateResidencyData = stateResidencyList.toArray(
|
||||
new StateResidency[stateResidencyList.size()]);
|
||||
return stateResidencyResult;
|
||||
|
||||
default:
|
||||
Slog.e(TAG, "Unhandled field in StateResidencyResultProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
break;
|
||||
}
|
||||
} catch (WireTypeMismatchException wtme) {
|
||||
Slog.e(TAG, "Wire Type mismatch in StateResidencyResultProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static StateResidency unpackStateResidencyProto(ProtoInputStream pis)
|
||||
throws IOException {
|
||||
StateResidency stateResidency = new StateResidency();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
switch (pis.nextField()) {
|
||||
case (int) StateResidencyProto.STATE_ID:
|
||||
stateResidency.stateId = pis.readInt(StateResidencyProto.STATE_ID);
|
||||
break;
|
||||
|
||||
case (int) StateResidencyProto.TOTAL_TIME_IN_STATE_MS:
|
||||
stateResidency.totalTimeInStateMs =
|
||||
pis.readLong(StateResidencyProto.TOTAL_TIME_IN_STATE_MS);
|
||||
break;
|
||||
|
||||
case (int) StateResidencyProto.TOTAL_STATE_ENTRY_COUNT:
|
||||
stateResidency.totalStateEntryCount =
|
||||
pis.readLong(StateResidencyProto.TOTAL_STATE_ENTRY_COUNT);
|
||||
break;
|
||||
|
||||
case (int) StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS:
|
||||
stateResidency.lastEntryTimestampMs =
|
||||
pis.readLong(StateResidencyProto.LAST_ENTRY_TIMESTAMP_MS);
|
||||
break;
|
||||
|
||||
case ProtoInputStream.NO_MORE_FIELDS:
|
||||
return stateResidency;
|
||||
|
||||
default:
|
||||
Slog.e(TAG, "Unhandled field in StateResidencyProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (WireTypeMismatchException wtme) {
|
||||
Slog.e(TAG, "Wire Type mismatch in StateResidencyProto: "
|
||||
+ ProtoUtils.currentFieldToString(pis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void print(StateResidencyResult[] stateResidencyResult) {
|
||||
if (stateResidencyResult == null) return;
|
||||
|
||||
@ -98,17 +261,14 @@ public class ProtoStreamUtils {
|
||||
|
||||
static class ChannelInfoUtils {
|
||||
public static void packProtoMessage(ChannelInfo[] channelInfo, ProtoOutputStream pos) {
|
||||
long token;
|
||||
|
||||
if (channelInfo == null) return;
|
||||
|
||||
for (int i = 0; i < channelInfo.length; i++) {
|
||||
token = pos.start(PowerStatsServiceMeterProto.CHANNEL_INFO);
|
||||
long token = pos.start(PowerStatsServiceMeterProto.CHANNEL_INFO);
|
||||
pos.write(ChannelInfoProto.CHANNEL_ID, channelInfo[i].channelId);
|
||||
pos.write(ChannelInfoProto.CHANNEL_NAME, channelInfo[i].channelName);
|
||||
pos.end(token);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void print(ChannelInfo[] channelInfo) {
|
||||
@ -139,12 +299,10 @@ public class ProtoStreamUtils {
|
||||
|
||||
public static void packProtoMessage(EnergyMeasurement[] energyMeasurement,
|
||||
ProtoOutputStream pos) {
|
||||
long token;
|
||||
|
||||
if (energyMeasurement == null) return;
|
||||
|
||||
for (int i = 0; i < energyMeasurement.length; i++) {
|
||||
token = pos.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
|
||||
long token = pos.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
|
||||
pos.write(EnergyMeasurementProto.CHANNEL_ID, energyMeasurement[i].channelId);
|
||||
pos.write(EnergyMeasurementProto.TIMESTAMP_MS, energyMeasurement[i].timestampMs);
|
||||
pos.write(EnergyMeasurementProto.ENERGY_UWS, energyMeasurement[i].energyUWs);
|
||||
@ -155,7 +313,6 @@ public class ProtoStreamUtils {
|
||||
public static EnergyMeasurement[] unpackProtoMessage(byte[] data) throws IOException {
|
||||
final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
|
||||
List<EnergyMeasurement> energyMeasurementList = new ArrayList<EnergyMeasurement>();
|
||||
long token;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@ -163,8 +320,8 @@ public class ProtoStreamUtils {
|
||||
EnergyMeasurement energyMeasurement = new EnergyMeasurement();
|
||||
|
||||
if (nextField == (int) PowerStatsServiceMeterProto.ENERGY_MEASUREMENT) {
|
||||
token = pis.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
|
||||
energyMeasurementList.add(unpackProtoMessage(pis));
|
||||
long token = pis.start(PowerStatsServiceMeterProto.ENERGY_MEASUREMENT);
|
||||
energyMeasurementList.add(unpackEnergyMeasurementProto(pis));
|
||||
pis.end(token);
|
||||
} else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
|
||||
return energyMeasurementList.toArray(
|
||||
@ -180,7 +337,7 @@ public class ProtoStreamUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static EnergyMeasurement unpackProtoMessage(ProtoInputStream pis)
|
||||
private static EnergyMeasurement unpackEnergyMeasurementProto(ProtoInputStream pis)
|
||||
throws IOException {
|
||||
EnergyMeasurement energyMeasurement = new EnergyMeasurement();
|
||||
|
||||
@ -230,12 +387,10 @@ public class ProtoStreamUtils {
|
||||
|
||||
static class EnergyConsumerIdUtils {
|
||||
public static void packProtoMessage(int[] energyConsumerId, ProtoOutputStream pos) {
|
||||
long token;
|
||||
|
||||
if (energyConsumerId == null) return;
|
||||
|
||||
for (int i = 0; i < energyConsumerId.length; i++) {
|
||||
token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_ID);
|
||||
long token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_ID);
|
||||
pos.write(EnergyConsumerIdProto.ENERGY_CONSUMER_ID, energyConsumerId[i]);
|
||||
pos.end(token);
|
||||
}
|
||||
@ -267,12 +422,10 @@ public class ProtoStreamUtils {
|
||||
|
||||
public static void packProtoMessage(EnergyConsumerResult[] energyConsumerResult,
|
||||
ProtoOutputStream pos) {
|
||||
long token;
|
||||
|
||||
if (energyConsumerResult == null) return;
|
||||
|
||||
for (int i = 0; i < energyConsumerResult.length; i++) {
|
||||
token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
|
||||
long token = pos.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
|
||||
pos.write(EnergyConsumerResultProto.ENERGY_CONSUMER_ID,
|
||||
energyConsumerResult[i].energyConsumerId);
|
||||
pos.write(EnergyConsumerResultProto.TIMESTAMP_MS,
|
||||
@ -286,16 +439,14 @@ public class ProtoStreamUtils {
|
||||
final ProtoInputStream pis = new ProtoInputStream(new ByteArrayInputStream(data));
|
||||
List<EnergyConsumerResult> energyConsumerResultList =
|
||||
new ArrayList<EnergyConsumerResult>();
|
||||
long token;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
int nextField = pis.nextField();
|
||||
EnergyConsumerResult energyConsumerResult = new EnergyConsumerResult();
|
||||
|
||||
if (nextField == (int) PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT) {
|
||||
token = pis.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
|
||||
energyConsumerResultList.add(unpackProtoMessage(pis));
|
||||
long token = pis.start(PowerStatsServiceModelProto.ENERGY_CONSUMER_RESULT);
|
||||
energyConsumerResultList.add(unpackEnergyConsumerResultProto(pis));
|
||||
pis.end(token);
|
||||
} else if (nextField == ProtoInputStream.NO_MORE_FIELDS) {
|
||||
return energyConsumerResultList.toArray(
|
||||
@ -311,7 +462,7 @@ public class ProtoStreamUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static EnergyConsumerResult unpackProtoMessage(ProtoInputStream pis)
|
||||
private static EnergyConsumerResult unpackEnergyConsumerResultProto(ProtoInputStream pis)
|
||||
throws IOException {
|
||||
EnergyConsumerResult energyConsumerResult = new EnergyConsumerResult();
|
||||
|
||||
|
@ -32,8 +32,13 @@ import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import com.android.server.SystemService;
|
||||
import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper;
|
||||
import com.android.server.powerstats.nano.PowerEntityInfoProto;
|
||||
import com.android.server.powerstats.nano.PowerStatsServiceMeterProto;
|
||||
import com.android.server.powerstats.nano.PowerStatsServiceModelProto;
|
||||
import com.android.server.powerstats.nano.PowerStatsServiceResidencyProto;
|
||||
import com.android.server.powerstats.nano.StateInfoProto;
|
||||
import com.android.server.powerstats.nano.StateResidencyProto;
|
||||
import com.android.server.powerstats.nano.StateResidencyResultProto;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -58,6 +63,7 @@ public class PowerStatsServiceTest {
|
||||
private static final String DATA_STORAGE_SUBDIR = "powerstatstest";
|
||||
private static final String METER_FILENAME = "metertest";
|
||||
private static final String MODEL_FILENAME = "modeltest";
|
||||
private static final String RESIDENCY_FILENAME = "residencytest";
|
||||
private static final String PROTO_OUTPUT_FILENAME = "powerstats.proto";
|
||||
private static final String CHANNEL_NAME = "channelname";
|
||||
private static final String POWER_ENTITY_NAME = "powerentityinfo";
|
||||
@ -98,6 +104,11 @@ public class PowerStatsServiceTest {
|
||||
return MODEL_FILENAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
String createResidencyFilename() {
|
||||
return RESIDENCY_FILENAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
IPowerStatsHALWrapper createPowerStatsHALWrapperImpl() {
|
||||
return new TestPowerStatsHALWrapper();
|
||||
@ -105,10 +116,10 @@ public class PowerStatsServiceTest {
|
||||
|
||||
@Override
|
||||
PowerStatsLogger createPowerStatsLogger(Context context, File dataStoragePath,
|
||||
String meterFilename, String modelFilename,
|
||||
String meterFilename, String modelFilename, String residencyFilename,
|
||||
IPowerStatsHALWrapper powerStatsHALWrapper) {
|
||||
mPowerStatsLogger = new PowerStatsLogger(context, dataStoragePath, meterFilename,
|
||||
modelFilename, powerStatsHALWrapper);
|
||||
modelFilename, residencyFilename, powerStatsHALWrapper);
|
||||
return mPowerStatsLogger;
|
||||
}
|
||||
|
||||
@ -137,7 +148,7 @@ public class PowerStatsServiceTest {
|
||||
for (int j = 0; j < powerEntityInfoList[i].states.length; j++) {
|
||||
powerEntityInfoList[i].states[j] = new StateInfo();
|
||||
powerEntityInfoList[i].states[j].stateId = j;
|
||||
powerEntityInfoList[i].states[j].stateName = new String(STATE_NAME + i);
|
||||
powerEntityInfoList[i].states[j].stateName = new String(STATE_NAME + j);
|
||||
}
|
||||
}
|
||||
return powerEntityInfoList;
|
||||
@ -154,6 +165,7 @@ public class PowerStatsServiceTest {
|
||||
new StateResidency[STATE_RESIDENCY_COUNT];
|
||||
for (int j = 0; j < stateResidencyResultList[i].stateResidencyData.length; j++) {
|
||||
stateResidencyResultList[i].stateResidencyData[j] = new StateResidency();
|
||||
stateResidencyResultList[i].stateResidencyData[j].stateId = j;
|
||||
stateResidencyResultList[i].stateResidencyData[j].totalTimeInStateMs = j;
|
||||
stateResidencyResultList[i].stateResidencyData[j].totalStateEntryCount = j;
|
||||
stateResidencyResultList[i].stateResidencyData[j].lastEntryTimestampMs = j;
|
||||
@ -300,6 +312,61 @@ public class PowerStatsServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrittenResidencyDataMatchesReadIncidentReportData()
|
||||
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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
|
||||
PowerStatsServiceResidencyProto pssProto =
|
||||
PowerStatsServiceResidencyProto.parseFrom(fileContent);
|
||||
|
||||
// Validate the powerEntityInfo array matches what was written to on-device storage.
|
||||
assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
|
||||
for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
|
||||
PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
|
||||
assertTrue(powerEntityInfo.powerEntityId == i);
|
||||
assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
|
||||
for (int j = 0; j < powerEntityInfo.states.length; j++) {
|
||||
StateInfoProto stateInfo = powerEntityInfo.states[j];
|
||||
assertTrue(stateInfo.stateId == j);
|
||||
assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the stateResidencyResult array matches what was written to on-device storage.
|
||||
assertTrue(pssProto.stateResidencyResult.length == POWER_ENTITY_COUNT);
|
||||
for (int i = 0; i < pssProto.stateResidencyResult.length; i++) {
|
||||
StateResidencyResultProto stateResidencyResult = pssProto.stateResidencyResult[i];
|
||||
assertTrue(stateResidencyResult.powerEntityId == i);
|
||||
assertTrue(stateResidencyResult.stateResidencyData.length == STATE_RESIDENCY_COUNT);
|
||||
for (int j = 0; j < stateResidencyResult.stateResidencyData.length; j++) {
|
||||
StateResidencyProto stateResidency = stateResidencyResult.stateResidencyData[j];
|
||||
assertTrue(stateResidency.stateId == j);
|
||||
assertTrue(stateResidency.totalTimeInStateMs == j);
|
||||
assertTrue(stateResidency.totalStateEntryCount == j);
|
||||
assertTrue(stateResidency.lastEntryTimestampMs == j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorruptOnDeviceMeterStorage() throws IOException {
|
||||
mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
|
||||
@ -383,6 +450,55 @@ public class PowerStatsServiceTest {
|
||||
assertTrue(pssProto.energyConsumerResult.length == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorruptOnDeviceResidencyStorage() 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, RESIDENCY_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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
|
||||
PowerStatsServiceResidencyProto pssProto =
|
||||
PowerStatsServiceResidencyProto.parseFrom(fileContent);
|
||||
|
||||
// Valid powerEntityInfo data is written to the incident report in the call to
|
||||
// mPowerStatsLogger.writeResidencyDataToFile().
|
||||
assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
|
||||
for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
|
||||
PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
|
||||
assertTrue(powerEntityInfo.powerEntityId == i);
|
||||
assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
|
||||
for (int j = 0; j < powerEntityInfo.states.length; j++) {
|
||||
StateInfoProto stateInfo = powerEntityInfo.states[j];
|
||||
assertTrue(stateInfo.stateId == j);
|
||||
assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
|
||||
}
|
||||
}
|
||||
|
||||
// No stateResidencyResults should be written to the incident report since it
|
||||
// is all corrupt (random bytes generated above).
|
||||
assertTrue(pssProto.stateResidencyResult.length == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotEnoughBytesAfterMeterLengthField() throws IOException {
|
||||
mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
|
||||
@ -467,4 +583,54 @@ public class PowerStatsServiceTest {
|
||||
// input buffer had only length and no data.
|
||||
assertTrue(pssProto.energyConsumerResult.length == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotEnoughBytesAfterResidencyLengthField() 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, RESIDENCY_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.writeResidencyDataToFile(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 PowerStatsServiceResidencyProto object.
|
||||
PowerStatsServiceResidencyProto pssProto =
|
||||
PowerStatsServiceResidencyProto.parseFrom(fileContent);
|
||||
|
||||
// Valid powerEntityInfo data is written to the incident report in the call to
|
||||
// mPowerStatsLogger.writeResidencyDataToFile().
|
||||
assertTrue(pssProto.powerEntityInfo.length == POWER_ENTITY_COUNT);
|
||||
for (int i = 0; i < pssProto.powerEntityInfo.length; i++) {
|
||||
PowerEntityInfoProto powerEntityInfo = pssProto.powerEntityInfo[i];
|
||||
assertTrue(powerEntityInfo.powerEntityId == i);
|
||||
assertTrue(powerEntityInfo.powerEntityName.equals(POWER_ENTITY_NAME + i));
|
||||
for (int j = 0; j < powerEntityInfo.states.length; j++) {
|
||||
StateInfoProto stateInfo = powerEntityInfo.states[j];
|
||||
assertTrue(stateInfo.stateId == j);
|
||||
assertTrue(stateInfo.stateName.equals(STATE_NAME + j));
|
||||
}
|
||||
}
|
||||
|
||||
// No stateResidencyResults should be written to the incident report since the
|
||||
// input buffer had only length and no data.
|
||||
assertTrue(pssProto.stateResidencyResult.length == 0);
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,38 @@ public class PowerStatsServiceProtoParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static void printPowerEntityInfo(PowerStatsServiceResidencyProto proto) {
|
||||
String csvHeader = new String();
|
||||
for (int i = 0; i < proto.getPowerEntityInfoCount(); i++) {
|
||||
PowerEntityInfoProto powerEntityInfo = proto.getPowerEntityInfo(i);
|
||||
csvHeader += powerEntityInfo.getPowerEntityId() + ","
|
||||
+ powerEntityInfo.getPowerEntityName() + ",";
|
||||
for (int j = 0; j < powerEntityInfo.getStatesCount(); j++) {
|
||||
StateInfoProto stateInfo = powerEntityInfo.getStates(j);
|
||||
csvHeader += stateInfo.getStateId() + "," + stateInfo.getStateName() + ",";
|
||||
}
|
||||
}
|
||||
System.out.println(csvHeader);
|
||||
}
|
||||
|
||||
private static void printStateResidencyResult(PowerStatsServiceResidencyProto proto) {
|
||||
for (int i = 0; i < proto.getStateResidencyResultCount(); i++) {
|
||||
String csvRow = new String();
|
||||
|
||||
StateResidencyResultProto stateResidencyResult = proto.getStateResidencyResult(i);
|
||||
csvRow += stateResidencyResult.getPowerEntityId() + ",";
|
||||
|
||||
for (int j = 0; j < stateResidencyResult.getStateResidencyDataCount(); j++) {
|
||||
StateResidencyProto stateResidency = stateResidencyResult.getStateResidencyData(j);
|
||||
csvRow += stateResidency.getStateId() + ","
|
||||
+ stateResidency.getTotalTimeInStateMs() + ","
|
||||
+ stateResidency.getTotalStateEntryCount() + ","
|
||||
+ stateResidency.getLastEntryTimestampMs() + ",";
|
||||
}
|
||||
System.out.println(csvRow);
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateCsvFile(String pathToIncidentReport) {
|
||||
try {
|
||||
// Print power meter data.
|
||||
@ -115,6 +147,21 @@ public class PowerStatsServiceProtoParser {
|
||||
} else {
|
||||
System.out.println("Model incident report not found. Exiting.");
|
||||
}
|
||||
|
||||
// Print state residency data.
|
||||
IncidentReportResidencyProto irResidencyProto =
|
||||
IncidentReportResidencyProto.parseFrom(
|
||||
new FileInputStream(pathToIncidentReport));
|
||||
|
||||
if (irResidencyProto.hasIncidentReport()) {
|
||||
PowerStatsServiceResidencyProto pssResidencyProto =
|
||||
irResidencyProto.getIncidentReport();
|
||||
printPowerEntityInfo(pssResidencyProto);
|
||||
printStateResidencyResult(pssResidencyProto);
|
||||
} else {
|
||||
System.out.println("Residency incident report not found. Exiting.");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
System.out.println("Unable to open incident report file: " + pathToIncidentReport);
|
||||
System.out.println(e);
|
||||
|
Loading…
x
Reference in New Issue
Block a user