diff options
10 files changed, 663 insertions, 18 deletions
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java index 000a537cf3c3..623196b637f6 100644 --- a/core/java/android/os/BatteryConsumer.java +++ b/core/java/android/os/BatteryConsumer.java @@ -209,7 +209,8 @@ public abstract class BatteryConsumer { POWER_COMPONENT_VIDEO, POWER_COMPONENT_FLASHLIGHT, POWER_COMPONENT_CAMERA, - POWER_COMPONENT_GNSS}; + POWER_COMPONENT_GNSS, + POWER_COMPONENT_SENSORS}; Arrays.sort(supportedPowerComponents); SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = IntArray.wrap(supportedPowerComponents); }; diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index cdffea4e9ede..c7751e3e5cea 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -3077,7 +3077,7 @@ public abstract class BatteryStats { public static final String[] HISTORY_EVENT_NAMES = new String[] { "null", "proc", "fg", "top", "sync", "wake_lock_in", "job", "user", "userfg", "conn", "active", "pkginst", "pkgunin", "alarm", "stats", "pkginactive", "pkgactive", - "tmpwhitelist", "screenwake", "wakeupap", "longwake", "est_capacity", "state" + "tmpwhitelist", "screenwake", "wakeupap", "longwake", "state" }; public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index e46ab8f0f1cb..03fbfd37cbdb 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -144,6 +144,7 @@ import com.android.server.power.stats.PowerStatsScheduler; import com.android.server.power.stats.PowerStatsStore; import com.android.server.power.stats.PowerStatsUidResolver; import com.android.server.power.stats.ScreenPowerStatsProcessor; +import com.android.server.power.stats.SensorPowerStatsProcessor; import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes; import com.android.server.power.stats.VideoPowerStatsProcessor; import com.android.server.power.stats.WifiPowerStatsProcessor; @@ -595,6 +596,17 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setProcessor( new GnssPowerStatsProcessor(mPowerProfile, mPowerStatsUidResolver)); + config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_SENSORS) + .trackDeviceStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN) + .trackUidStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN, + AggregatedPowerStatsConfig.STATE_PROCESS_STATE) + .setProcessor(new SensorPowerStatsProcessor( + () -> mContext.getSystemService(SensorManager.class))); + config.trackCustomPowerComponents(CustomEnergyConsumerPowerStatsProcessor::new) .trackDeviceStates( AggregatedPowerStatsConfig.STATE_POWER, @@ -706,6 +718,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub BatteryConsumer.POWER_COMPONENT_GNSS, Flags.streamlinedMiscBatteryStats()); + mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( + BatteryConsumer.POWER_COMPONENT_SENSORS, + Flags.streamlinedMiscBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CAMERA, Flags.streamlinedMiscBatteryStats()); mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index c4b37c6939b7..143b3ffa93af 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -5431,8 +5431,6 @@ public class BatteryStatsImpl extends BatteryStats { } } - int mSensorNesting; - @GuardedBy("this") public void noteStartSensorLocked(int uid, int sensor) { noteStartSensorLocked(uid, sensor, mClock.elapsedRealtime(), mClock.uptimeMillis()); @@ -5441,11 +5439,8 @@ public class BatteryStatsImpl extends BatteryStats { @GuardedBy("this") public void noteStartSensorLocked(int uid, int sensor, long elapsedRealtimeMs, long uptimeMs) { uid = mapUid(uid); - if (mSensorNesting == 0) { - mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs, - HistoryItem.STATE_SENSOR_ON_FLAG); - } - mSensorNesting++; + mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs, + HistoryItem.STATE_SENSOR_ON_FLAG, uid, "sensor:0x" + Integer.toHexString(sensor)); getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs) .noteStartSensor(sensor, elapsedRealtimeMs); } @@ -5458,11 +5453,8 @@ public class BatteryStatsImpl extends BatteryStats { @GuardedBy("this") public void noteStopSensorLocked(int uid, int sensor, long elapsedRealtimeMs, long uptimeMs) { uid = mapUid(uid); - mSensorNesting--; - if (mSensorNesting == 0) { - mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs, - HistoryItem.STATE_SENSOR_ON_FLAG); - } + mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs, + HistoryItem.STATE_SENSOR_ON_FLAG, uid, "sensor:0x" + Integer.toHexString(sensor)); getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs) .noteStopSensor(sensor, elapsedRealtimeMs); } diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index a5e4cf5a0e33..b308f3840383 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -93,8 +93,10 @@ public class BatteryUsageStatsProvider { if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_BLUETOOTH)) { mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile)); } - mPowerCalculators.add(new SensorPowerCalculator( - mContext.getSystemService(SensorManager.class))); + if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_SENSORS)) { + mPowerCalculators.add(new SensorPowerCalculator( + mContext.getSystemService(SensorManager.class))); + } if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_GNSS)) { mPowerCalculators.add(new GnssPowerCalculator(mPowerProfile)); } diff --git a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java index 7d7b3c2fa3c5..c81c7ffe5371 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java @@ -257,8 +257,8 @@ public abstract class PowerStatsProcessor { for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) { deviceStateEstimations.get(i).intermediates = null; } - for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) { - deviceStateEstimations.get(i).intermediates = null; + for (int i = combinedDeviceStateEstimations.size() - 1; i >= 0; i--) { + combinedDeviceStateEstimations.get(i).intermediates = null; } for (int i = uidStateEstimates.size() - 1; i >= 0; i--) { UidStateEstimate uidStateEstimate = uidStateEstimates.get(i); diff --git a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java index e203e4a4175b..908c75155f42 100644 --- a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java @@ -119,6 +119,7 @@ public class ScreenPowerStatsProcessor extends PowerStatsProcessor { if (!uids.isEmpty()) { computeUidPowerEstimates(stats, uids); } + mPlan.resetIntermediates(); } private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) { diff --git a/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java new file mode 100644 index 000000000000..e66cd3970d2f --- /dev/null +++ b/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 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.power.stats; + +import android.os.PersistableBundle; +import android.util.Slog; +import android.util.SparseIntArray; + +public class SensorPowerStatsLayout extends PowerStatsLayout { + private static final String TAG = "SensorPowerStatsLayout"; + private static final String EXTRA_DEVICE_SENSOR_HANDLES = "dsh"; + private static final String EXTRA_UID_SENSOR_POSITIONS = "usp"; + + private final SparseIntArray mSensorPositions = new SparseIntArray(); + + void addUidSensorSection(int handle, String label) { + mSensorPositions.put(handle, addUidSection(1, label, FLAG_OPTIONAL)); + } + + /** + * Returns the position in the uid stats array of the duration element corresponding + * to the specified sensor identified by its handle. + */ + public int getUidSensorDurationPosition(int handle) { + return mSensorPositions.get(handle, UNSUPPORTED); + } + + /** + * Adds the specified duration to the accumulated timer for the specified sensor. + */ + public void addUidSensorDuration(long[] stats, int handle, long durationMs) { + int position = mSensorPositions.get(handle, UNSUPPORTED); + if (position == UNSUPPORTED) { + Slog.e(TAG, "Unknown sensor: " + handle); + return; + } + stats[position] += durationMs; + } + + @Override + public void toExtras(PersistableBundle extras) { + super.toExtras(extras); + + int[] handlers = new int[mSensorPositions.size()]; + int[] uidDurationPositions = new int[mSensorPositions.size()]; + + for (int i = 0; i < mSensorPositions.size(); i++) { + handlers[i] = mSensorPositions.keyAt(i); + uidDurationPositions[i] = mSensorPositions.valueAt(i); + } + + extras.putIntArray(EXTRA_DEVICE_SENSOR_HANDLES, handlers); + extras.putIntArray(EXTRA_UID_SENSOR_POSITIONS, uidDurationPositions); + } + + @Override + public void fromExtras(PersistableBundle extras) { + super.fromExtras(extras); + + int[] handlers = extras.getIntArray(EXTRA_DEVICE_SENSOR_HANDLES); + int[] uidDurationPositions = extras.getIntArray(EXTRA_UID_SENSOR_POSITIONS); + + for (int i = 0; i < handlers.length; i++) { + mSensorPositions.put(handlers[i], uidDurationPositions[i]); + } + } +} diff --git a/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java new file mode 100644 index 000000000000..7b815d91b325 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2024 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.power.stats; + +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.BatteryConsumer; +import android.os.BatteryStats; +import android.os.PersistableBundle; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.os.PowerStats; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Supplier; + +public class SensorPowerStatsProcessor extends PowerStatsProcessor { + private static final String TAG = "SensorPowerStatsProcessor"; + private static final String ANDROID_SENSOR_TYPE_PREFIX = "android.sensor."; + + private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60; + private static final String SENSOR_EVENT_TAG_PREFIX = "sensor:0x"; + private final Supplier<SensorManager> mSensorManagerSupplier; + + private static final long INITIAL_TIMESTAMP = -1; + private SensorManager mSensorManager; + private SensorPowerStatsLayout mStatsLayout; + private PowerStats mPowerStats; + private boolean mIsInitialized; + private PowerStats.Descriptor mDescriptor; + private long mLastUpdateTimestamp; + private PowerEstimationPlan mPlan; + + private static class SensorState { + public int sensorHandle; + public boolean stateOn; + public int uid; + public long startTime = INITIAL_TIMESTAMP; + } + + private static class Intermediates { + public double power; + } + + private final SparseArray<SensorState> mSensorStates = new SparseArray<>(); + private long[] mTmpDeviceStatsArray; + private long[] mTmpUidStatsArray; + + public SensorPowerStatsProcessor(Supplier<SensorManager> sensorManagerSupplier) { + mSensorManagerSupplier = sensorManagerSupplier; + } + + private boolean ensureInitialized() { + if (mIsInitialized) { + return true; + } + + mSensorManager = mSensorManagerSupplier.get(); + if (mSensorManager == null) { + return false; + } + + mStatsLayout = new SensorPowerStatsLayout(); + List<Sensor> sensorList = new ArrayList<>(mSensorManager.getSensorList(Sensor.TYPE_ALL)); + sensorList.sort(Comparator.comparingInt(Sensor::getId)); + for (int i = 0; i < sensorList.size(); i++) { + Sensor sensor = sensorList.get(i); + String label = makeLabel(sensor, sensorList); + mStatsLayout.addUidSensorSection(sensor.getHandle(), label); + } + mStatsLayout.addUidSectionPowerEstimate(); + mStatsLayout.addDeviceSectionPowerEstimate(); + + PersistableBundle extras = new PersistableBundle(); + mStatsLayout.toExtras(extras); + mDescriptor = new PowerStats.Descriptor( + BatteryConsumer.POWER_COMPONENT_SENSORS, mStatsLayout.getDeviceStatsArrayLength(), + null, 0, mStatsLayout.getUidStatsArrayLength(), + extras); + + mPowerStats = new PowerStats(mDescriptor); + mTmpUidStatsArray = new long[mDescriptor.uidStatsArrayLength]; + mTmpDeviceStatsArray = new long[mDescriptor.statsArrayLength]; + + mIsInitialized = true; + return true; + } + + private String makeLabel(Sensor sensor, List<Sensor> sensorList) { + int type = sensor.getType(); + String label = sensor.getStringType(); + + boolean isSingleton = true; + for (int i = sensorList.size() - 1; i >= 0; i--) { + Sensor s = sensorList.get(i); + if (s == sensor) { + continue; + } + if (s.getType() == type) { + isSingleton = false; + break; + } + } + if (!isSingleton) { + StringBuilder sb = new StringBuilder(label).append('.'); + if (sensor.getId() > 0) { // 0 and -1 are reserved + sb.append(sensor.getId()); + } else { + sb.append(sensor.getName()); + } + label = sb.toString(); + } + if (label.startsWith(ANDROID_SENSOR_TYPE_PREFIX)) { + label = label.substring(ANDROID_SENSOR_TYPE_PREFIX.length()); + } + return label.replace(' ', '_'); + } + + @Override + void start(PowerComponentAggregatedPowerStats stats, long timestampMs) { + if (!ensureInitialized()) { + return; + } + + // Establish a baseline at the beginning of an accumulation pass + mLastUpdateTimestamp = timestampMs; + flushPowerStats(stats, timestampMs); + } + + @Override + void noteStateChange(PowerComponentAggregatedPowerStats stats, BatteryStats.HistoryItem item) { + if (!mIsInitialized) { + return; + } + + if (item.eventTag == null || !item.eventTag.string.startsWith(SENSOR_EVENT_TAG_PREFIX)) { + return; + } + + int sensorHandle; + try { + sensorHandle = Integer.parseInt(item.eventTag.string, SENSOR_EVENT_TAG_PREFIX.length(), + item.eventTag.string.length(), 16); + } catch (NumberFormatException e) { + Slog.wtf(TAG, "Bad format of event tag: " + item.eventTag.string); + return; + } + + SensorState sensor = mSensorStates.get(sensorHandle); + if (sensor == null) { + sensor = new SensorState(); + sensor.sensorHandle = sensorHandle; + mSensorStates.put(sensorHandle, sensor); + } + + int uid = item.eventTag.uid; + boolean sensorOn = (item.states & BatteryStats.HistoryItem.STATE_SENSOR_ON_FLAG) != 0; + if (sensorOn) { + if (!sensor.stateOn) { + sensor.stateOn = true; + sensor.uid = uid; + sensor.startTime = item.time; + } else if (sensor.uid != uid) { + recordUsageDuration(sensor, item.time); + sensor.uid = uid; + } + } else { + if (sensor.stateOn) { + recordUsageDuration(sensor, item.time); + sensor.stateOn = false; + } + } + } + + @Override + void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) { + if (!mIsInitialized) { + return; + } + + for (int i = mSensorStates.size() - 1; i >= 0; i--) { + SensorState sensor = mSensorStates.valueAt(i); + if (sensor.stateOn) { + recordUsageDuration(sensor, timestampMs); + } + } + flushPowerStats(stats, timestampMs); + + if (mPlan == null) { + mPlan = new PowerEstimationPlan(stats.getConfig()); + } + + List<Integer> uids = new ArrayList<>(); + stats.collectUids(uids); + + computeUidPowerEstimates(stats, uids); + computeDevicePowerEstimates(stats); + + mPlan.resetIntermediates(); + } + + protected void recordUsageDuration(SensorState sensorState, long time) { + long durationMs = Math.max(0, time - sensorState.startTime); + if (durationMs > 0) { + long[] uidStats = mPowerStats.uidStats.get(sensorState.uid); + if (uidStats == null) { + uidStats = new long[mDescriptor.uidStatsArrayLength]; + mPowerStats.uidStats.put(sensorState.uid, uidStats); + } + mStatsLayout.addUidSensorDuration(uidStats, sensorState.sensorHandle, durationMs); + } + sensorState.startTime = time; + } + + private void flushPowerStats(PowerComponentAggregatedPowerStats stats, long timestamp) { + mPowerStats.durationMs = timestamp - mLastUpdateTimestamp; + stats.addPowerStats(mPowerStats, timestamp); + + Arrays.fill(mPowerStats.stats, 0); + mPowerStats.uidStats.clear(); + mLastUpdateTimestamp = timestamp; + } + + private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, + List<Integer> uids) { + List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL); + int[] uidSensorDurationPositions = new int[sensorList.size()]; + double[] sensorPower = new double[sensorList.size()]; + for (int i = sensorList.size() - 1; i >= 0; i--) { + Sensor sensor = sensorList.get(i); + uidSensorDurationPositions[i] = + mStatsLayout.getUidSensorDurationPosition(sensor.getHandle()); + sensorPower[i] = sensor.getPower() / MILLIS_IN_HOUR; + } + + for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) { + UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i); + List<UidStateProportionalEstimate> proportionalEstimates = + uidStateEstimate.proportionalEstimates; + for (int j = proportionalEstimates.size() - 1; j >= 0; j--) { + UidStateProportionalEstimate proportionalEstimate = proportionalEstimates.get(j); + for (int k = uids.size() - 1; k >= 0; k--) { + int uid = uids.get(k); + if (!stats.getUidStats(mTmpUidStatsArray, uid, + proportionalEstimate.stateValues)) { + continue; + } + double power = 0; + for (int m = 0; m < uidSensorDurationPositions.length; m++) { + int position = uidSensorDurationPositions[m]; + if (position == PowerStatsLayout.UNSUPPORTED + || mTmpUidStatsArray[position] == 0) { + continue; + } + power += sensorPower[m] * mTmpUidStatsArray[position]; + } + if (power == 0) { + continue; + } + + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + + Intermediates intermediates = (Intermediates) uidStateEstimate + .combinedDeviceStateEstimate.intermediates; + if (intermediates == null) { + intermediates = new Intermediates(); + uidStateEstimate.combinedDeviceStateEstimate.intermediates = intermediates; + } + intermediates.power += power; + } + } + } + } + + private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) { + for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) { + CombinedDeviceStateEstimate estimation = + mPlan.combinedDeviceStateEstimations.get(i); + if (estimation.intermediates == null) { + continue; + } + + if (!stats.getDeviceStats(mTmpDeviceStatsArray, estimation.stateValues)) { + continue; + } + + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, + ((Intermediates) estimation.intermediates).power); + stats.setDeviceStats(estimation.stateValues, mTmpDeviceStatsArray); + } + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java new file mode 100644 index 000000000000..7000487e7912 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2024 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.power.stats; + +import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND; +import static android.os.BatteryConsumer.PROCESS_STATE_CACHED; +import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND; +import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE; + +import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.hardware.input.InputSensorInfo; +import android.os.BatteryConsumer; +import android.os.BatteryStats; +import android.os.Process; +import android.platform.test.ravenwood.RavenwoodRule; + +import com.android.internal.os.MonotonicClock; +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +public class SensorPowerStatsProcessorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Rule(order = 1) + public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() + .initMeasuredEnergyStatsLocked(); + + private static final double PRECISION = 0.00001; + private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42; + private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101; + private static final int SENSOR_HANDLE_1 = 77; + private static final int SENSOR_HANDLE_2 = 88; + private static final int SENSOR_HANDLE_3 = 99; + + @Mock + private SensorManager mSensorManager; + + private MonotonicClock mMonotonicClock; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mMonotonicClock = new MonotonicClock(0, mStatsRule.getMockClock()); + Sensor sensor1 = createSensor(SENSOR_HANDLE_1, Sensor.TYPE_STEP_COUNTER, + Sensor.STRING_TYPE_STEP_COUNTER, "dancing", 100); + Sensor sensor2 = createSensor(SENSOR_HANDLE_2, Sensor.TYPE_MOTION_DETECT, + "com.example", "tango", 200); + Sensor sensor3 = createSensor(SENSOR_HANDLE_3, Sensor.TYPE_MOTION_DETECT, + "com.example", "waltz", 300); + when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn( + List.of(sensor1, sensor2, sensor3)); + } + + @Test + public void testPowerEstimation() { + SensorPowerStatsProcessor processor = new SensorPowerStatsProcessor(() -> mSensorManager); + + PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats(processor); + + processor.noteStateChange(stats, buildHistoryItem(0, true, APP_UID1, SENSOR_HANDLE_1)); + + // Turn the screen off after 2.5 seconds + stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000); + + processor.noteStateChange(stats, buildHistoryItem(6000, false, APP_UID1, SENSOR_HANDLE_1)); + processor.noteStateChange(stats, buildHistoryItem(7000, true, APP_UID2, SENSOR_HANDLE_1)); + processor.noteStateChange(stats, buildHistoryItem(8000, true, APP_UID2, SENSOR_HANDLE_2)); + processor.noteStateChange(stats, buildHistoryItem(9000, false, APP_UID2, SENSOR_HANDLE_1)); + + processor.finish(stats, 10000); + + PowerStats.Descriptor descriptor = stats.getPowerStatsDescriptor(); + SensorPowerStatsLayout statsLayout = new SensorPowerStatsLayout(); + statsLayout.fromExtras(descriptor.extras); + + String dump = stats.toString(); + assertThat(dump).contains(" step_counter: "); + assertThat(dump).contains(" com.example.tango: "); + + long[] uidStats = new long[descriptor.uidStatsArrayLength]; + + // For UID1: + // SENSOR1 was on for 6000 ms. + // Estimated power: 6000 * 100 = 0.167 mAh + // split between three different states + // fg screen-on: 6000 * 2500/10000 + // bg screen-off: 6000 * 2500/10000 + // fgs screen-off: 6000 * 5000/10000 + double expectedPower1 = 0.166666; + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 2500 / 10000); + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 2500 / 10000); + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 5000 / 10000); + + // For UID2: + // SENSOR1 was on for 2000 ms. + // Estimated power: 2000 * 100 = 0.0556 mAh + // split between three different states + // cached screen-on: 2000 * 2500/10000 + // cached screen-off: 2000 * 7500/10000 + // SENSOR2 was on for 2000 ms. + // Estimated power: 2000 * 200 = 0.11111 mAh + // split between three different states + // cached screen-on: 2000 * 2500/10000 + // cached screen-off: 2000 * 7500/10000 + double expectedPower2 = 0.05555 + 0.11111; + stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 2500 / 10000); + stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 7500 / 10000); + + long[] deviceStats = new long[descriptor.statsArrayLength]; + + stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of((expectedPower1 + expectedPower2) * 2500 / 10000); + + stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of((expectedPower1 + expectedPower2) * 7500 / 10000); + } + + private BatteryStats.HistoryItem buildHistoryItem(int timestamp, boolean stateOn, + int uid, int sensor) { + mStatsRule.setTime(timestamp, timestamp); + BatteryStats.HistoryItem historyItem = new BatteryStats.HistoryItem(); + historyItem.time = mMonotonicClock.monotonicTime(); + historyItem.states = stateOn ? BatteryStats.HistoryItem.STATE_SENSOR_ON_FLAG : 0; + if (stateOn) { + historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE + | BatteryStats.HistoryItem.EVENT_FLAG_START; + } else { + historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE + | BatteryStats.HistoryItem.EVENT_FLAG_FINISH; + } + historyItem.eventTag = historyItem.localEventTag; + historyItem.eventTag.uid = uid; + historyItem.eventTag.string = "sensor:0x" + Integer.toHexString(sensor); + return historyItem; + } + + private int[] states(int... states) { + return states; + } + + private static PowerComponentAggregatedPowerStats createAggregatedPowerStats( + SensorPowerStatsProcessor processor) { + AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig(); + config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_SENSORS) + .trackDeviceStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN) + .trackUidStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN, + AggregatedPowerStatsConfig.STATE_PROCESS_STATE) + .setProcessor(processor); + + AggregatedPowerStats aggregatedPowerStats = new AggregatedPowerStats(config); + PowerComponentAggregatedPowerStats powerComponentStats = + aggregatedPowerStats.getPowerComponentStats( + BatteryConsumer.POWER_COMPONENT_SENSORS); + processor.start(powerComponentStats, 0); + + powerComponentStats.setState(STATE_POWER, POWER_STATE_OTHER, 0); + powerComponentStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0); + powerComponentStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0); + powerComponentStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0); + + return powerComponentStats; + } + + private Sensor createSensor(int handle, int type, String stringType, String name, float power) { + if (RavenwoodRule.isOnRavenwood()) { + Sensor sensor = mock(Sensor.class); + when(sensor.getHandle()).thenReturn(handle); + when(sensor.getType()).thenReturn(type); + when(sensor.getStringType()).thenReturn(stringType); + when(sensor.getName()).thenReturn(name); + when(sensor.getPower()).thenReturn(power); + return sensor; + } else { + return new Sensor(new InputSensorInfo(name, "vendor", 0 /* version */, + handle, type, 100.0f /*maxRange */, 0.02f /* resolution */, + (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */, + 0 /* fifoMaxEventCount */, stringType /* stringType */, + "" /* requiredPermission */, 0 /* maxDelay */, 0 /* flags */, 0 /* id */)); + } + } +} |