diff options
| author | 2023-08-19 05:37:24 +0000 | |
|---|---|---|
| committer | 2023-08-19 05:37:24 +0000 | |
| commit | 5e309ba6abbb0e4c1fd2fca834b213d5af220c2b (patch) | |
| tree | eb33544bbe1d6ba65abad9cc6ea10bc17d2b473f | |
| parent | f92b264a182b5a5132e49c410905351920c88ec2 (diff) | |
| parent | 0db0328f46d6724a99c42adc224d3172151dcc44 (diff) | |
Merge "Add CpuPowerStatsCollector for reading power-related CPU stats from kernel" into main
19 files changed, 1024 insertions, 31 deletions
diff --git a/core/java/com/android/internal/os/OWNERS b/core/java/com/android/internal/os/OWNERS index 0b9773e2c907..e35b7f184663 100644 --- a/core/java/com/android/internal/os/OWNERS +++ b/core/java/com/android/internal/os/OWNERS @@ -11,6 +11,7 @@ per-file BatteryUsageStats* = file:/BATTERY_STATS_OWNERS per-file *ChargeCalculator* = file:/BATTERY_STATS_OWNERS per-file *PowerCalculator* = file:/BATTERY_STATS_OWNERS per-file *PowerEstimator* = file:/BATTERY_STATS_OWNERS +per-file *PowerStats* = file:/BATTERY_STATS_OWNERS per-file *Kernel* = file:/BATTERY_STATS_OWNERS per-file *MultiState* = file:/BATTERY_STATS_OWNERS per-file *PowerProfile* = file:/BATTERY_STATS_OWNERS diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java new file mode 100644 index 000000000000..1169552e165d --- /dev/null +++ b/core/java/com/android/internal/os/PowerStats.java @@ -0,0 +1,67 @@ +/* + * 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.internal.os; + +import android.os.BatteryConsumer; +import android.util.IndentingPrintWriter; +import android.util.SparseArray; + +import java.util.Arrays; + +/** + * Container for power stats, acquired by various PowerStatsCollector classes. See subclasses for + * details. + */ +public final class PowerStats { + /** + * Power component (e.g. CPU, WIFI etc) that this snapshot relates to. + */ + public @BatteryConsumer.PowerComponent int powerComponentId; + + /** + * Duration, in milliseconds, covered by this snapshot. + */ + public long durationMs; + + /** + * Device-wide stats. + */ + public long[] stats; + + /** + * Per-UID CPU stats. + */ + public final SparseArray<long[]> uidStats = new SparseArray<>(); + + /** + * Prints the contents of the stats snapshot. + */ + public void dump(IndentingPrintWriter pw) { + pw.print("PowerStats: "); + pw.println(BatteryConsumer.powerComponentIdToString(powerComponentId)); + pw.increaseIndent(); + pw.print("duration", durationMs).println(); + for (int i = 0; i < uidStats.size(); i++) { + pw.print("UID "); + pw.print(uidStats.keyAt(i)); + pw.print(": "); + pw.print(Arrays.toString(uidStats.valueAt(i))); + pw.println(); + } + pw.decreaseIndent(); + } +} diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e0b656565ce1..aeeaacaacf11 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6624,11 +6624,6 @@ <!-- Whether to show weather on the lock screen by default. --> <bool name="config_lockscreenWeatherEnabledByDefault">false</bool> - <!-- Whether to reset Battery Stats on unplug when the battery level is high. --> - <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool> - <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged --> - <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool> - <!-- Whether we should persist the brightness value in nits for the default display even if the underlying display device changes. --> <bool name="config_persistBrightnessNitsForDefaultDisplay">false</bool> diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml new file mode 100644 index 000000000000..8fb48bcb8947 --- /dev/null +++ b/core/res/res/values/config_battery_stats.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + --> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds. Do not translate. + + NOTE: The naming convention is "config_camelCaseValue". --> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Whether to reset Battery Stats on unplug when the battery level is high. --> + <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool> + <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged --> + <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool> + + <!-- CPU power stats collection throttle period in milliseconds. Since power stats collection + is a relatively expensive operation, this throttle period may need to be adjusted for low-power + devices--> + <integer name="config_defaultPowerStatsThrottlePeriodCpu">60000</integer> +</resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4918bbefa900..8330f7bdc251 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5089,6 +5089,7 @@ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" /> <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> + <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodCpu" /> <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/> <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/> diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 04ebb2b14af6..b0f30d6875ae 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -156,6 +156,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub private final PowerProfile mPowerProfile; private final CpuScalingPolicies mCpuScalingPolicies; + private final BatteryStatsImpl.BatteryStatsConfig mBatteryStatsConfig; final BatteryStatsImpl mStats; final CpuWakeupStats mCpuWakeupStats; private final BatteryUsageStatsStore mBatteryUsageStatsStore; @@ -374,22 +375,24 @@ public final class BatteryStatsService extends IBatteryStats.Stub mPowerProfile = new PowerProfile(context); mCpuScalingPolicies = new CpuScalingPolicyReader().read(); - mStats = new BatteryStatsImpl(systemDir, handler, this, - this, mUserManagerUserInfoProvider, mPowerProfile, mCpuScalingPolicies); - mWorker = new BatteryExternalStatsWorker(context, mStats); - mStats.setExternalStatsSyncLocked(mWorker); - mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger( - com.android.internal.R.integer.config_radioScanningTimeout) * 1000L); - final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean( com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel); final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean( com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge); - mStats.setBatteryStatsConfig( + final long powerStatsThrottlePeriodCpu = context.getResources().getInteger( + com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodCpu); + mBatteryStatsConfig = new BatteryStatsImpl.BatteryStatsConfig.Builder() .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel) .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge) - .build()); + .setPowerStatsThrottlePeriodCpu(powerStatsThrottlePeriodCpu) + .build(); + mStats = new BatteryStatsImpl(mBatteryStatsConfig, systemDir, handler, this, + this, mUserManagerUserInfoProvider, mPowerProfile, mCpuScalingPolicies); + mWorker = new BatteryExternalStatsWorker(context, mStats); + mStats.setExternalStatsSyncLocked(mWorker); + mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger( + com.android.internal.R.integer.config_radioScanningTimeout) * 1000L); mStats.startTrackingSystemServerCpuTime(); if (BATTERY_USAGE_STORE_ENABLED) { @@ -2591,6 +2594,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub pw.println(" --proto: output as a binary protobuffer"); pw.println(" --model power-profile: use the power profile model" + " even if measured energy is available"); + pw.println(" --sample: collect and dump a sample of stats for debugging purpose"); pw.println(" <package.name>: optional name of package to filter output by."); pw.println(" -h: print this help text."); pw.println("Battery stats (batterystats) commands:"); @@ -2623,6 +2627,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } + private void dumpStatsSample(PrintWriter pw) { + mStats.dumpStatsSample(pw); + } + private void dumpMeasuredEnergyStats(PrintWriter pw) { // Wait for the completion of pending works if there is any awaitCompletion(); @@ -2864,6 +2872,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "), SystemClock.elapsedRealtime()); return; + } else if ("--sample".equals(arg)) { + dumpStatsSample(pw); + return; } else if ("-a".equals(arg)) { flags |= BatteryStats.DUMP_VERBOSE; } else if (arg.length() > 0 && arg.charAt(0) == '-'){ @@ -2924,6 +2935,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub in.unmarshall(raw, 0, raw.length); in.setDataPosition(0); BatteryStatsImpl checkinStats = new BatteryStatsImpl( + mBatteryStatsConfig, null, mStats.mHandler, null, null, mUserManagerUserInfoProvider, mPowerProfile, mCpuScalingPolicies); @@ -2965,6 +2977,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub in.unmarshall(raw, 0, raw.length); in.setDataPosition(0); BatteryStatsImpl checkinStats = new BatteryStatsImpl( + mBatteryStatsConfig, null, mStats.mHandler, null, null, mUserManagerUserInfoProvider, mPowerProfile, mCpuScalingPolicies); 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 cf4e845a273b..5b10afadc0fc 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -281,6 +281,7 @@ public class BatteryStatsImpl extends BatteryStats { private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>(); private int[] mCpuPowerBracketMap; private final CpuUsageDetails mCpuUsageDetails = new CpuUsageDetails(); + private final CpuPowerStatsCollector mCpuPowerStatsCollector; public LongSparseArray<SamplingTimer> getKernelMemoryStats() { return mKernelMemoryStats; @@ -439,6 +440,7 @@ public class BatteryStatsImpl extends BatteryStats { static final int RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG = 1 << 1; private final int mFlags; + private final long mPowerStatsThrottlePeriodCpu; private BatteryStatsConfig(Builder builder) { int flags = 0; @@ -449,6 +451,7 @@ public class BatteryStatsImpl extends BatteryStats { flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG; } mFlags = flags; + mPowerStatsThrottlePeriodCpu = builder.mPowerStatsThrottlePeriodCpu; } /** @@ -469,15 +472,22 @@ public class BatteryStatsImpl extends BatteryStats { == RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG; } + long getPowerStatsThrottlePeriodCpu() { + return mPowerStatsThrottlePeriodCpu; + } + /** * Builder for BatteryStatsConfig */ public static class Builder { private boolean mResetOnUnplugHighBatteryLevel; private boolean mResetOnUnplugAfterSignificantCharge; + private long mPowerStatsThrottlePeriodCpu; + public Builder() { mResetOnUnplugHighBatteryLevel = true; mResetOnUnplugAfterSignificantCharge = true; + mPowerStatsThrottlePeriodCpu = 60000; } /** @@ -504,8 +514,16 @@ public class BatteryStatsImpl extends BatteryStats { mResetOnUnplugAfterSignificantCharge = reset; return this; } - } + /** + * Sets the minimum amount of time (in millis) to wait between passes + * of CPU power stats collection. + */ + public Builder setPowerStatsThrottlePeriodCpu(long periodMs) { + mPowerStatsThrottlePeriodCpu = periodMs; + return this; + } + } } private final PlatformIdleStateCallback mPlatformIdleStateCallback; @@ -1556,7 +1574,7 @@ public class BatteryStatsImpl extends BatteryStats { @VisibleForTesting @GuardedBy("this") - protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build(); + protected BatteryStatsConfig mBatteryStatsConfig; @GuardedBy("this") private AlarmManager mAlarmManager = null; @@ -1733,6 +1751,7 @@ public class BatteryStatsImpl extends BatteryStats { public BatteryStatsImpl(Clock clock, File historyDirectory) { init(clock); + mBatteryStatsConfig = new BatteryStatsConfig.Builder().build(); mHandler = null; mConstants = new Constants(mHandler); mStartClockTimeMs = clock.currentTimeMillis(); @@ -1751,6 +1770,7 @@ public class BatteryStatsImpl extends BatteryStats { mPlatformIdleStateCallback = null; mEnergyConsumerRetriever = null; mUserInfoProvider = null; + mCpuPowerStatsCollector = null; } private void init(Clock clock) { @@ -10911,21 +10931,23 @@ public class BatteryStatsImpl extends BatteryStats { return mTmpCpuTimeInFreq; } - public BatteryStatsImpl(@Nullable File systemDir, @NonNull Handler handler, - @Nullable PlatformIdleStateCallback cb, @Nullable EnergyStatsRetriever energyStatsCb, + public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @Nullable File systemDir, + @NonNull Handler handler, @Nullable PlatformIdleStateCallback cb, + @Nullable EnergyStatsRetriever energyStatsCb, @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies) { - this(Clock.SYSTEM_CLOCK, systemDir, handler, cb, energyStatsCb, userInfoProvider, + this(config, Clock.SYSTEM_CLOCK, systemDir, handler, cb, energyStatsCb, userInfoProvider, powerProfile, cpuScalingPolicies); } - private BatteryStatsImpl(@NonNull Clock clock, @Nullable File systemDir, - @NonNull Handler handler, @Nullable PlatformIdleStateCallback cb, - @Nullable EnergyStatsRetriever energyStatsCb, + private BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock, + @Nullable File systemDir, @NonNull Handler handler, + @Nullable PlatformIdleStateCallback cb, @Nullable EnergyStatsRetriever energyStatsCb, @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies) { init(clock); + mBatteryStatsConfig = config; mHandler = new MyHandler(handler.getLooper()); mConstants = new Constants(mHandler); @@ -10947,6 +10969,10 @@ public class BatteryStatsImpl extends BatteryStats { mHistory = new BatteryStatsHistory(systemDir, mConstants.MAX_HISTORY_FILES, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock); } + + mCpuPowerStatsCollector = new CpuPowerStatsCollector(mCpuScalingPolicies, mPowerProfile, + mHandler, mBatteryStatsConfig.getPowerStatsThrottlePeriodCpu()); + mStartCount++; initTimersAndCounters(); mOnBattery = mOnBatteryInternal = false; @@ -11095,15 +11121,6 @@ public class BatteryStatsImpl extends BatteryStats { } /** - * Injects BatteryStatsConfig - */ - public void setBatteryStatsConfig(BatteryStatsConfig config) { - synchronized (this) { - mBatteryStatsConfig = config; - } - } - - /** * Starts tracking CPU time-in-state for threads of the system server process, * keeping a separate account of threads receiving incoming binder calls. */ @@ -14197,6 +14214,9 @@ public class BatteryStatsImpl extends BatteryStats { if (mCpuUidFreqTimeReader != null) { mCpuUidFreqTimeReader.onSystemReady(); } + if (mCpuPowerStatsCollector != null) { + mCpuPowerStatsCollector.onSystemReady(); + } mSystemReady = true; } @@ -15705,6 +15725,13 @@ public class BatteryStatsImpl extends BatteryStats { iPw.decreaseIndent(); } + /** + * Grabs one sample of PowerStats and prints it. + */ + public void dumpStatsSample(PrintWriter pw) { + mCpuPowerStatsCollector.collectAndDump(pw); + } + private final Runnable mWriteAsyncRunnable = () -> { synchronized (BatteryStatsImpl.this) { writeSyncLocked(); diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java new file mode 100644 index 000000000000..14017467e60f --- /dev/null +++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java @@ -0,0 +1,134 @@ +/* + * 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.Handler; +import android.util.SparseArray; + +import com.android.internal.annotations.Keep; +import com.android.internal.annotations.VisibleForNative; +import com.android.internal.os.Clock; +import com.android.internal.os.CpuScalingPolicies; +import com.android.internal.os.PowerProfile; +import com.android.internal.os.PowerStats; +import com.android.server.power.optimization.Flags; + +/** + * Collects snapshots of power-related system statistics. + * <p> + * The class is intended to be used in a serialized fashion using the handler supplied in the + * constructor. Thus the object is not thread-safe except where noted. + */ +public class CpuPowerStatsCollector extends PowerStatsCollector { + private static final long NANOS_PER_MILLIS = 1000000; + + private final KernelCpuStatsReader mKernelCpuStatsReader; + private final int[] mScalingStepToPowerBracketMap; + private final long[] mTempUidStats; + private final SparseArray<UidStats> mUidStats = new SparseArray<>(); + private final int mUidStatsSize; + // Reusable instance + private final PowerStats mCpuPowerStats = new PowerStats(); + private long mLastUpdateTimestampNanos; + + public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile, + Handler handler, long throttlePeriodMs) { + this(cpuScalingPolicies, powerProfile, handler, new KernelCpuStatsReader(), + throttlePeriodMs, Clock.SYSTEM_CLOCK); + } + + public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile, + Handler handler, KernelCpuStatsReader kernelCpuStatsReader, + long throttlePeriodMs, Clock clock) { + super(handler, throttlePeriodMs, clock); + mKernelCpuStatsReader = kernelCpuStatsReader; + + int scalingStepCount = cpuScalingPolicies.getScalingStepCount(); + mScalingStepToPowerBracketMap = new int[scalingStepCount]; + int index = 0; + for (int policy : cpuScalingPolicies.getPolicies()) { + int[] frequencies = cpuScalingPolicies.getFrequencies(policy); + for (int step = 0; step < frequencies.length; step++) { + int bracket = powerProfile.getCpuPowerBracketForScalingStep(policy, step); + mScalingStepToPowerBracketMap[index++] = bracket; + } + } + mUidStatsSize = powerProfile.getCpuPowerBracketCount(); + mTempUidStats = new long[mUidStatsSize]; + } + + /** + * Initializes the collector during the boot sequence. + */ + public void onSystemReady() { + setEnabled(Flags.streamlinedBatteryStats()); + } + + @Override + protected PowerStats collectStats() { + mCpuPowerStats.uidStats.clear(); + long newTimestampNanos = mKernelCpuStatsReader.nativeReadCpuStats( + this::processUidStats, mScalingStepToPowerBracketMap, mLastUpdateTimestampNanos, + mTempUidStats); + mCpuPowerStats.durationMs = + (newTimestampNanos - mLastUpdateTimestampNanos) / NANOS_PER_MILLIS; + mLastUpdateTimestampNanos = newTimestampNanos; + return mCpuPowerStats; + } + + @VisibleForNative + interface KernelCpuStatsCallback { + @Keep // Called from native + void processUidStats(int uid, long[] stats); + } + + private void processUidStats(int uid, long[] stats) { + UidStats uidStats = mUidStats.get(uid); + if (uidStats == null) { + uidStats = new UidStats(); + uidStats.stats = new long[mUidStatsSize]; + uidStats.delta = new long[mUidStatsSize]; + mUidStats.put(uid, uidStats); + } + + boolean nonzero = false; + for (int i = mUidStatsSize - 1; i >= 0; i--) { + long delta = uidStats.delta[i] = stats[i] - uidStats.stats[i]; + if (delta != 0) { + nonzero = true; + } + uidStats.stats[i] = stats[i]; + } + if (nonzero) { + mCpuPowerStats.uidStats.put(uid, uidStats.delta); + } + } + + /** + * Native class that retrieves CPU stats from the kernel. + */ + public static class KernelCpuStatsReader { + protected native long nativeReadCpuStats(KernelCpuStatsCallback callback, + int[] scalingStepToPowerBracketMap, long lastUpdateTimestampNanos, + long[] tempForUidStats); + } + + private static class UidStats { + public long[] stats; + public long[] delta; + } +} diff --git a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java new file mode 100644 index 000000000000..b49c89fcf6ad --- /dev/null +++ b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java @@ -0,0 +1,183 @@ +/* + * 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.ConditionVariable; +import android.os.Handler; +import android.util.FastImmutableArraySet; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.Clock; +import com.android.internal.os.PowerStats; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Collects snapshots of power-related system statistics. + * <p> + * Instances of this class are intended to be used in a serialized fashion using + * the handler supplied in the constructor. Thus these objects are not thread-safe + * except where noted. + */ +public abstract class PowerStatsCollector { + private final Handler mHandler; + private final Clock mClock; + private final long mThrottlePeriodMs; + private final Runnable mCollectAndDeliverStats = this::collectAndDeliverStats; + private boolean mEnabled; + private long mLastScheduledUpdateMs = -1; + + @GuardedBy("this") + @SuppressWarnings("unchecked") + private volatile FastImmutableArraySet<Consumer<PowerStats>> mConsumerList = + new FastImmutableArraySet<Consumer<PowerStats>>(new Consumer[0]); + + public PowerStatsCollector(Handler handler, long throttlePeriodMs, Clock clock) { + mHandler = handler; + mThrottlePeriodMs = throttlePeriodMs; + mClock = clock; + } + + /** + * Adds a consumer that will receive a callback every time a snapshot of stats is collected. + * The method is thread safe. + */ + @SuppressWarnings("unchecked") + public void addConsumer(Consumer<PowerStats> consumer) { + synchronized (this) { + mConsumerList = new FastImmutableArraySet<Consumer<PowerStats>>( + Stream.concat(mConsumerList.stream(), Stream.of(consumer)) + .toArray(Consumer[]::new)); + } + } + + /** + * Removes a consumer. + * The method is thread safe. + */ + @SuppressWarnings("unchecked") + public void removeConsumer(Consumer<PowerStats> consumer) { + synchronized (this) { + mConsumerList = new FastImmutableArraySet<Consumer<PowerStats>>( + mConsumerList.stream().filter(c -> c != consumer) + .toArray(Consumer[]::new)); + } + } + + /** + * Should be called at most once, before the first invocation of {@link #schedule} or + * {@link #forceSchedule} + */ + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + /** + * Returns true if the collector is enabled. + */ + public boolean isEnabled() { + return mEnabled; + } + + @SuppressWarnings("GuardedBy") // Field is volatile + private void collectAndDeliverStats() { + PowerStats stats = collectStats(); + for (Consumer<PowerStats> consumer : mConsumerList) { + consumer.accept(stats); + } + } + + /** + * Schedules a stats snapshot collection, throttled in accordance with the + * {@link #mThrottlePeriodMs} parameter. + */ + public boolean schedule() { + if (!mEnabled) { + return false; + } + + long uptimeMillis = mClock.uptimeMillis(); + if (uptimeMillis - mLastScheduledUpdateMs < mThrottlePeriodMs + && mLastScheduledUpdateMs >= 0) { + return false; + } + mLastScheduledUpdateMs = uptimeMillis; + mHandler.post(mCollectAndDeliverStats); + return true; + } + + /** + * Schedules an immediate snapshot collection, foregoing throttling. + */ + public boolean forceSchedule() { + if (!mEnabled) { + return false; + } + + mHandler.removeCallbacks(mCollectAndDeliverStats); + mHandler.postAtFrontOfQueue(mCollectAndDeliverStats); + return true; + } + + protected abstract PowerStats collectStats(); + + /** + * Collects a fresh stats snapshot and prints it to the supplied printer. + */ + public void collectAndDump(PrintWriter pw) { + if (Thread.currentThread() == mHandler.getLooper().getThread()) { + throw new RuntimeException( + "Calling this method from the handler thread would cause a deadlock"); + } + + IndentingPrintWriter out = new IndentingPrintWriter(pw); + out.print(getClass().getSimpleName()); + if (!isEnabled()) { + out.println(": disabled"); + return; + } + out.println(); + + ArrayList<PowerStats> collected = new ArrayList<>(); + Consumer<PowerStats> consumer = collected::add; + addConsumer(consumer); + + try { + if (forceSchedule()) { + awaitCompletion(); + } + } finally { + removeConsumer(consumer); + } + + out.increaseIndent(); + for (PowerStats stats : collected) { + stats.dump(out); + } + out.decreaseIndent(); + } + + private void awaitCompletion() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 405b133a6de1..2f150a12dbf0 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -50,6 +50,7 @@ cc_library_static { "com_android_server_locksettings_SyntheticPasswordManager.cpp", "com_android_server_power_PowerManagerService.cpp", "com_android_server_powerstats_PowerStatsService.cpp", + "com_android_server_power_stats_CpuPowerStatsCollector.cpp", "com_android_server_hint_HintManagerService.cpp", "com_android_server_SerialService.cpp", "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp", @@ -144,6 +145,7 @@ cc_defaults { "libsensorservicehidl", "libsensorserviceaidl", "libgui", + "libtimeinstate", "libtimestats_atoms_proto", "libusbhost", "libtinyalsa", diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index d9acf4182736..7e8ce60b8a50 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -23,6 +23,7 @@ per-file com_android_server_net_* = file:/services/core/java/com/android/server/ per-file com_android_server_pm_* = file:/services/core/java/com/android/server/pm/OWNERS per-file com_android_server_power_* = file:/services/core/java/com/android/server/power/OWNERS per-file com_android_server_powerstats_* = file:/services/core/java/com/android/server/powerstats/OWNERS +per-file com_android_server_power_stats_* = file:/BATTERY_STATS_OWNERS per-file com_android_server_security_* = file:/core/java/android/security/OWNERS per-file com_android_server_tv_* = file:/media/java/android/media/tv/OWNERS per-file com_android_server_vibrator_* = file:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp new file mode 100644 index 000000000000..a6084ea17806 --- /dev/null +++ b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp @@ -0,0 +1,135 @@ +/* + * 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. + */ + +#define LOG_TAG "CpuPowerStatsCollector" + +#include <cputimeinstate.h> +#include <log/log.h> +#include <nativehelper/ScopedPrimitiveArray.h> + +#include "core_jni_helpers.h" + +#define EXCEPTION (-1) + +namespace android { + +#define JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "com/android/server/power/stats/CpuPowerStatsCollector" +#define JAVA_CLASS_KERNEL_CPU_STATS_READER \ + JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "$KernelCpuStatsReader" +#define JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK \ + JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "$KernelCpuStatsCallback" + +static constexpr uint64_t NSEC_PER_MSEC = 1000000; + +static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> ×, + ScopedIntArrayRO &scopedScalingStepToPowerBracketMap, + jlongArray tempForUidStats); + +static bool initialized = false; +static jclass class_KernelCpuStatsCallback; +static jmethodID method_KernelCpuStatsCallback_processUidStats; + +static int init(JNIEnv *env) { + jclass temp = env->FindClass(JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK); + class_KernelCpuStatsCallback = (jclass)env->NewGlobalRef(temp); + if (!class_KernelCpuStatsCallback) { + jniThrowExceptionFmt(env, "java/lang/ClassNotFoundException", + "Class not found: " JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK); + return EXCEPTION; + } + method_KernelCpuStatsCallback_processUidStats = + env->GetMethodID(class_KernelCpuStatsCallback, "processUidStats", "(I[J)V"); + if (!method_KernelCpuStatsCallback_processUidStats) { + jniThrowExceptionFmt(env, "java/lang/NoSuchMethodException", + "Method not found: " JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK + ".processUidStats"); + return EXCEPTION; + } + initialized = true; + return OK; +} + +static jlong nativeReadCpuStats(JNIEnv *env, [[maybe_unused]] jobject zis, jobject callback, + jintArray scalingStepToPowerBracketMap, + jlong lastUpdateTimestampNanos, jlongArray tempForUidStats) { + if (!initialized) { + if (init(env) == EXCEPTION) { + return 0L; + } + } + + uint64_t newLastUpdate = lastUpdateTimestampNanos; + auto data = android::bpf::getUidsUpdatedCpuFreqTimes(&newLastUpdate); + if (!data.has_value()) return lastUpdateTimestampNanos; + + ScopedIntArrayRO scopedScalingStepToPowerBracketMap(env, scalingStepToPowerBracketMap); + + for (auto &[uid, times] : *data) { + int status = + extractUidStats(env, times, scopedScalingStepToPowerBracketMap, tempForUidStats); + if (status == EXCEPTION) { + return 0L; + } + env->CallVoidMethod(callback, method_KernelCpuStatsCallback_processUidStats, (jint)uid, + tempForUidStats); + } + return newLastUpdate; +} + +static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> ×, + ScopedIntArrayRO &scopedScalingStepToPowerBracketMap, + jlongArray tempForUidStats) { + ScopedLongArrayRW scopedTempForStats(env, tempForUidStats); + uint64_t *arrayForStats = reinterpret_cast<uint64_t *>(scopedTempForStats.get()); + const uint8_t statsSize = scopedTempForStats.size(); + memset(arrayForStats, 0, statsSize * sizeof(uint64_t)); + const uint8_t scalingStepCount = scopedScalingStepToPowerBracketMap.size(); + + uint32_t scalingStep = 0; + for (const auto &subVec : times) { + for (uint32_t i = 0; i < subVec.size(); ++i) { + if (scalingStep >= scalingStepCount) { + jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException", + "scalingStepToPowerBracketMap is too short, " + "size=%u, scalingStep=%u", + scalingStepCount, scalingStep); + return EXCEPTION; + } + uint32_t bucket = scopedScalingStepToPowerBracketMap[scalingStep]; + if (bucket >= statsSize) { + jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException", + "UidStats array is too short, length=%u, bucket[%u]=%u", + statsSize, scalingStep, bucket); + return EXCEPTION; + } + arrayForStats[bucket] += subVec[i] / NSEC_PER_MSEC; + scalingStep++; + } + } + return OK; +} + +static const JNINativeMethod method_table[] = { + {"nativeReadCpuStats", "(L" JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK ";[IJ[J)J", + (void *)nativeReadCpuStats}, +}; + +int register_android_server_power_stats_CpuPowerStatsCollector(JNIEnv *env) { + return jniRegisterNativeMethods(env, JAVA_CLASS_KERNEL_CPU_STATS_READER, method_table, + NELEM(method_table)); +} + +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 290ad8de9547..97d7be6a718e 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -30,6 +30,7 @@ int register_android_server_InputManager(JNIEnv* env); int register_android_server_LightsService(JNIEnv* env); int register_android_server_PowerManagerService(JNIEnv* env); int register_android_server_PowerStatsService(JNIEnv* env); +int register_android_server_power_stats_CpuPowerStatsCollector(JNIEnv* env); int register_android_server_HintManagerService(JNIEnv* env); int register_android_server_storage_AppFuse(JNIEnv* env); int register_android_server_SerialService(JNIEnv* env); @@ -85,6 +86,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_broadcastradio_Tuner(vm, env); register_android_server_PowerManagerService(env); register_android_server_PowerStatsService(env); + register_android_server_power_stats_CpuPowerStatsCollector(env); register_android_server_HintManagerService(env); register_android_server_SerialService(env); register_android_server_InputManager(env); diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index 05acd9b8eeb1..5ea1929e185f 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -23,6 +23,7 @@ android_test { "androidx.test.uiautomator_uiautomator", "mockito-target-minus-junit4", "servicestests-utils", + "flag-junit", ], libs: [ diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java new file mode 100644 index 000000000000..f2ee6dbf4ed6 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java @@ -0,0 +1,137 @@ +/* + * 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.CpuScalingPolicies; +import com.android.internal.os.PowerProfile; +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CpuPowerStatsCollectorTest { + private final MockClock mMockClock = new MockClock(); + private final HandlerThread mHandlerThread = new HandlerThread("test"); + private Handler mHandler; + private CpuPowerStatsCollector mCollector; + private PowerStats mCollectedStats; + @Mock + private PowerProfile mPowerProfile; + @Mock + private CpuPowerStatsCollector.KernelCpuStatsReader mMockKernelCpuStatsReader; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mHandlerThread.start(); + mHandler = mHandlerThread.getThreadHandler(); + when(mPowerProfile.getCpuPowerBracketCount()).thenReturn(2); + when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 0)).thenReturn(0); + when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 1)).thenReturn(1); + mCollector = new CpuPowerStatsCollector(new CpuScalingPolicies( + new SparseArray<>() {{ + put(0, new int[]{0}); + }}, + new SparseArray<>() {{ + put(0, new int[]{1, 12}); + }}), + mPowerProfile, mHandler, mMockKernelCpuStatsReader, 60_000, mMockClock); + mCollector.addConsumer(stats -> mCollectedStats = stats); + mCollector.setEnabled(true); + } + + @Test + public void collectStats() { + mockKernelCpuStats(new SparseArray<>() {{ + put(42, new long[]{100, 200}); + put(99, new long[]{300, 600}); + }}, 0, 1234); + + mMockClock.uptime = 1000; + mCollector.forceSchedule(); + waitForIdle(); + + assertThat(mCollectedStats.durationMs).isEqualTo(1234); + assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{100, 200}); + assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{300, 600}); + + mockKernelCpuStats(new SparseArray<>() {{ + put(42, new long[]{123, 234}); + put(99, new long[]{345, 678}); + }}, 1234, 3421); + + mMockClock.uptime = 2000; + mCollector.forceSchedule(); + waitForIdle(); + + assertThat(mCollectedStats.durationMs).isEqualTo(3421 - 1234); + assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{23, 34}); + assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{45, 78}); + } + + private void mockKernelCpuStats(SparseArray<long[]> uidToCpuStats, + long expectedLastUpdateTimestampMs, long newLastUpdateTimestampMs) { + when(mMockKernelCpuStatsReader.nativeReadCpuStats( + any(CpuPowerStatsCollector.KernelCpuStatsCallback.class), + any(int[].class), anyLong(), any(long[].class))) + .thenAnswer(invocation -> { + CpuPowerStatsCollector.KernelCpuStatsCallback callback = + invocation.getArgument(0); + int[] powerBucketIndexes = invocation.getArgument(1); + long lastTimestamp = invocation.getArgument(2); + long[] tempStats = invocation.getArgument(3); + + assertThat(powerBucketIndexes).isEqualTo(new int[]{0, 1}); + assertThat(lastTimestamp / 1000000L).isEqualTo(expectedLastUpdateTimestampMs); + assertThat(tempStats).hasLength(2); + + for (int i = 0; i < uidToCpuStats.size(); i++) { + int uid = uidToCpuStats.keyAt(i); + long[] cpuStats = uidToCpuStats.valueAt(i); + System.arraycopy(cpuStats, 0, tempStats, 0, tempStats.length); + callback.processUidStats(uid, tempStats); + } + return newLastUpdateTimestampMs * 1000000L; // Nanoseconds + }); + } + + private void waitForIdle() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java new file mode 100644 index 000000000000..38a5d1943f8b --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java @@ -0,0 +1,165 @@ +/* + * 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 junit.framework.Assert.fail; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.DeviceConfig; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.UiDevice; + +import com.android.frameworks.coretests.aidl.ICmdCallback; +import com.android.frameworks.coretests.aidl.ICmdReceiver; +import com.android.server.power.optimization.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class CpuPowerStatsCollectorValidationTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final int WORK_DURATION_MS = 2000; + private static final String TEST_PKG = "com.android.coretests.apps.bstatstestapp"; + private static final String TEST_ACTIVITY = TEST_PKG + ".TestActivity"; + private static final String EXTRA_KEY_CMD_RECEIVER = "cmd_receiver"; + private static final int START_ACTIVITY_TIMEOUT_MS = 2000; + + private Context mContext; + private UiDevice mUiDevice; + private DeviceConfig.Properties mBackupFlags; + private int mTestPkgUid; + + @Before + public void setup() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + mTestPkgUid = mContext.getPackageManager().getPackageUid(TEST_PKG, 0); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_STREAMLINED_BATTERY_STATS) + public void totalTimeInPowerBrackets() throws Exception { + dumpCpuStats(); // For the side effect of capturing the baseline. + + doSomeWork(); + + long duration = 0; + long[] stats = null; + + String[] cpuStatsDump = dumpCpuStats(); + Pattern durationPattern = Pattern.compile("duration=([0-9]*)"); + Pattern uidPattern = Pattern.compile("UID " + mTestPkgUid + ": \\[([0-9,\\s]*)]"); + for (String line : cpuStatsDump) { + Matcher durationMatcher = durationPattern.matcher(line); + if (durationMatcher.find()) { + duration = Long.parseLong(durationMatcher.group(1)); + } + Matcher uidMatcher = uidPattern.matcher(line); + if (uidMatcher.find()) { + String[] strings = uidMatcher.group(1).split(", "); + stats = new long[strings.length]; + for (int i = 0; i < strings.length; i++) { + stats[i] = Long.parseLong(strings[i]); + } + } + } + if (stats == null) { + fail("No CPU stats for " + mTestPkgUid + " (" + TEST_PKG + ")"); + } + + assertThat(duration).isAtLeast(WORK_DURATION_MS); + + long total = Arrays.stream(stats).sum(); + assertThat(total).isAtLeast((long) (WORK_DURATION_MS * 0.8)); + } + + private String[] dumpCpuStats() throws Exception { + String dump = executeCmdSilent("dumpsys batterystats --sample"); + String[] lines = dump.split("\n"); + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("CpuPowerStatsCollector")) { + return Arrays.copyOfRange(lines, i + 1, lines.length); + } + } + return new String[0]; + } + + private void doSomeWork() throws Exception { + final ICmdReceiver receiver; + receiver = ICmdReceiver.Stub.asInterface(startActivity()); + try { + receiver.doSomeWork(WORK_DURATION_MS); + } finally { + receiver.finishHost(); + } + } + + private IBinder startActivity() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final Intent launchIntent = new Intent().setComponent( + new ComponentName(TEST_PKG, TEST_ACTIVITY)); + final Bundle extras = new Bundle(); + final IBinder[] binders = new IBinder[1]; + extras.putBinder(EXTRA_KEY_CMD_RECEIVER, new ICmdCallback.Stub() { + @Override + public void onLaunched(IBinder receiver) { + binders[0] = receiver; + latch.countDown(); + } + }); + launchIntent.putExtras(extras).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(launchIntent); + if (latch.await(START_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + if (binders[0] == null) { + fail("Receiver binder should not be null"); + } + return binders[0]; + } else { + fail("Timed out waiting for the test activity to start; testUid=" + mTestPkgUid); + } + return null; + } + + private String executeCmdSilent(String cmd) throws Exception { + return mUiDevice.executeShellCommand(cmd).trim(); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index 6d3f1f27b572..4150972ab69a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -119,6 +119,13 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { return MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS; } + public MockBatteryStatsImpl setBatteryStatsConfig(BatteryStatsConfig config) { + synchronized (this) { + mBatteryStatsConfig = config; + } + return this; + } + public MockBatteryStatsImpl setNetworkStats(NetworkStats networkStats) { mNetworkStats = networkStats; return this; 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 new file mode 100644 index 000000000000..08c821344670 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java @@ -0,0 +1,87 @@ +/* + * 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 android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PowerStatsCollectorTest { + private final MockClock mMockClock = new MockClock(); + private final HandlerThread mHandlerThread = new HandlerThread("test"); + private Handler mHandler; + private PowerStatsCollector mCollector; + private PowerStats mCollectedStats; + + @Before + public void setup() { + mHandlerThread.start(); + mHandler = mHandlerThread.getThreadHandler(); + mCollector = new PowerStatsCollector(mHandler, + 60000, + mMockClock) { + @Override + protected PowerStats collectStats() { + return new PowerStats(); + } + }; + mCollector.addConsumer(stats -> mCollectedStats = stats); + mCollector.setEnabled(true); + } + + @Test + public void throttlePeriod() { + mMockClock.uptime = 1000; + mCollector.schedule(); + waitForIdle(); + + assertThat(mCollectedStats).isNotNull(); + + mMockClock.uptime += 1000; + mCollectedStats = null; + mCollector.schedule(); // Should be throttled + waitForIdle(); + + assertThat(mCollectedStats).isNull(); + + // Should be allowed to run + mMockClock.uptime += 100_000; + mCollector.schedule(); + waitForIdle(); + + assertThat(mCollectedStats).isNotNull(); + } + + private void waitForIdle() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 92ff7ab86247..20d8a5d1efeb 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -68,6 +68,7 @@ android_test { "ActivityContext", "coretests-aidl", "securebox", + "flag-junit", ], libs: [ |