Add CpuPowerStatsCollector for reading power-related CPU stats from kernel
Bug: 285646152
Bug: 285042200
Test: atest FrameworksServicesTests:BatteryStatsTests
Change-Id: I1cd64b8b1c5e1ffd58bc475507393c21f13f32fd
diff --git a/core/java/com/android/internal/os/OWNERS b/core/java/com/android/internal/os/OWNERS
index 0b9773e..e35b7f1 100644
--- a/core/java/com/android/internal/os/OWNERS
+++ b/core/java/com/android/internal/os/OWNERS
@@ -11,6 +11,7 @@
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 0000000..1169552
--- /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 72333fb..a5ee4b5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6559,11 +6559,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 0000000..8fb48bc
--- /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 851c0c2..9f37a6e5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5074,6 +5074,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 04ebb2b..b0f30d6 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 @@
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 @@
mPowerProfile = new PowerProfile(context);
mCpuScalingPolicies = new CpuScalingPolicyReader().read();
- mStats = new BatteryStatsImpl(systemDir, handler, this,
+ 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);
+ final long powerStatsThrottlePeriodCpu = context.getResources().getInteger(
+ com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodCpu);
+ mBatteryStatsConfig =
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
+ .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
+ .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);
-
- 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(
- new BatteryStatsImpl.BatteryStatsConfig.Builder()
- .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
- .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
- .build());
mStats.startTrackingSystemServerCpuTime();
if (BATTERY_USAGE_STORE_ENABLED) {
@@ -2591,6 +2594,7 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
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 cf4e845..5b10afa 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 @@
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 @@
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 @@
flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
}
mFlags = flags;
+ mPowerStatsThrottlePeriodCpu = builder.mPowerStatsThrottlePeriodCpu;
}
/**
@@ -469,15 +472,22 @@
== 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 @@
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 @@
@VisibleForTesting
@GuardedBy("this")
- protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
+ protected BatteryStatsConfig mBatteryStatsConfig;
@GuardedBy("this")
private AlarmManager mAlarmManager = null;
@@ -1733,6 +1751,7 @@
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 @@
mPlatformIdleStateCallback = null;
mEnergyConsumerRetriever = null;
mUserInfoProvider = null;
+ mCpuPowerStatsCollector = null;
}
private void init(Clock clock) {
@@ -10911,21 +10931,23 @@
return mTmpCpuTimeInFreq;
}
- public BatteryStatsImpl(@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,
- powerProfile, cpuScalingPolicies);
- }
-
- private BatteryStatsImpl(@NonNull Clock clock, @Nullable File systemDir,
+ 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(config, Clock.SYSTEM_CLOCK, systemDir, handler, cb, energyStatsCb, userInfoProvider,
+ powerProfile, cpuScalingPolicies);
+ }
+
+ 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 @@
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 @@
}
/**
- * 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 @@
if (mCpuUidFreqTimeReader != null) {
mCpuUidFreqTimeReader.onSystemReady();
}
+ if (mCpuPowerStatsCollector != null) {
+ mCpuPowerStatsCollector.onSystemReady();
+ }
mSystemReady = true;
}
@@ -15705,6 +15725,13 @@
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 0000000..1401746
--- /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 0000000..b49c89f
--- /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 405b133..2f150a1 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -50,6 +50,7 @@
"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 @@
"libsensorservicehidl",
"libsensorserviceaidl",
"libgui",
+ "libtimeinstate",
"libtimestats_atoms_proto",
"libusbhost",
"libtinyalsa",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index d9acf41..7e8ce60 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -23,6 +23,7 @@
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 0000000..a6084ea
--- /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 290ad8d..97d7be6 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -30,6 +30,7 @@
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 @@
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 05acd9b..5ea1929 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -23,6 +23,7 @@
"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 0000000..f2ee6db
--- /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 0000000..38a5d19
--- /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 6d3f1f2..4150972a 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 @@
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 0000000..08c8213
--- /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 92ff7ab..20d8a5d 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -68,6 +68,7 @@
"ActivityContext",
"coretests-aidl",
"securebox",
+ "flag-junit",
],
libs: [