diff options
13 files changed, 952 insertions, 31 deletions
diff --git a/core/java/com/android/internal/os/MultiStateStats.java b/core/java/com/android/internal/os/MultiStateStats.java index ad1507177731..f971849987dd 100644 --- a/core/java/com/android/internal/os/MultiStateStats.java +++ b/core/java/com/android/internal/os/MultiStateStats.java @@ -41,6 +41,10 @@ public class MultiStateStats { this.mTracked = tracked; this.mLabels = labels; } + + public boolean isTracked() { + return mTracked; + } } /** diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java index 8b0e814c87aa..8f66d1f9365c 100644 --- a/core/java/com/android/internal/os/PowerStats.java +++ b/core/java/com/android/internal/os/PowerStats.java @@ -21,12 +21,14 @@ import android.annotation.Nullable; import android.os.BatteryConsumer; import android.os.Bundle; import android.os.Parcel; +import android.os.PersistableBundle; import android.os.UserHandle; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.SparseArray; import java.util.Arrays; +import java.util.Objects; /** * Container for power stats, acquired by various PowerStatsCollector classes. See subclasses for @@ -74,16 +76,16 @@ public final class PowerStats { * Extra parameters specific to the power component, e.g. the availability of power * monitors. */ - public final Bundle extras; + public final PersistableBundle extras; public Descriptor(@BatteryConsumer.PowerComponent int powerComponentId, - int statsArrayLength, int uidStatsArrayLength, @NonNull Bundle extras) { + int statsArrayLength, int uidStatsArrayLength, @NonNull PersistableBundle extras) { this(powerComponentId, BatteryConsumer.powerComponentIdToString(powerComponentId), statsArrayLength, uidStatsArrayLength, extras); } public Descriptor(int customPowerComponentId, String name, int statsArrayLength, - int uidStatsArrayLength, Bundle extras) { + int uidStatsArrayLength, PersistableBundle extras) { if (statsArrayLength > MAX_STATS_ARRAY_LENGTH) { throw new IllegalArgumentException( "statsArrayLength is too high. Max = " + MAX_STATS_ARRAY_LENGTH); @@ -104,11 +106,11 @@ public final class PowerStats { */ public void writeSummaryToParcel(Parcel parcel) { int firstWord = ((PARCEL_FORMAT_VERSION << PARCEL_FORMAT_VERSION_SHIFT) - & PARCEL_FORMAT_VERSION_MASK) - | ((statsArrayLength << STATS_ARRAY_LENGTH_SHIFT) - & STATS_ARRAY_LENGTH_MASK) - | ((uidStatsArrayLength << UID_STATS_ARRAY_LENGTH_SHIFT) - & UID_STATS_ARRAY_LENGTH_MASK); + & PARCEL_FORMAT_VERSION_MASK) + | ((statsArrayLength << STATS_ARRAY_LENGTH_SHIFT) + & STATS_ARRAY_LENGTH_MASK) + | ((uidStatsArrayLength << UID_STATS_ARRAY_LENGTH_SHIFT) + & UID_STATS_ARRAY_LENGTH_MASK); parcel.writeInt(firstWord); parcel.writeInt(powerComponentId); parcel.writeString(name); @@ -125,7 +127,7 @@ public final class PowerStats { int version = (firstWord & PARCEL_FORMAT_VERSION_MASK) >>> PARCEL_FORMAT_VERSION_SHIFT; if (version != PARCEL_FORMAT_VERSION) { Log.w(TAG, "Cannot read PowerStats from Parcel - the parcel format version has " - + "changed from " + version + " to " + PARCEL_FORMAT_VERSION); + + "changed from " + version + " to " + PARCEL_FORMAT_VERSION); return null; } int statsArrayLength = @@ -134,23 +136,42 @@ public final class PowerStats { (firstWord & UID_STATS_ARRAY_LENGTH_MASK) >>> UID_STATS_ARRAY_LENGTH_SHIFT; int powerComponentId = parcel.readInt(); String name = parcel.readString(); - Bundle extras = parcel.readBundle(PowerStats.class.getClassLoader()); + PersistableBundle extras = parcel.readPersistableBundle(); return new Descriptor(powerComponentId, name, statsArrayLength, uidStatsArrayLength, extras); } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Descriptor)) return false; + Descriptor that = (Descriptor) o; + return powerComponentId == that.powerComponentId + && statsArrayLength == that.statsArrayLength + && uidStatsArrayLength == that.uidStatsArrayLength + && Objects.equals(name, that.name) + && extras.size() == that.extras.size() // Unparcel the Parcel if not yet + && Bundle.kindofEquals(extras, + that.extras); // Since the Parcel is now unparceled, do a deep comparison + } + + @Override + public int hashCode() { + return Objects.hash(powerComponentId); + } + + @Override public String toString() { if (extras != null) { extras.size(); // Unparcel } return "PowerStats.Descriptor{" - + "powerComponentId=" + powerComponentId - + ", name='" + name + '\'' - + ", statsArrayLength=" + statsArrayLength - + ", uidStatsArrayLength=" + uidStatsArrayLength - + ", extras=" + extras - + '}'; + + "powerComponentId=" + powerComponentId + + ", name='" + name + '\'' + + ", statsArrayLength=" + statsArrayLength + + ", uidStatsArrayLength=" + uidStatsArrayLength + + ", extras=" + extras + + '}'; } } @@ -254,7 +275,7 @@ public final class PowerStats { } if (parcel.dataPosition() != endPos) { Log.e(TAG, "Corrupted PowerStats parcel. Expected length: " + length - + ", actual length: " + (parcel.dataPosition() - startPos)); + + ", actual length: " + (parcel.dataPosition() - startPos)); return null; } return stats; diff --git a/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java b/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java index ee04383107e2..29da2319adc2 100644 --- a/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java @@ -19,8 +19,8 @@ package com.android.internal.os; import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; -import android.os.Bundle; import android.os.Parcel; +import android.os.PersistableBundle; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -39,7 +39,7 @@ public class PowerStatsTest { @Before public void setup() { mRegistry = new PowerStats.DescriptorRegistry(); - Bundle extras = new Bundle(); + PersistableBundle extras = new PersistableBundle(); extras.putBoolean("hasPowerMonitor", true); mDescriptor = new PowerStats.Descriptor(BatteryConsumer.POWER_COMPONENT_CPU, 3, 2, extras); mRegistry.register(mDescriptor); @@ -52,8 +52,8 @@ public class PowerStatsTest { stats.stats[0] = 10; stats.stats[1] = 20; stats.stats[2] = 30; - stats.uidStats.put(42, new long[] {40, 50}); - stats.uidStats.put(99, new long[] {60, 70}); + stats.uidStats.put(42, new long[]{40, 50}); + stats.uidStats.put(99, new long[]{60, 70}); Parcel parcel = Parcel.obtain(); mDescriptor.writeSummaryToParcel(parcel); @@ -74,10 +74,10 @@ public class PowerStatsTest { PowerStats newStats = PowerStats.readFromParcel(newParcel, mRegistry); assertThat(newStats.durationMs).isEqualTo(1234); - assertThat(newStats.stats).isEqualTo(new long[] {10, 20, 30}); + assertThat(newStats.stats).isEqualTo(new long[]{10, 20, 30}); assertThat(newStats.uidStats.size()).isEqualTo(2); - assertThat(newStats.uidStats.get(42)).isEqualTo(new long[] {40, 50}); - assertThat(newStats.uidStats.get(99)).isEqualTo(new long[] {60, 70}); + assertThat(newStats.uidStats.get(42)).isEqualTo(new long[]{40, 50}); + assertThat(newStats.uidStats.get(99)).isEqualTo(new long[]{60, 70}); String end = newParcel.readString(); assertThat(end).isEqualTo("END"); @@ -86,7 +86,7 @@ public class PowerStatsTest { @Test public void parceling_unrecognizedPowerComponent() { PowerStats stats = new PowerStats( - new PowerStats.Descriptor(777, "luck", 3, 2, new Bundle())); + new PowerStats.Descriptor(777, "luck", 3, 2, new PersistableBundle())); stats.durationMs = 1234; Parcel parcel = Parcel.obtain(); diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index b0f30d6875ae..610b8af48cd1 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -2631,6 +2631,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub mStats.dumpStatsSample(pw); } + private void dumpAggregatedStats(PrintWriter pw) { + mStats.dumpAggregatedStats(pw, /* startTime */ 0, /* endTime */0); + } + private void dumpMeasuredEnergyStats(PrintWriter pw) { // Wait for the completion of pending works if there is any awaitCompletion(); @@ -2875,6 +2879,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if ("--sample".equals(arg)) { dumpStatsSample(pw); return; + } else if ("--aggregated".equals(arg)) { + dumpAggregatedStats(pw); + return; } else if ("-a".equals(arg)) { flags |= BatteryStats.DUMP_VERBOSE; } else if (arg.length() > 0 && arg.charAt(0) == '-'){ diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java new file mode 100644 index 000000000000..6cc9d0a54607 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 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.annotation.CurrentTimeMillisLong; +import android.annotation.DurationMillisLong; +import android.os.UserHandle; +import android.text.format.DateFormat; +import android.util.IndentingPrintWriter; + +import com.android.internal.os.PowerStats; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * This class represents aggregated power stats for a variety of power components (CPU, WiFi, + * etc) covering a specific period of power usage history. + */ +class AggregatedPowerStats { + private final PowerComponentAggregatedPowerStats[] mPowerComponentStats; + + @CurrentTimeMillisLong + private long mStartTime; + + @DurationMillisLong + private long mDurationMs; + + AggregatedPowerStats(PowerComponentAggregatedPowerStats... powerComponentAggregatedPowerStats) { + this.mPowerComponentStats = powerComponentAggregatedPowerStats; + } + + void setStartTime(@CurrentTimeMillisLong long startTime) { + mStartTime = startTime; + } + + @CurrentTimeMillisLong + public long getStartTime() { + return mStartTime; + } + + void setDuration(long durationMs) { + mDurationMs = durationMs; + } + + @DurationMillisLong + public long getDuration() { + return mDurationMs; + } + + PowerComponentAggregatedPowerStats getPowerComponentStats(int powerComponentId) { + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + if (stats.powerComponentId == powerComponentId) { + return stats; + } + } + return null; + } + + void setDeviceState(@PowerStatsAggregator.TrackedState int stateId, int state, long time) { + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.setState(stateId, state, time); + } + } + + void setUidState(int uid, @PowerStatsAggregator.TrackedState int stateId, int state, + long time) { + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.setUidState(uid, stateId, state, time); + } + } + + boolean isCompatible(PowerStats powerStats) { + int powerComponentId = powerStats.descriptor.powerComponentId; + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + if (stats.powerComponentId == powerComponentId && !stats.isCompatible(powerStats)) { + return false; + } + } + return true; + } + + void addPowerStats(PowerStats powerStats, long time) { + int powerComponentId = powerStats.descriptor.powerComponentId; + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + if (stats.powerComponentId == powerComponentId) { + stats.addPowerStats(powerStats, time); + } + } + } + + void reset() { + mStartTime = 0; + mDurationMs = 0; + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.reset(); + } + } + + void dump(PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.print("Start time: "); + ipw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartTime)); + ipw.print(" duration: "); + ipw.print(mDurationMs); + ipw.println(); + + ipw.println("Device"); + ipw.increaseIndent(); + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.dumpDevice(ipw); + } + ipw.decreaseIndent(); + + Set<Integer> uids = new HashSet<>(); + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.collectUids(uids); + } + + Integer[] allUids = uids.toArray(new Integer[uids.size()]); + Arrays.sort(allUids); + for (int uid : allUids) { + ipw.println(UserHandle.formatUid(uid)); + ipw.increaseIndent(); + for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) { + stats.dumpUid(ipw, uid); + } + ipw.decreaseIndent(); + } + } +} 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 c5f879548210..613f18982b86 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -283,6 +283,7 @@ public class BatteryStatsImpl extends BatteryStats { private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>(); private int[] mCpuPowerBracketMap; private final CpuPowerStatsCollector mCpuPowerStatsCollector; + private final PowerStatsAggregator mPowerStatsAggregator; public LongSparseArray<SamplingTimer> getKernelMemoryStats() { return mKernelMemoryStats; @@ -1747,6 +1748,7 @@ public class BatteryStatsImpl extends BatteryStats { mEnergyConsumerRetriever = null; mUserInfoProvider = null; mCpuPowerStatsCollector = null; + mPowerStatsAggregator = null; } private void init(Clock clock) { @@ -10947,6 +10949,18 @@ public class BatteryStatsImpl extends BatteryStats { mHandler, mBatteryStatsConfig.getPowerStatsThrottlePeriodCpu()); mCpuPowerStatsCollector.addConsumer(this::recordPowerStats); + PowerStatsAggregator.Builder builder = new PowerStatsAggregator.Builder(mHistory); + builder.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_CPU) + .trackDeviceStates( + PowerStatsAggregator.STATE_POWER, + PowerStatsAggregator.STATE_SCREEN) + .trackUidStates( + PowerStatsAggregator.STATE_POWER, + PowerStatsAggregator.STATE_SCREEN, + PowerStatsAggregator.STATE_PROCESS_STATE); + + mPowerStatsAggregator = builder.build(); + mStartCount++; initTimersAndCounters(); mOnBattery = mOnBatteryInternal = false; @@ -15694,6 +15708,14 @@ public class BatteryStatsImpl extends BatteryStats { mCpuPowerStatsCollector.collectAndDump(pw); } + /** + * Aggregates power stats between the specified times and prints them. + */ + public void dumpAggregatedStats(PrintWriter pw, long startTimeMs, long endTimeMs) { + mPowerStatsAggregator.aggregateBatteryStats(startTimeMs, endTimeMs, + stats-> stats.dump(pw)); + } + private final Runnable mWriteAsyncRunnable = () -> { synchronized (BatteryStatsImpl.this) { writeSyncLocked(); diff --git a/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java new file mode 100644 index 000000000000..5b3fe064d79a --- /dev/null +++ b/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.BatteryConsumer; + +import com.android.internal.os.MultiStateStats; + +class CpuAggregatedPowerStats extends PowerComponentAggregatedPowerStats { + + CpuAggregatedPowerStats(MultiStateStats.States[] deviceStates, + MultiStateStats.States[] uidStates) { + super(BatteryConsumer.POWER_COMPONENT_CPU, deviceStates, uidStates); + } +} diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java index 020cd91c51c1..376ca897fbd1 100644 --- a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java @@ -17,8 +17,8 @@ package com.android.server.power.stats; import android.os.BatteryConsumer; -import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.util.SparseArray; import com.android.internal.annotations.Keep; @@ -74,7 +74,7 @@ public class CpuPowerStatsCollector extends PowerStatsCollector { mCpuPowerStats = new PowerStats( new PowerStats.Descriptor(BatteryConsumer.POWER_COMPONENT_CPU, 0, mUidStatsSize, - new Bundle())); + new PersistableBundle())); } /** diff --git a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java new file mode 100644 index 000000000000..686268fea22b --- /dev/null +++ b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2023 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.util.IndentingPrintWriter; +import android.util.SparseArray; + +import com.android.internal.os.MultiStateStats; +import com.android.internal.os.PowerStats; + +import java.util.Collection; + +/** + * Aggregated power stats for a specific power component (e.g. CPU, WiFi, etc). This class + * treats stats as arrays of nonspecific longs. Subclasses contain specific logic to interpret those + * longs and use them for calculations such as power attribution. They may use meta-data supplied + * as part of the {@link PowerStats.Descriptor}. + */ +class PowerComponentAggregatedPowerStats { + public final int powerComponentId; + private final MultiStateStats.States[] mDeviceStateConfig; + private final MultiStateStats.States[] mUidStateConfig; + private final int[] mDeviceStates; + private final long[] mDeviceStateTimestamps; + + private MultiStateStats.Factory mStatsFactory; + private MultiStateStats.Factory mUidStatsFactory; + private PowerStats.Descriptor mPowerStatsDescriptor; + private MultiStateStats mDeviceStats; + private final SparseArray<UidStats> mUidStats = new SparseArray<>(); + + private static class UidStats { + public int[] states; + public long[] stateTimestampMs; + public MultiStateStats stats; + } + + PowerComponentAggregatedPowerStats(int powerComponentId, + MultiStateStats.States[] deviceStates, + MultiStateStats.States[] uidStates) { + this.powerComponentId = powerComponentId; + mDeviceStateConfig = deviceStates; + mUidStateConfig = uidStates; + mDeviceStates = new int[mDeviceStateConfig.length]; + mDeviceStateTimestamps = new long[mDeviceStateConfig.length]; + } + + void setState(@PowerStatsAggregator.TrackedState int stateId, int state, long time) { + mDeviceStates[stateId] = state; + mDeviceStateTimestamps[stateId] = time; + + if (mDeviceStateConfig[stateId].isTracked()) { + if (mDeviceStats != null || createDeviceStats()) { + mDeviceStats.setState(stateId, state, time); + } + } + + if (mUidStateConfig[stateId].isTracked()) { + for (int i = mUidStats.size() - 1; i >= 0; i--) { + PowerComponentAggregatedPowerStats.UidStats uidStats = mUidStats.valueAt(i); + if (uidStats.stats != null || createUidStats(uidStats)) { + uidStats.stats.setState(stateId, state, time); + } + } + } + } + + void setUidState(int uid, @PowerStatsAggregator.TrackedState int stateId, int state, + long time) { + if (!mUidStateConfig[stateId].isTracked()) { + return; + } + + UidStats uidStats = getUidStats(uid); + uidStats.states[stateId] = state; + uidStats.stateTimestampMs[stateId] = time; + + if (uidStats.stats != null || createUidStats(uidStats)) { + uidStats.stats.setState(stateId, state, time); + } + } + + boolean isCompatible(PowerStats powerStats) { + return mPowerStatsDescriptor == null || mPowerStatsDescriptor.equals(powerStats.descriptor); + } + + void addPowerStats(PowerStats powerStats, long timestampMs) { + mPowerStatsDescriptor = powerStats.descriptor; + + if (mDeviceStats == null) { + if (mStatsFactory == null) { + mStatsFactory = new MultiStateStats.Factory( + mPowerStatsDescriptor.statsArrayLength, mDeviceStateConfig); + mUidStatsFactory = new MultiStateStats.Factory( + mPowerStatsDescriptor.uidStatsArrayLength, mUidStateConfig); + } + + createDeviceStats(); + } + + mDeviceStats.increment(powerStats.stats, timestampMs); + + for (int i = powerStats.uidStats.size() - 1; i >= 0; i--) { + int uid = powerStats.uidStats.keyAt(i); + PowerComponentAggregatedPowerStats.UidStats uidStats = getUidStats(uid); + if (uidStats.stats == null) { + createUidStats(uidStats); + } + uidStats.stats.increment(powerStats.uidStats.valueAt(i), timestampMs); + } + } + + void reset() { + mPowerStatsDescriptor = null; + mStatsFactory = null; + mUidStatsFactory = null; + mDeviceStats = null; + for (int i = mUidStats.size() - 1; i >= 0; i--) { + mUidStats.valueAt(i).stats = null; + } + } + + private UidStats getUidStats(int uid) { + // TODO(b/292247660): map isolated and sandbox UIDs + UidStats uidStats = mUidStats.get(uid); + if (uidStats == null) { + uidStats = new UidStats(); + uidStats.states = new int[mUidStateConfig.length]; + uidStats.stateTimestampMs = new long[mUidStateConfig.length]; + mUidStats.put(uid, uidStats); + } + return uidStats; + } + + void collectUids(Collection<Integer> uids) { + for (int i = mUidStats.size() - 1; i >= 0; i--) { + if (mUidStats.valueAt(i).stats != null) { + uids.add(mUidStats.keyAt(i)); + } + } + } + + boolean getDeviceStats(long[] outValues, int[] deviceStates) { + if (deviceStates.length != mDeviceStateConfig.length) { + throw new IllegalArgumentException( + "Invalid number of tracked states: " + deviceStates.length + + " expected: " + mDeviceStateConfig.length); + } + if (mDeviceStats != null) { + mDeviceStats.getStats(outValues, deviceStates); + return true; + } + return false; + } + + boolean getUidStats(long[] outValues, int uid, int[] uidStates) { + if (uidStates.length != mUidStateConfig.length) { + throw new IllegalArgumentException( + "Invalid number of tracked states: " + uidStates.length + + " expected: " + mUidStateConfig.length); + } + UidStats uidStats = mUidStats.get(uid); + if (uidStats != null && uidStats.stats != null) { + uidStats.stats.getStats(outValues, uidStates); + return true; + } + return false; + } + + private boolean createDeviceStats() { + if (mStatsFactory == null) { + return false; + } + + mDeviceStats = mStatsFactory.create(); + for (int stateId = 0; stateId < mDeviceStateConfig.length; stateId++) { + mDeviceStats.setState(stateId, mDeviceStates[stateId], + mDeviceStateTimestamps[stateId]); + } + return true; + } + + private boolean createUidStats(UidStats uidStats) { + if (mUidStatsFactory == null) { + return false; + } + + uidStats.stats = mUidStatsFactory.create(); + for (int stateId = 0; stateId < mDeviceStateConfig.length; stateId++) { + uidStats.stats.setState(stateId, mDeviceStates[stateId], + mDeviceStateTimestamps[stateId]); + } + for (int stateId = mDeviceStateConfig.length; stateId < mUidStateConfig.length; stateId++) { + uidStats.stats.setState(stateId, uidStats.states[stateId], + uidStats.stateTimestampMs[stateId]); + } + return true; + } + + void dumpDevice(IndentingPrintWriter ipw) { + if (mDeviceStats != null) { + ipw.println(mPowerStatsDescriptor.name); + ipw.increaseIndent(); + mDeviceStats.dump(ipw); + ipw.decreaseIndent(); + } + } + + void dumpUid(IndentingPrintWriter ipw, int uid) { + UidStats uidStats = mUidStats.get(uid); + if (uidStats != null && uidStats.stats != null) { + ipw.println(mPowerStatsDescriptor.name); + ipw.increaseIndent(); + uidStats.stats.dump(ipw); + ipw.decreaseIndent(); + } + } +} diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java new file mode 100644 index 000000000000..6a1c1da93163 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2023 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.annotation.IntDef; +import android.os.BatteryConsumer; +import android.os.BatteryStats; + +import com.android.internal.os.BatteryStatsHistory; +import com.android.internal.os.BatteryStatsHistoryIterator; +import com.android.internal.os.MultiStateStats; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +class PowerStatsAggregator { + public static final int STATE_POWER = 0; + public static final int STATE_SCREEN = 1; + public static final int STATE_PROCESS_STATE = 2; + + @IntDef({ + STATE_POWER, + STATE_SCREEN, + STATE_PROCESS_STATE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TrackedState { + } + + static final int POWER_STATE_BATTERY = 0; + static final int POWER_STATE_OTHER = 1; // Plugged in, or on wireless charger, etc. + static final String[] STATE_LABELS_POWER = {"pwr-battery", "pwr-other"}; + + static final int SCREEN_STATE_ON = 0; + static final int SCREEN_STATE_OTHER = 1; // Off, doze etc + static final String[] STATE_LABELS_SCREEN = {"scr-on", "scr-other"}; + + static final String[] STATE_LABELS_PROCESS_STATE; + + static { + String[] procStateLabels = new String[BatteryConsumer.PROCESS_STATE_COUNT]; + for (int i = 0; i < BatteryConsumer.PROCESS_STATE_COUNT; i++) { + procStateLabels[i] = BatteryConsumer.processStateToString(i); + } + STATE_LABELS_PROCESS_STATE = procStateLabels; + } + + private final BatteryStatsHistory mHistory; + private final AggregatedPowerStats mStats; + + private PowerStatsAggregator(BatteryStatsHistory history, + AggregatedPowerStats aggregatedPowerStats) { + mHistory = history; + mStats = aggregatedPowerStats; + } + + /** + * Iterates of the battery history and aggregates power stats between the specified times. + * The start and end are specified in the battery-stats monotonic time, which is the + * adjusted elapsed time found in HistoryItem.time. + * <p> + * The aggregated stats are sent to the consumer. One aggregation pass may produce + * multiple sets of aggregated stats if there was an incompatible change that occurred in the + * middle of the recorded battery history. + * <p> + * Note: the AggregatedPowerStats object is reused, so the consumer should fully consume + * the stats in the <code>accept</code> method and never cache it. + */ + void aggregateBatteryStats(long startTimeMs, long endTimeMs, + Consumer<AggregatedPowerStats> consumer) { + mStats.reset(); + + int currentBatteryState = POWER_STATE_BATTERY; + int currentScreenState = SCREEN_STATE_OTHER; + long baseTime = -1; + long lastTime = 0; + try (BatteryStatsHistoryIterator iterator = + mHistory.copy().iterate(startTimeMs, endTimeMs)) { + while (iterator.hasNext()) { + BatteryStats.HistoryItem item = iterator.next(); + + if (baseTime < 0) { + mStats.setStartTime(item.currentTime); + baseTime = item.time; + } + + lastTime = item.time; + + int batteryState = + (item.states & BatteryStats.HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0 + ? POWER_STATE_OTHER : POWER_STATE_BATTERY; + if (batteryState != currentBatteryState) { + mStats.setDeviceState(STATE_POWER, batteryState, item.time); + currentBatteryState = batteryState; + } + + int screenState = + (item.states & BatteryStats.HistoryItem.STATE_SCREEN_ON_FLAG) != 0 + ? SCREEN_STATE_ON : SCREEN_STATE_OTHER; + if (screenState != currentScreenState) { + mStats.setDeviceState(STATE_SCREEN, screenState, item.time); + currentScreenState = screenState; + } + + if (item.processStateChange != null) { + mStats.setUidState(item.processStateChange.uid, STATE_PROCESS_STATE, + item.processStateChange.processState, item.time); + } + + if (item.powerStats != null) { + if (!mStats.isCompatible(item.powerStats)) { + mStats.setDuration(lastTime - baseTime); + consumer.accept(mStats); + mStats.reset(); + mStats.setStartTime(item.currentTime); + baseTime = lastTime = item.time; + } + mStats.addPowerStats(item.powerStats, item.time); + } + } + } + mStats.setDuration(lastTime - baseTime); + consumer.accept(mStats); + } + + static class Builder { + static class PowerComponentAggregateStatsBuilder { + private final int mPowerComponentId; + private @TrackedState int[] mTrackedDeviceStates; + private @TrackedState int[] mTrackedUidStates; + + PowerComponentAggregateStatsBuilder(int powerComponentId) { + this.mPowerComponentId = powerComponentId; + } + + public PowerComponentAggregateStatsBuilder trackDeviceStates( + @TrackedState int... states) { + mTrackedDeviceStates = states; + return this; + } + + public PowerComponentAggregateStatsBuilder trackUidStates(@TrackedState int... states) { + mTrackedUidStates = states; + return this; + } + + private PowerComponentAggregatedPowerStats build() { + MultiStateStats.States[] deviceStates = new MultiStateStats.States[]{ + new MultiStateStats.States(isTracked(mTrackedDeviceStates, STATE_POWER), + PowerStatsAggregator.STATE_LABELS_POWER), + new MultiStateStats.States(isTracked(mTrackedDeviceStates, STATE_SCREEN), + PowerStatsAggregator.STATE_LABELS_SCREEN), + }; + + MultiStateStats.States[] uidStates = new MultiStateStats.States[]{ + new MultiStateStats.States(isTracked(mTrackedUidStates, STATE_POWER), + PowerStatsAggregator.STATE_LABELS_POWER), + new MultiStateStats.States(isTracked(mTrackedUidStates, STATE_SCREEN), + PowerStatsAggregator.STATE_LABELS_SCREEN), + new MultiStateStats.States( + isTracked(mTrackedUidStates, STATE_PROCESS_STATE), + PowerStatsAggregator.STATE_LABELS_PROCESS_STATE), + }; + + switch (mPowerComponentId) { + case BatteryConsumer.POWER_COMPONENT_CPU: + return new CpuAggregatedPowerStats(deviceStates, uidStates); + default: + return new PowerComponentAggregatedPowerStats(mPowerComponentId, + deviceStates, uidStates); + } + } + + private boolean isTracked(int[] trackedStates, int state) { + if (trackedStates == null) { + return false; + } + + for (int trackedState : trackedStates) { + if (trackedState == state) { + return true; + } + } + return false; + } + } + + private final BatteryStatsHistory mHistory; + private final List<PowerComponentAggregateStatsBuilder> mPowerComponents = + new ArrayList<>(); + + Builder(BatteryStatsHistory history) { + mHistory = history; + } + + PowerComponentAggregateStatsBuilder trackPowerComponent(int powerComponentId) { + PowerComponentAggregateStatsBuilder builder = new PowerComponentAggregateStatsBuilder( + powerComponentId); + mPowerComponents.add(builder); + return builder; + } + + PowerStatsAggregator build() { + return new PowerStatsAggregator(mHistory, new AggregatedPowerStats( + mPowerComponents.stream() + .map(PowerComponentAggregateStatsBuilder::build) + .toArray(PowerComponentAggregatedPowerStats[]::new))); + } + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 1d86b88235c0..f22296a6261c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -28,8 +28,8 @@ import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.BatteryStats.HistoryItem; -import android.os.Bundle; import android.os.Parcel; +import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; import android.telephony.NetworkRegistrationInfo; @@ -387,7 +387,8 @@ public class BatteryStatsHistoryTest { @Test public void recordPowerStats() { - PowerStats.Descriptor descriptor = new PowerStats.Descriptor(42, "foo", 1, 2, new Bundle()); + PowerStats.Descriptor descriptor = new PowerStats.Descriptor(42, "foo", 1, 2, + new PersistableBundle()); PowerStats powerStats = new PowerStats(descriptor); powerStats.durationMs = 100; powerStats.stats[0] = 200; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java new file mode 100644 index 000000000000..47de44324ae1 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2023 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 com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import android.os.BatteryConsumer; +import android.os.BatteryStats; +import android.os.PersistableBundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.BatteryStatsHistory; +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PowerStatsAggregatorTest { + private static final int TEST_POWER_COMPONENT = 77; + private static final int TEST_UID = 42; + + private final MockClock mClock = new MockClock(); + private long mStartTime; + private BatteryStatsHistory mHistory; + private PowerStatsAggregator mAggregator; + private int mAggregatedStatsCount; + + @Before + public void setup() throws ParseException { + mHistory = new BatteryStatsHistory(32, 1024, + mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock); + mStartTime = new SimpleDateFormat("yyyy-MM-dd HH:mm") + .parse("2008-09-23 08:00").getTime(); + mClock.currentTime = mStartTime; + + PowerStatsAggregator.Builder builder = new PowerStatsAggregator.Builder(mHistory); + builder.trackPowerComponent(TEST_POWER_COMPONENT) + .trackDeviceStates( + PowerStatsAggregator.STATE_POWER, + PowerStatsAggregator.STATE_SCREEN) + .trackUidStates( + PowerStatsAggregator.STATE_POWER, + PowerStatsAggregator.STATE_SCREEN, + PowerStatsAggregator.STATE_PROCESS_STATE); + mAggregator = builder.build(); + } + + @Test + public void stateUpdates() { + mHistory.forceRecordAllHistory(); + mHistory.recordBatteryState(mClock.realtime, mClock.uptime, 10, /* plugged */ true); + mHistory.recordStateStartEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.STATE_SCREEN_ON_FLAG); + mHistory.recordProcessStateChange(mClock.realtime, mClock.uptime, TEST_UID, + BatteryConsumer.PROCESS_STATE_FOREGROUND); + + advance(1000); + + PowerStats.Descriptor descriptor = + new PowerStats.Descriptor(TEST_POWER_COMPONENT, "majorDrain", 1, 1, + new PersistableBundle()); + PowerStats powerStats = new PowerStats(descriptor); + powerStats.stats = new long[]{10000}; + powerStats.uidStats.put(TEST_UID, new long[]{1234}); + mHistory.recordPowerStats(mClock.realtime, mClock.uptime, powerStats); + + mHistory.recordBatteryState(mClock.realtime, mClock.uptime, 90, /* plugged */ false); + mHistory.recordStateStopEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.STATE_SCREEN_ON_FLAG); + + advance(1000); + + mHistory.recordProcessStateChange(mClock.realtime, mClock.uptime, TEST_UID, + BatteryConsumer.PROCESS_STATE_BACKGROUND); + + advance(3000); + + powerStats.stats = new long[]{20000}; + powerStats.uidStats.put(TEST_UID, new long[]{4444}); + mHistory.recordPowerStats(mClock.realtime, mClock.uptime, powerStats); + + mAggregator.aggregateBatteryStats(0, 0, stats -> { + assertThat(mAggregatedStatsCount++).isEqualTo(0); + assertThat(stats.getStartTime()).isEqualTo(mStartTime); + assertThat(stats.getDuration()).isEqualTo(5000); + + long[] values = new long[1]; + + PowerComponentAggregatedPowerStats powerComponentStats = stats.getPowerComponentStats( + TEST_POWER_COMPONENT); + + assertThat(powerComponentStats.getDeviceStats(values, new int[]{ + PowerStatsAggregator.POWER_STATE_OTHER, + PowerStatsAggregator.SCREEN_STATE_ON})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{10000}); + + assertThat(powerComponentStats.getDeviceStats(values, new int[]{ + PowerStatsAggregator.POWER_STATE_BATTERY, + PowerStatsAggregator.SCREEN_STATE_OTHER})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{20000}); + + assertThat(powerComponentStats.getUidStats(values, TEST_UID, new int[]{ + PowerStatsAggregator.POWER_STATE_OTHER, + PowerStatsAggregator.SCREEN_STATE_ON, + BatteryConsumer.PROCESS_STATE_FOREGROUND})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{1234}); + + assertThat(powerComponentStats.getUidStats(values, TEST_UID, new int[]{ + PowerStatsAggregator.POWER_STATE_BATTERY, + PowerStatsAggregator.SCREEN_STATE_OTHER, + BatteryConsumer.PROCESS_STATE_FOREGROUND})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{1111}); + + assertThat(powerComponentStats.getUidStats(values, TEST_UID, new int[]{ + PowerStatsAggregator.POWER_STATE_BATTERY, + PowerStatsAggregator.SCREEN_STATE_OTHER, + BatteryConsumer.PROCESS_STATE_BACKGROUND})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{3333}); + }); + } + + @Test + public void incompatiblePowerStats() { + mHistory.forceRecordAllHistory(); + mHistory.recordBatteryState(mClock.realtime, mClock.uptime, 10, /* plugged */ true); + mHistory.recordProcessStateChange(mClock.realtime, mClock.uptime, TEST_UID, + BatteryConsumer.PROCESS_STATE_FOREGROUND); + + advance(1000); + + PowerStats.Descriptor descriptor = + new PowerStats.Descriptor(TEST_POWER_COMPONENT, "majorDrain", 1, 1, + new PersistableBundle()); + PowerStats powerStats = new PowerStats(descriptor); + powerStats.stats = new long[]{10000}; + powerStats.uidStats.put(TEST_UID, new long[]{1234}); + mHistory.recordPowerStats(mClock.realtime, mClock.uptime, powerStats); + + mHistory.recordBatteryState(mClock.realtime, mClock.uptime, 90, /* plugged */ false); + + advance(1000); + + descriptor = new PowerStats.Descriptor(TEST_POWER_COMPONENT, "majorDrain", 1, 1, + PersistableBundle.forPair("something", "changed")); + powerStats = new PowerStats(descriptor); + powerStats.stats = new long[]{20000}; + powerStats.uidStats.put(TEST_UID, new long[]{4444}); + mHistory.recordPowerStats(mClock.realtime, mClock.uptime, powerStats); + + advance(1000); + + mHistory.recordBatteryState(mClock.realtime, mClock.uptime, 50, /* plugged */ true); + + mAggregator.aggregateBatteryStats(0, 0, stats -> { + long[] values = new long[1]; + + PowerComponentAggregatedPowerStats powerComponentStats = + stats.getPowerComponentStats(TEST_POWER_COMPONENT); + + if (mAggregatedStatsCount == 0) { + assertThat(stats.getStartTime()).isEqualTo(mStartTime); + assertThat(stats.getDuration()).isEqualTo(2000); + + assertThat(powerComponentStats.getDeviceStats(values, new int[]{ + PowerStatsAggregator.POWER_STATE_OTHER, + PowerStatsAggregator.SCREEN_STATE_ON})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{10000}); + assertThat(powerComponentStats.getUidStats(values, TEST_UID, new int[]{ + PowerStatsAggregator.POWER_STATE_OTHER, + PowerStatsAggregator.SCREEN_STATE_ON, + BatteryConsumer.PROCESS_STATE_FOREGROUND})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{1234}); + } else if (mAggregatedStatsCount == 1) { + assertThat(stats.getStartTime()).isEqualTo(mStartTime + 2000); + assertThat(stats.getDuration()).isEqualTo(1000); + + assertThat(powerComponentStats.getDeviceStats(values, new int[]{ + PowerStatsAggregator.POWER_STATE_BATTERY, + PowerStatsAggregator.SCREEN_STATE_ON})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{20000}); + assertThat(powerComponentStats.getUidStats(values, TEST_UID, new int[]{ + PowerStatsAggregator.POWER_STATE_BATTERY, + PowerStatsAggregator.SCREEN_STATE_ON, + BatteryConsumer.PROCESS_STATE_FOREGROUND})) + .isTrue(); + assertThat(values).isEqualTo(new long[]{4444}); + } else { + fail(); + } + mAggregatedStatsCount++; + }); + } + + private void advance(long durationMs) { + mClock.realtime += durationMs; + mClock.uptime += durationMs; + mClock.currentTime += durationMs; + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java index f02164d49a55..330f698277f8 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java @@ -18,10 +18,10 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; -import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; +import android.os.PersistableBundle; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -50,7 +50,7 @@ public class PowerStatsCollectorTest { mMockClock) { @Override protected PowerStats collectStats() { - return new PowerStats(new PowerStats.Descriptor(0, 0, 0, new Bundle())); + return new PowerStats(new PowerStats.Descriptor(0, 0, 0, new PersistableBundle())); } }; mCollector.addConsumer(stats -> mCollectedStats = stats); |