diff options
| author | 2023-03-17 01:23:28 +0000 | |
|---|---|---|
| committer | 2023-03-17 01:23:28 +0000 | |
| commit | 2a523ecc0939dd1dda92d676c3a5127f856f9980 (patch) | |
| tree | 70160736c8d00173de25e5201a7e158b4858b5ad | |
| parent | 2e196a41e3d04001d99bc66bb40856053f2197cd (diff) | |
| parent | b9e7f3dd8798e45e9da08d6f78801ac03fcd8e3d (diff) | |
Merge changes from topic "cherrypicker-L08300000959200714:N79700001348406342" into udc-dev
* changes:
Start/Stop CPU monitoring based on the client callback addition/removal.
Verify CPU availability against client thresholds and notify the clients.
Monitor CPU availability using CPU frequency stats.
Update CpuInfoReader to compute normalized available CPU frequency.
6 files changed, 1384 insertions, 142 deletions
diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java index 06b45bf0fb4b..97507be96d84 100644 --- a/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java +++ b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java @@ -21,6 +21,8 @@ import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACK import com.android.internal.util.Preconditions; +import java.util.Objects; + /** CPU availability information. */ public final class CpuAvailabilityInfo { /** Constant to indicate missing CPU availability percent. */ @@ -35,29 +37,64 @@ public final class CpuAvailabilityInfo { @CpuAvailabilityMonitoringConfig.Cpuset public final int cpuset; + /** Uptime (in milliseconds) when the data in this object was captured. */ + public final long dataTimestampUptimeMillis; + /** The latest average CPU availability percent. */ public final int latestAvgAvailabilityPercent; - /** The past N-second average CPU availability percent. */ - public final int pastNSecAvgAvailabilityPercent; + /** + * The past N-millisecond average CPU availability percent. + * + * <p>When there is not enough data to calculate the past N-millisecond average, this field will + * contain the value {@link MISSING_CPU_AVAILABILITY_PERCENT}. + */ + public final int pastNMillisAvgAvailabilityPercent; - /** The duration over which the {@link pastNSecAvgAvailabilityPercent} was calculated. */ - public final int avgAvailabilityDurationSec; + /** The duration over which the {@link pastNMillisAvgAvailabilityPercent} was calculated. */ + public final long pastNMillisDuration; @Override public String toString() { - return "CpuAvailabilityInfo{" + "cpuset=" + cpuset + ", latestAvgAvailabilityPercent=" - + latestAvgAvailabilityPercent + ", pastNSecAvgAvailabilityPercent=" - + pastNSecAvgAvailabilityPercent + ", avgAvailabilityDurationSec=" - + avgAvailabilityDurationSec + '}'; + return "CpuAvailabilityInfo{" + "cpuset = " + cpuset + ", dataTimestampUptimeMillis = " + + dataTimestampUptimeMillis + ", latestAvgAvailabilityPercent = " + + latestAvgAvailabilityPercent + ", pastNMillisAvgAvailabilityPercent = " + + pastNMillisAvgAvailabilityPercent + ", pastNMillisDuration = " + + pastNMillisDuration + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CpuAvailabilityInfo)) { + return false; + } + CpuAvailabilityInfo info = (CpuAvailabilityInfo) obj; + return cpuset == info.cpuset && dataTimestampUptimeMillis == info.dataTimestampUptimeMillis + && latestAvgAvailabilityPercent == info.latestAvgAvailabilityPercent + && pastNMillisAvgAvailabilityPercent == info.pastNMillisAvgAvailabilityPercent + && pastNMillisDuration == info.pastNMillisDuration; + } + + @Override + public int hashCode() { + return Objects.hash(cpuset, dataTimestampUptimeMillis, latestAvgAvailabilityPercent, + pastNMillisAvgAvailabilityPercent, pastNMillisDuration); } - CpuAvailabilityInfo(int cpuset, int latestAvgAvailabilityPercent, - int pastNSecAvgAvailabilityPercent, int avgAvailabilityDurationSec) { + CpuAvailabilityInfo(int cpuset, long dataTimestampUptimeMillis, + int latestAvgAvailabilityPercent, int pastNMillisAvgAvailabilityPercent, + long pastNMillisDuration) { this.cpuset = Preconditions.checkArgumentInRange(cpuset, CPUSET_ALL, CPUSET_BACKGROUND, "cpuset"); - this.latestAvgAvailabilityPercent = latestAvgAvailabilityPercent; - this.pastNSecAvgAvailabilityPercent = pastNSecAvgAvailabilityPercent; - this.avgAvailabilityDurationSec = avgAvailabilityDurationSec; + this.dataTimestampUptimeMillis = + Preconditions.checkArgumentNonnegative(dataTimestampUptimeMillis); + this.latestAvgAvailabilityPercent = Preconditions.checkArgumentNonnegative( + latestAvgAvailabilityPercent); + this.pastNMillisAvgAvailabilityPercent = pastNMillisAvgAvailabilityPercent; + this.pastNMillisDuration = Preconditions.checkArgumentNonnegative( + pastNMillisDuration); } } diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java index a3c4c9e828b4..cbe02fc09d84 100644 --- a/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java +++ b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java @@ -90,8 +90,19 @@ public final class CpuAvailabilityMonitoringConfig { @Override public String toString() { - return "CpuAvailabilityMonitoringConfig{cpuset=" + cpuset + ", mThresholds=" + mThresholds - + ')'; + return "CpuAvailabilityMonitoringConfig{cpuset=" + toCpusetString(cpuset) + ", mThresholds=" + + mThresholds + ')'; + } + + /** Returns the string equivalent of the provided cpuset. */ + public static String toCpusetString(int cpuset) { + switch (cpuset) { + case CPUSET_ALL: + return "CPUSET_ALL"; + case CPUSET_BACKGROUND: + return "CPUSET_BACKGROUND"; + } + return "Invalid cpuset: " + cpuset; } private CpuAvailabilityMonitoringConfig(Builder builder) { diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java index ca97a9847b39..ce68edbb9fa1 100644 --- a/services/core/java/com/android/server/cpu/CpuInfoReader.java +++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java @@ -21,8 +21,10 @@ import static com.android.server.cpu.CpuMonitorService.TAG; import android.annotation.IntDef; import android.annotation.Nullable; +import android.os.SystemClock; import android.system.Os; import android.system.OsConstants; +import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.LongSparseLongArray; import android.util.SparseArray; @@ -50,6 +52,9 @@ public final class CpuInfoReader { private static final String POLICY_DIR_PREFIX = "policy"; private static final String RELATED_CPUS_FILE = "related_cpus"; private static final String AFFECTED_CPUS_FILE = "affected_cpus"; + // TODO(b/263154344): Avoid reading from cpuinfo_cur_freq because non-root users don't have + // read permission for this file. The file permissions are set by the Kernel. Instead, read + // the current frequency only from scaling_cur_freq. private static final String CUR_CPUFREQ_FILE = "cpuinfo_cur_freq"; private static final String MAX_CPUFREQ_FILE = "cpuinfo_max_freq"; private static final String CUR_SCALING_FREQ_FILE = "scaling_cur_freq"; @@ -70,16 +75,18 @@ public final class CpuInfoReader { private static final Pattern TIME_IN_STATE_PATTERN = Pattern.compile("(?<freqKHz>[0-9]+)\\s(?<time>[0-9]+)"); private static final long MILLIS_PER_CLOCK_TICK = 1000L / Os.sysconf(OsConstants._SC_CLK_TCK); + private static final long MIN_READ_INTERVAL_MILLISECONDS = 500; @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"FLAG_CPUSET_CATEGORY_"}, flag = true, value = { FLAG_CPUSET_CATEGORY_TOP_APP, FLAG_CPUSET_CATEGORY_BACKGROUND }) - private @interface CpusetCategory{} + /** package **/ @interface CpusetCategory{} // TODO(b/242722241): Protect updatable variables with a local lock. private final File mCpusetDir; + private final long mMinReadIntervalMillis; private final SparseIntArray mCpusetCategoriesByCpus = new SparseIntArray(); private final SparseArray<File> mCpuFreqPolicyDirsById = new SparseArray<>(); private final SparseArray<StaticPolicyInfo> mStaticPolicyInfoById = new SparseArray<>(); @@ -90,16 +97,20 @@ public final class CpuInfoReader { private SparseArray<CpuUsageStats> mCumulativeCpuUsageStats = new SparseArray<>(); private boolean mIsEnabled; private boolean mHasTimeInStateFile; + private long mLastReadUptimeMillis; + private SparseArray<CpuInfo> mLastReadCpuInfos; public CpuInfoReader() { - this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH)); + this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH), + MIN_READ_INTERVAL_MILLISECONDS); } @VisibleForTesting - CpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile) { + CpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile, long minReadIntervalMillis) { mCpusetDir = cpusetDir; mCpuFreqDir = cpuFreqDir; mProcStatFile = procStatFile; + mMinReadIntervalMillis = minReadIntervalMillis; } /** @@ -167,6 +178,16 @@ public final class CpuInfoReader { if (!mIsEnabled) { return null; } + long uptimeMillis = SystemClock.uptimeMillis(); + if (mLastReadUptimeMillis > 0 + && uptimeMillis - mLastReadUptimeMillis < mMinReadIntervalMillis) { + Slogf.w(TAG, "Skipping reading from device and returning the last read CpuInfos. " + + "Last read was %d ms ago, min read interval is %d ms", + uptimeMillis - mLastReadUptimeMillis, mMinReadIntervalMillis); + return mLastReadCpuInfos; + } + mLastReadUptimeMillis = uptimeMillis; + mLastReadCpuInfos = null; SparseArray<CpuUsageStats> cpuUsageStatsByCpus = readLatestCpuUsageStats(); if (cpuUsageStatsByCpus == null || cpuUsageStatsByCpus.size() == 0) { Slogf.e(TAG, "Failed to read latest CPU usage stats"); @@ -202,6 +223,12 @@ public final class CpuInfoReader { + " policy ID %d", policyId); continue; } + if (curFreqKHz > maxFreqKHz) { + Slogf.w(TAG, "Current CPU frequency (%d) is greater than maximum CPU frequency" + + " (%d) for policy ID (%d). Skipping CPU frequency policy", curFreqKHz, + maxFreqKHz, policyId); + continue; + } for (int coreIdx = 0; coreIdx < staticPolicyInfo.relatedCpuCores.size(); coreIdx++) { int relatedCpuCore = staticPolicyInfo.relatedCpuCores.get(coreIdx); CpuInfo prevCpuInfo = cpuInfoByCpus.get(relatedCpuCore); @@ -241,9 +268,73 @@ public final class CpuInfoReader { } } } + mLastReadCpuInfos = cpuInfoByCpus; return cpuInfoByCpus; } + /** Dumps the current state. */ + public void dump(IndentingPrintWriter writer) { + writer.printf("*%s*\n", getClass().getSimpleName()); + writer.increaseIndent(); // Add intend for the outermost block. + + writer.printf("mCpusetDir = %s\n", mCpusetDir.getAbsolutePath()); + writer.printf("mCpuFreqDir = %s\n", mCpuFreqDir.getAbsolutePath()); + writer.printf("mProcStatFile = %s\n", mProcStatFile.getAbsolutePath()); + writer.printf("mIsEnabled = %s\n", mIsEnabled); + writer.printf("mHasTimeInStateFile = %s\n", mHasTimeInStateFile); + writer.printf("mLastReadUptimeMillis = %d\n", mLastReadUptimeMillis); + writer.printf("mMinReadIntervalMillis = %d\n", mMinReadIntervalMillis); + + writer.printf("Cpuset categories by CPU core:\n"); + writer.increaseIndent(); + for (int i = 0; i < mCpusetCategoriesByCpus.size(); i++) { + writer.printf("CPU core id = %d, %s\n", mCpusetCategoriesByCpus.keyAt(i), + toCpusetCategoriesStr(mCpusetCategoriesByCpus.valueAt(i))); + } + writer.decreaseIndent(); + + writer.println("Cpu frequency policy directories by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mCpuFreqPolicyDirsById.size(); i++) { + writer.printf("Policy id = %d, Dir = %s\n", mCpuFreqPolicyDirsById.keyAt(i), + mCpuFreqPolicyDirsById.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Static cpu frequency policy infos by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mStaticPolicyInfoById.size(); i++) { + writer.printf("Policy id = %d, %s\n", mStaticPolicyInfoById.keyAt(i), + mStaticPolicyInfoById.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Cpu time in frequency state by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mTimeInStateByPolicyId.size(); i++) { + writer.printf("Policy id = %d, Time(millis) in state by CPU frequency(KHz) = %s\n", + mTimeInStateByPolicyId.keyAt(i), mTimeInStateByPolicyId.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Last read CPU infos:"); + writer.increaseIndent(); + for (int i = 0; i < mLastReadCpuInfos.size(); i++) { + writer.printf("%s\n", mLastReadCpuInfos.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Latest cumulative CPU usage stats by CPU core:"); + writer.increaseIndent(); + for (int i = 0; i < mCumulativeCpuUsageStats.size(); i++) { + writer.printf("CPU core id = %d, %s\n", mCumulativeCpuUsageStats.keyAt(i), + mCumulativeCpuUsageStats.valueAt(i)); + } + writer.decreaseIndent(); + + writer.decreaseIndent(); // Remove intend for the outermost block. + } + /** * Sets the CPU frequency for testing. * @@ -496,6 +587,9 @@ public final class CpuInfoReader { for (int i = 0; i < timeInState.size(); i++) { totalTimeInState += timeInState.valueAt(i); } + if (totalTimeInState == 0) { + return CpuInfo.MISSING_FREQUENCY; + } double avgFreqKHz = 0; for (int i = 0; i < timeInState.size(); i++) { avgFreqKHz += (timeInState.keyAt(i) * timeInState.valueAt(i)) / totalTimeInState; @@ -624,16 +718,29 @@ public final class CpuInfoReader { @CpusetCategory public final int cpusetCategories; public final boolean isOnline; + public final long maxCpuFreqKHz; // Values in the below fields may be missing when a CPU core is offline. public final long curCpuFreqKHz; - public final long maxCpuFreqKHz; public final long avgTimeInStateCpuFreqKHz; @Nullable public final CpuUsageStats latestCpuUsageStats; + private long mNormalizedAvailableCpuFreqKHz; + CpuInfo(int cpuCore, @CpusetCategory int cpusetCategories, boolean isOnline, long curCpuFreqKHz, long maxCpuFreqKHz, long avgTimeInStateCpuFreqKHz, CpuUsageStats latestCpuUsageStats) { + this(cpuCore, cpusetCategories, isOnline, curCpuFreqKHz, maxCpuFreqKHz, + avgTimeInStateCpuFreqKHz, /* normalizedAvailableCpuFreqKHz= */ 0, + latestCpuUsageStats); + this.mNormalizedAvailableCpuFreqKHz = computeNormalizedAvailableCpuFreqKHz(); + } + + // Should be used only for testing. + @VisibleForTesting + CpuInfo(int cpuCore, @CpusetCategory int cpusetCategories, boolean isOnline, + long curCpuFreqKHz, long maxCpuFreqKHz, long avgTimeInStateCpuFreqKHz, + long normalizedAvailableCpuFreqKHz, CpuUsageStats latestCpuUsageStats) { this.cpuCore = cpuCore; this.cpusetCategories = cpusetCategories; this.isOnline = isOnline; @@ -641,6 +748,11 @@ public final class CpuInfoReader { this.maxCpuFreqKHz = maxCpuFreqKHz; this.avgTimeInStateCpuFreqKHz = avgTimeInStateCpuFreqKHz; this.latestCpuUsageStats = latestCpuUsageStats; + this.mNormalizedAvailableCpuFreqKHz = normalizedAvailableCpuFreqKHz; + } + + public long getNormalizedAvailableCpuFreqKHz() { + return mNormalizedAvailableCpuFreqKHz; } @Override @@ -657,6 +769,8 @@ public final class CpuInfoReader { .append(avgTimeInStateCpuFreqKHz == MISSING_FREQUENCY ? "missing" : avgTimeInStateCpuFreqKHz) .append(", latestCpuUsageStats = ").append(latestCpuUsageStats) + .append(", mNormalizedAvailableCpuFreqKHz = ") + .append(mNormalizedAvailableCpuFreqKHz) .append(" }").toString(); } @@ -673,13 +787,32 @@ public final class CpuInfoReader { && isOnline == other.isOnline && curCpuFreqKHz == other.curCpuFreqKHz && maxCpuFreqKHz == other.maxCpuFreqKHz && avgTimeInStateCpuFreqKHz == other.avgTimeInStateCpuFreqKHz - && latestCpuUsageStats.equals(other.latestCpuUsageStats); + && latestCpuUsageStats.equals(other.latestCpuUsageStats) + && mNormalizedAvailableCpuFreqKHz == other.mNormalizedAvailableCpuFreqKHz; } @Override public int hashCode() { return Objects.hash(cpuCore, cpusetCategories, isOnline, curCpuFreqKHz, maxCpuFreqKHz, - avgTimeInStateCpuFreqKHz, latestCpuUsageStats); + avgTimeInStateCpuFreqKHz, latestCpuUsageStats, mNormalizedAvailableCpuFreqKHz); + } + + private long computeNormalizedAvailableCpuFreqKHz() { + if (!isOnline) { + return MISSING_FREQUENCY; + } + long totalTimeMillis = latestCpuUsageStats.getTotalTimeMillis(); + if (totalTimeMillis == 0) { + Slogf.wtf(TAG, "Total CPU time millis is 0. This shouldn't happen unless stats are" + + " polled too frequently"); + return MISSING_FREQUENCY; + } + double nonIdlePercent = 100.0 * (totalTimeMillis + - (double) latestCpuUsageStats.idleTimeMillis) / totalTimeMillis; + long curFreqKHz = avgTimeInStateCpuFreqKHz == MISSING_FREQUENCY + ? curCpuFreqKHz : avgTimeInStateCpuFreqKHz; + double availablePercent = 100.0 - (nonIdlePercent * curFreqKHz / maxCpuFreqKHz); + return (long) ((availablePercent * maxCpuFreqKHz) / 100.0); } } @@ -712,7 +845,7 @@ public final class CpuInfoReader { this.guestNiceTimeMillis = guestNiceTimeMillis; } - public long getTotalTime() { + public long getTotalTimeMillis() { return userTimeMillis + niceTimeMillis + systemTimeMillis + idleTimeMillis + iowaitTimeMillis + irqTimeMillis + softirqTimeMillis + stealTimeMillis + guestTimeMillis + guestNiceTimeMillis; @@ -796,8 +929,8 @@ public final class CpuInfoReader { @Override public String toString() { - return "FrequencyPair{cpuFreqKHz=" + cpuFreqKHz + ", scalingFreqKHz=" + scalingFreqKHz - + '}'; + return "FrequencyPair{cpuFreqKHz = " + cpuFreqKHz + ", scalingFreqKHz = " + + scalingFreqKHz + '}'; } } @@ -812,7 +945,7 @@ public final class CpuInfoReader { @Override public String toString() { - return "StaticPolicyInfo{maxCpuFreqPair=" + maxCpuFreqPair + ", relatedCpuCores=" + return "StaticPolicyInfo{maxCpuFreqPair = " + maxCpuFreqPair + ", relatedCpuCores = " + relatedCpuCores + '}'; } } @@ -831,9 +964,9 @@ public final class CpuInfoReader { @Override public String toString() { - return "DynamicPolicyInfo{curCpuFreqPair=" + curCpuFreqPair - + ", avgTimeInStateCpuFreqKHz=" + avgTimeInStateCpuFreqKHz - + ", affectedCpuCores=" + affectedCpuCores + '}'; + return "DynamicPolicyInfo{curCpuFreqPair = " + curCpuFreqPair + + ", avgTimeInStateCpuFreqKHz = " + avgTimeInStateCpuFreqKHz + + ", affectedCpuCores = " + affectedCpuCores + '}'; } } } diff --git a/services/core/java/com/android/server/cpu/CpuMonitorService.java b/services/core/java/com/android/server/cpu/CpuMonitorService.java index 4eefe5c8cad5..df8cfad4ab03 100644 --- a/services/core/java/com/android/server/cpu/CpuMonitorService.java +++ b/services/core/java/com/android/server/cpu/CpuMonitorService.java @@ -18,15 +18,33 @@ package com.android.server.cpu; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; + +import android.annotation.Nullable; import android.content.Context; import android.os.Binder; -import android.util.ArrayMap; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.SystemClock; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.util.SparseArrayMap; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.internal.util.Preconditions; +import com.android.server.ServiceThread; import com.android.server.SystemService; +import com.android.server.Watchdog; import com.android.server.utils.PriorityDump; import com.android.server.utils.Slogf; @@ -34,28 +52,55 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; /** Service to monitor CPU availability and usage. */ public final class CpuMonitorService extends SystemService { static final String TAG = CpuMonitorService.class.getSimpleName(); static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - // TODO(b/242722241): Make this a resource overlay property. - // Maintain 3 monitoring intervals: - // * One to poll very frequently when mCpuAvailabilityCallbackInfoByCallbacks are available and - // CPU availability is above a threshold (such as at least 10% of CPU is available). - // * One to poll less frequently when mCpuAvailabilityCallbackInfoByCallbacks are available - // and CPU availability is below a threshold (such as less than 10% of CPU is available). - // * One to poll very less frequently when no callbacks are available and the build is either - // user-debug or eng. This will be useful for debugging in development environment. - static final int DEFAULT_CPU_MONITORING_INTERVAL_MILLISECONDS = 5_000; + // TODO(b/267500110): Make these constants resource overlay properties. + /** Default monitoring interval when no monitoring is in progress. */ + static final long DEFAULT_MONITORING_INTERVAL_MILLISECONDS = -1; + /** Monitoring interval when callbacks are registered and the CPU load is normal. */ + private static final long NORMAL_MONITORING_INTERVAL_MILLISECONDS = + TimeUnit.SECONDS.toMillis(5); + + /** + * Monitoring interval when no registered callbacks and the build is either user-debug or eng. + */ + private static final long DEBUG_MONITORING_INTERVAL_MILLISECONDS = TimeUnit.MINUTES.toMillis(1); + /** + * Size of the in-memory cache relative to the current uptime. + * + * On user-debug or eng builds, continuously cache stats with a bigger cache size for debugging + * purposes. + */ + private static final long CACHE_DURATION_MILLISECONDS = Build.IS_USERDEBUG || Build.IS_ENG + ? TimeUnit.MINUTES.toMillis(30) : TimeUnit.MINUTES.toMillis(10); + // TODO(b/267500110): Investigate whether this duration should change when the monitoring + // interval is updated. When the CPU is under heavy load, the monitoring will happen less + // frequently. Should this duration be increased as well when this happens? + private static final long LATEST_AVAILABILITY_DURATION_MILLISECONDS = + TimeUnit.SECONDS.toMillis(30); private final Context mContext; + private final HandlerThread mHandlerThread; + private final CpuInfoReader mCpuInfoReader; + private final boolean mShouldDebugMonitor; + private final long mNormalMonitoringIntervalMillis; + private final long mDebugMonitoringIntervalMillis; + private final long mLatestAvailabilityDurationMillis; private final Object mLock = new Object(); @GuardedBy("mLock") - private final ArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, CpuAvailabilityCallbackInfo> - mCpuAvailabilityCallbackInfoByCallbacks = new ArrayMap<>(); + private final SparseArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, + CpuAvailabilityCallbackInfo> mAvailabilityCallbackInfosByCallbacksByCpuset; @GuardedBy("mLock") - private long mMonitoringIntervalMilliseconds = DEFAULT_CPU_MONITORING_INTERVAL_MILLISECONDS; + private final SparseArray<CpusetInfo> mCpusetInfosByCpuset; + private final Runnable mMonitorCpuStats = this::monitorCpuStats; + + @GuardedBy("mLock") + private long mCurrentMonitoringIntervalMillis = DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + private Handler mHandler; private final CpuMonitorInternal mLocalService = new CpuMonitorInternal() { @Override @@ -63,89 +108,389 @@ public final class CpuMonitorService extends SystemService { CpuAvailabilityMonitoringConfig config, CpuAvailabilityCallback callback) { Objects.requireNonNull(callback, "Callback must be non-null"); Objects.requireNonNull(config, "Config must be non-null"); + CpuAvailabilityCallbackInfo callbackInfo; synchronized (mLock) { - if (mCpuAvailabilityCallbackInfoByCallbacks.containsKey(callback)) { - Slogf.i(TAG, "Overwriting the existing CpuAvailabilityCallback %s", - mCpuAvailabilityCallbackInfoByCallbacks.get(callback)); - // TODO(b/242722241): Overwrite any internal cache (will be added in future CLs) - // that maps callbacks based on the CPU availability thresholds. - } - CpuAvailabilityCallbackInfo info = new CpuAvailabilityCallbackInfo(config, - executor); - mCpuAvailabilityCallbackInfoByCallbacks.put(callback, info); - if (DEBUG) { - Slogf.d(TAG, "Added a CPU availability callback: %s", info); + // Verify all CPUSET entries before adding the callback because this will help + // delete any previously added callback for a different CPUSET. + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + int cpuset = mAvailabilityCallbackInfosByCallbacksByCpuset.keyAt(i); + callbackInfo = mAvailabilityCallbackInfosByCallbacksByCpuset.delete(cpuset, + callback); + if (callbackInfo != null) { + Slogf.i(TAG, "Overwriting the existing %s", callbackInfo); + } } + callbackInfo = newCallbackInfoLocked(config, callback, executor); + } + asyncNotifyMonitoringIntervalChangeToClient(callbackInfo); + if (DEBUG) { + Slogf.d(TAG, "Successfully added %s", callbackInfo); } - // TODO(b/242722241): - // * On the executor or on the handler thread, call the callback with the latest CPU - // availability info and monitoring interval. - // * Monitor the CPU stats more frequently when the first callback is added. } @Override public void removeCpuAvailabilityCallback(CpuAvailabilityCallback callback) { synchronized (mLock) { - if (!mCpuAvailabilityCallbackInfoByCallbacks.containsKey(callback)) { - Slogf.i(TAG, "CpuAvailabilityCallback was not previously added." - + " Ignoring the remove request"); - return; - } - CpuAvailabilityCallbackInfo info = - mCpuAvailabilityCallbackInfoByCallbacks.remove(callback); - if (DEBUG) { - Slogf.d(TAG, "Removed a CPU availability callback: %s", info); + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + int cpuset = mAvailabilityCallbackInfosByCallbacksByCpuset.keyAt(i); + CpuAvailabilityCallbackInfo callbackInfo = + mAvailabilityCallbackInfosByCallbacksByCpuset.delete(cpuset, callback); + if (callbackInfo != null) { + if (DEBUG) { + Slogf.d(TAG, "Successfully removed %s", callbackInfo); + } + checkAndStopMonitoringLocked(); + return; + } } + Slogf.w(TAG, "CpuAvailabilityCallback was not previously added. Ignoring the remove" + + " request"); } - // TODO(b/242722241): Increase CPU monitoring interval when all callbacks are removed. } }; public CpuMonitorService(Context context) { + this(context, new CpuInfoReader(), new ServiceThread(TAG, + Process.THREAD_PRIORITY_BACKGROUND, /* allowIo= */ true), + Build.IS_USERDEBUG || Build.IS_ENG, NORMAL_MONITORING_INTERVAL_MILLISECONDS, + DEBUG_MONITORING_INTERVAL_MILLISECONDS, LATEST_AVAILABILITY_DURATION_MILLISECONDS); + } + + @VisibleForTesting + CpuMonitorService(Context context, CpuInfoReader cpuInfoReader, HandlerThread handlerThread, + boolean shouldDebugMonitor, long normalMonitoringIntervalMillis, + long debugMonitoringIntervalMillis, long latestAvailabilityDurationMillis) { super(context); mContext = context; + mHandlerThread = handlerThread; + mShouldDebugMonitor = shouldDebugMonitor; + mNormalMonitoringIntervalMillis = normalMonitoringIntervalMillis; + mDebugMonitoringIntervalMillis = debugMonitoringIntervalMillis; + mLatestAvailabilityDurationMillis = latestAvailabilityDurationMillis; + mCpuInfoReader = cpuInfoReader; + mCpusetInfosByCpuset = new SparseArray<>(2); + mCpusetInfosByCpuset.append(CPUSET_ALL, new CpusetInfo(CPUSET_ALL)); + mCpusetInfosByCpuset.append(CPUSET_BACKGROUND, new CpusetInfo(CPUSET_BACKGROUND)); + mAvailabilityCallbackInfosByCallbacksByCpuset = new SparseArrayMap<>(); } @Override public void onStart() { + // Initialize CPU info reader and perform the first read to make sure the CPU stats are + // readable without any issues. + if (!mCpuInfoReader.init() || mCpuInfoReader.readCpuInfos() == null) { + Slogf.wtf(TAG, "Failed to initialize CPU info reader. This happens when the CPU " + + "frequency stats are not available or the sysfs interface has changed in " + + "the Kernel. Cannot monitor CPU without these stats. Terminating CPU monitor " + + "service"); + return; + } + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); publishLocalService(CpuMonitorInternal.class, mLocalService); publishBinderService("cpu_monitor", new CpuMonitorBinder(), /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL); + Watchdog.getInstance().addThread(mHandler); + synchronized (mLock) { + if (mShouldDebugMonitor && !mHandler.hasCallbacks(mMonitorCpuStats)) { + mCurrentMonitoringIntervalMillis = mDebugMonitoringIntervalMillis; + Slogf.i(TAG, "Starting debug monitoring"); + mHandler.post(mMonitorCpuStats); + } + } + } + + @VisibleForTesting + long getCurrentMonitoringIntervalMillis() { + synchronized (mLock) { + return mCurrentMonitoringIntervalMillis; + } } private void doDump(IndentingPrintWriter writer) { writer.printf("*%s*\n", getClass().getSimpleName()); writer.increaseIndent(); + mCpuInfoReader.dump(writer); + writer.printf("mShouldDebugMonitor = %s\n", mShouldDebugMonitor ? "Yes" : "No"); + writer.printf("mNormalMonitoringIntervalMillis = %d\n", mNormalMonitoringIntervalMillis); + writer.printf("mDebugMonitoringIntervalMillis = %d\n", mDebugMonitoringIntervalMillis); + writer.printf("mLatestAvailabilityDurationMillis = %d\n", + mLatestAvailabilityDurationMillis); synchronized (mLock) { - writer.printf("CPU monitoring interval: %d ms\n", mMonitoringIntervalMilliseconds); - if (!mCpuAvailabilityCallbackInfoByCallbacks.isEmpty()) { + writer.printf("mCurrentMonitoringIntervalMillis = %d\n", + mCurrentMonitoringIntervalMillis); + if (hasClientCallbacksLocked()) { writer.println("CPU availability change callbacks:"); writer.increaseIndent(); - for (int i = 0; i < mCpuAvailabilityCallbackInfoByCallbacks.size(); i++) { - writer.printf("%s: %s\n", mCpuAvailabilityCallbackInfoByCallbacks.keyAt(i), - mCpuAvailabilityCallbackInfoByCallbacks.valueAt(i)); + mAvailabilityCallbackInfosByCallbacksByCpuset.forEach( + (callbackInfo) -> writer.printf("%s\n", callbackInfo)); + writer.decreaseIndent(); + } + if (mCpusetInfosByCpuset.size() > 0) { + writer.println("Cpuset infos:"); + writer.increaseIndent(); + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + writer.printf("%s\n", mCpusetInfosByCpuset.valueAt(i)); } writer.decreaseIndent(); } } - // TODO(b/242722241): Print the recent past CPU stats. writer.decreaseIndent(); } + private void monitorCpuStats() { + long uptimeMillis = SystemClock.uptimeMillis(); + // Remove duplicate callbacks caused by switching form debug to normal monitoring. + // The removal of the duplicate callback done in the {@link newCallbackInfoLocked} method + // may result in a no-op when a duplicate execution of this callback has already started + // on the handler thread. + mHandler.removeCallbacks(mMonitorCpuStats); + SparseArray<CpuInfoReader.CpuInfo> cpuInfosByCoreId = mCpuInfoReader.readCpuInfos(); + if (cpuInfosByCoreId == null) { + // This shouldn't happen because the CPU infos are read & verified during + // the {@link onStart} call. + Slogf.wtf(TAG, "Failed to read CPU info from device"); + synchronized (mLock) { + stopMonitoringCpuStatsLocked(); + } + // Monitoring is stopped but no client callback is removed. + // TODO(b/267500110): Identify whether the clients should be notified about this state. + return; + } + + synchronized (mLock) { + // 1. Populate the {@link mCpusetInfosByCpuset} with the latest cpuInfo. + for (int i = 0; i < cpuInfosByCoreId.size(); i++) { + CpuInfoReader.CpuInfo cpuInfo = cpuInfosByCoreId.valueAt(i); + for (int j = 0; j < mCpusetInfosByCpuset.size(); j++) { + mCpusetInfosByCpuset.valueAt(j).appendCpuInfo(uptimeMillis, cpuInfo); + } + } + + // 2. Verify whether any monitoring thresholds are crossed and notify the corresponding + // clients. + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + CpusetInfo cpusetInfo = mCpusetInfosByCpuset.valueAt(i); + cpusetInfo.populateLatestCpuAvailabilityInfo(uptimeMillis, + mLatestAvailabilityDurationMillis); + checkClientThresholdsAndNotifyLocked(cpusetInfo); + } + + // TODO(b/267500110): Detect heavy CPU load. On detecting heavy CPU load, increase + // the monitoring interval and notify the clients. + + // 3. Continue monitoring only when either there is at least one registered client + // callback or debug monitoring is enabled. + if (mCurrentMonitoringIntervalMillis > 0 + && (hasClientCallbacksLocked() || mShouldDebugMonitor)) { + mHandler.postAtTime(mMonitorCpuStats, + uptimeMillis + mCurrentMonitoringIntervalMillis); + } else { + stopMonitoringCpuStatsLocked(); + } + } + } + + @GuardedBy("mLock") + private void checkClientThresholdsAndNotifyLocked(CpusetInfo cpusetInfo) { + int prevAvailabilityPercent = cpusetInfo.getPrevCpuAvailabilityPercent(); + CpuAvailabilityInfo latestAvailabilityInfo = cpusetInfo.getLatestCpuAvailabilityInfo(); + if (latestAvailabilityInfo == null || prevAvailabilityPercent < 0 + || mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKey( + cpusetInfo.cpuset) == 0) { + // When either the current or the previous CPU availability percents are + // missing, skip the current cpuset as there is not enough data to verify + // whether the CPU availability has crossed any monitoring threshold. + return; + } + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + for (int j = 0; j < mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKeyAt( + i); j++) { + CpuAvailabilityCallbackInfo callbackInfo = + mAvailabilityCallbackInfosByCallbacksByCpuset.valueAt(i, j); + if (callbackInfo.config.cpuset != cpusetInfo.cpuset) { + continue; + } + if (didCrossAnyThreshold(prevAvailabilityPercent, + latestAvailabilityInfo.latestAvgAvailabilityPercent, + callbackInfo.config.getThresholds())) { + asyncNotifyCpuAvailabilityToClient(latestAvailabilityInfo, callbackInfo); + } + } + } + } + + private void asyncNotifyMonitoringIntervalChangeToClient( + CpuAvailabilityCallbackInfo callbackInfo) { + if (callbackInfo.executor == null) { + mHandler.post(callbackInfo.notifyMonitoringIntervalChangeRunnable); + } else { + callbackInfo.executor.execute(callbackInfo.notifyMonitoringIntervalChangeRunnable); + } + } + + private void asyncNotifyCpuAvailabilityToClient(CpuAvailabilityInfo availabilityInfo, + CpuAvailabilityCallbackInfo callbackInfo) { + callbackInfo.notifyCpuAvailabilityChangeRunnable.prepare(availabilityInfo); + if (callbackInfo.executor == null) { + mHandler.post(callbackInfo.notifyCpuAvailabilityChangeRunnable); + } else { + callbackInfo.executor.execute(callbackInfo.notifyCpuAvailabilityChangeRunnable); + } + } + + @GuardedBy("mLock") + private CpuAvailabilityCallbackInfo newCallbackInfoLocked( + CpuAvailabilityMonitoringConfig config, + CpuMonitorInternal.CpuAvailabilityCallback callback, Executor executor) { + CpuAvailabilityCallbackInfo callbackInfo = new CpuAvailabilityCallbackInfo(this, config, + callback, executor); + String cpusetStr = CpuAvailabilityMonitoringConfig.toCpusetString( + callbackInfo.config.cpuset); + CpusetInfo cpusetInfo = mCpusetInfosByCpuset.get(callbackInfo.config.cpuset); + Preconditions.checkState(cpusetInfo != null, "Missing cpuset info for cpuset %s", + cpusetStr); + boolean hasExistingClientCallbacks = hasClientCallbacksLocked(); + mAvailabilityCallbackInfosByCallbacksByCpuset.add(callbackInfo.config.cpuset, + callbackInfo.callback, callbackInfo); + if (DEBUG) { + Slogf.d(TAG, "Added a CPU availability callback: %s", callbackInfo); + } + CpuAvailabilityInfo latestInfo = cpusetInfo.getLatestCpuAvailabilityInfo(); + if (latestInfo != null) { + asyncNotifyCpuAvailabilityToClient(latestInfo, callbackInfo); + } + if (hasExistingClientCallbacks && mHandler.hasCallbacks(mMonitorCpuStats)) { + return callbackInfo; + } + // Remove existing callbacks to ensure any debug monitoring (if started) is stopped before + // starting normal monitoring. + mHandler.removeCallbacks(mMonitorCpuStats); + mCurrentMonitoringIntervalMillis = mNormalMonitoringIntervalMillis; + mHandler.post(mMonitorCpuStats); + return callbackInfo; + } + + @GuardedBy("mLock") + private void checkAndStopMonitoringLocked() { + if (hasClientCallbacksLocked()) { + return; + } + if (mShouldDebugMonitor) { + if (DEBUG) { + Slogf.e(TAG, "Switching to debug monitoring"); + } + mCurrentMonitoringIntervalMillis = mDebugMonitoringIntervalMillis; + } else { + stopMonitoringCpuStatsLocked(); + } + } + + @GuardedBy("mLock") + private boolean hasClientCallbacksLocked() { + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + if (mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKeyAt(i) > 0) { + return true; + } + } + return false; + } + + @GuardedBy("mLock") + private void stopMonitoringCpuStatsLocked() { + mHandler.removeCallbacks(mMonitorCpuStats); + mCurrentMonitoringIntervalMillis = DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + // When the monitoring is stopped, the latest CPU availability info and the snapshots in + // {@code mCpusetInfosByCpuset} will become obsolete soon. So, remove them. + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + mCpusetInfosByCpuset.valueAt(i).clear(); + } + } + + private static boolean containsCpuset(@CpuInfoReader.CpusetCategory int cpusetCategories, + @CpuAvailabilityMonitoringConfig.Cpuset int expectedCpuset) { + switch (expectedCpuset) { + case CPUSET_ALL: + return (cpusetCategories & FLAG_CPUSET_CATEGORY_TOP_APP) != 0; + case CPUSET_BACKGROUND: + return (cpusetCategories & FLAG_CPUSET_CATEGORY_BACKGROUND) != 0; + default: + Slogf.wtf(TAG, "Provided invalid expectedCpuset %d", expectedCpuset); + } + return false; + } + + private static boolean didCrossAnyThreshold(int prevAvailabilityPercent, + int curAvailabilityPercent, IntArray thresholds) { + if (prevAvailabilityPercent == curAvailabilityPercent) { + return false; + } + for (int i = 0; i < thresholds.size(); i++) { + int threshold = thresholds.get(i); + // TODO(b/267500110): Identify whether or not the clients need to be notified when + // the CPU availability jumps too frequently around the provided thresholds. + // A. Should the client be notified twice - once when the availability reaches + // the threshold and once when it moves away (increase/decrease) from the threshold + // immediately? + // B. Should there be some sort of rate-limiting to avoid notifying the client too + // frequently? Should the client be able to config the rate-limit? + if (prevAvailabilityPercent < threshold && curAvailabilityPercent >= threshold) { + return true; + } + if (prevAvailabilityPercent >= threshold && curAvailabilityPercent < threshold) { + return true; + } + } + return false; + } + private static final class CpuAvailabilityCallbackInfo { + public final CpuMonitorService service; public final CpuAvailabilityMonitoringConfig config; + public final CpuMonitorInternal.CpuAvailabilityCallback callback; + @Nullable public final Executor executor; + public final Runnable notifyMonitoringIntervalChangeRunnable = new Runnable() { + @Override + public void run() { + callback.onMonitoringIntervalChanged(service.getCurrentMonitoringIntervalMillis()); + } + }; + public final NotifyCpuAvailabilityChangeRunnable notifyCpuAvailabilityChangeRunnable = + new NotifyCpuAvailabilityChangeRunnable(); - CpuAvailabilityCallbackInfo(CpuAvailabilityMonitoringConfig config, - Executor executor) { + CpuAvailabilityCallbackInfo(CpuMonitorService service, + CpuAvailabilityMonitoringConfig config, + CpuMonitorInternal.CpuAvailabilityCallback callback, @Nullable Executor executor) { + this.service = service; this.config = config; + this.callback = callback; this.executor = executor; } @Override public String toString() { - return "CpuAvailabilityCallbackInfo{" + "config=" + config + ", mExecutor=" + executor - + '}'; + return "CpuAvailabilityCallbackInfo{config = " + config + ", callback = " + callback + + ", mExecutor = " + executor + '}'; + } + + private final class NotifyCpuAvailabilityChangeRunnable implements Runnable { + private final Object mLock = new Object(); + @GuardedBy("mLock") + private CpuAvailabilityInfo mCpuAvailabilityInfo; + + public void prepare(CpuAvailabilityInfo cpuAvailabilityInfo) { + synchronized (mLock) { + mCpuAvailabilityInfo = cpuAvailabilityInfo; + } + } + + @Override + public void run() { + synchronized (mLock) { + callback.onAvailabilityChanged(mCpuAvailabilityInfo); + } + } } } @@ -170,4 +515,157 @@ public final class CpuMonitorService extends SystemService { PriorityDump.dump(mPriorityDumper, fd, pw, args); } } + + private static final class CpusetInfo { + @CpuAvailabilityMonitoringConfig.Cpuset + public final int cpuset; + private final LongSparseArray<Snapshot> mSnapshotsByUptime; + @Nullable + private CpuAvailabilityInfo mLatestCpuAvailabilityInfo; + + CpusetInfo(int cpuset) { + this.cpuset = cpuset; + mSnapshotsByUptime = new LongSparseArray<>(); + } + + public void appendCpuInfo(long uptimeMillis, CpuInfoReader.CpuInfo cpuInfo) { + if (!containsCpuset(cpuInfo.cpusetCategories, cpuset)) { + return; + } + Snapshot currentSnapshot = mSnapshotsByUptime.get(uptimeMillis); + if (currentSnapshot == null) { + currentSnapshot = new Snapshot(uptimeMillis); + mSnapshotsByUptime.append(uptimeMillis, currentSnapshot); + if (mSnapshotsByUptime.size() > 0 + && (uptimeMillis - mSnapshotsByUptime.valueAt(0).uptimeMillis) + > CACHE_DURATION_MILLISECONDS) { + mSnapshotsByUptime.removeAt(0); + } + } + currentSnapshot.appendCpuInfo(cpuInfo); + } + + @Nullable + public CpuAvailabilityInfo getLatestCpuAvailabilityInfo() { + return mLatestCpuAvailabilityInfo; + } + + public void populateLatestCpuAvailabilityInfo(long currentUptimeMillis, + long latestAvailabilityDurationMillis) { + int numSnapshots = mSnapshotsByUptime.size(); + if (numSnapshots == 0) { + mLatestCpuAvailabilityInfo = null; + return; + } + Snapshot latestSnapshot = mSnapshotsByUptime.valueAt(numSnapshots - 1); + if (latestSnapshot.uptimeMillis != currentUptimeMillis) { + // When the cpuset has no stats available for the current polling, the uptime will + // mismatch. When this happens, return {@code null} to avoid returning stale + // information. + if (DEBUG) { + Slogf.d(TAG, "Skipping stale CPU availability information for cpuset %s", + CpuAvailabilityMonitoringConfig.toCpusetString(cpuset)); + } + mLatestCpuAvailabilityInfo = null; + return; + } + // Avoid constructing {@link mLatestCpuAvailabilityInfo} if the uptime hasn't changed. + if (mLatestCpuAvailabilityInfo != null + && mLatestCpuAvailabilityInfo.dataTimestampUptimeMillis + == latestSnapshot.uptimeMillis) { + return; + } + long earliestUptimeMillis = currentUptimeMillis - latestAvailabilityDurationMillis; + mLatestCpuAvailabilityInfo = new CpuAvailabilityInfo(cpuset, + latestSnapshot.uptimeMillis, latestSnapshot.getAverageAvailableCpuFreqPercent(), + getCumulativeAvgAvailabilityPercent(earliestUptimeMillis), + latestAvailabilityDurationMillis); + } + + public int getPrevCpuAvailabilityPercent() { + int numSnapshots = mSnapshotsByUptime.size(); + if (numSnapshots < 2) { + return -1; + } + return mSnapshotsByUptime.valueAt(numSnapshots - 2).getAverageAvailableCpuFreqPercent(); + } + + private int getCumulativeAvgAvailabilityPercent(long earliestUptimeMillis) { + long totalAvailableCpuFreqKHz = 0; + long totalOnlineMaxCpuFreqKHz = 0; + int totalAccountedSnapshots = 0; + long earliestSeenUptimeMillis = Long.MAX_VALUE; + for (int i = mSnapshotsByUptime.size() - 1; i >= 0; i--) { + Snapshot snapshot = mSnapshotsByUptime.valueAt(i); + earliestSeenUptimeMillis = snapshot.uptimeMillis; + if (snapshot.uptimeMillis <= earliestUptimeMillis) { + break; + } + totalAccountedSnapshots++; + totalAvailableCpuFreqKHz += snapshot.totalNormalizedAvailableCpuFreqKHz; + totalOnlineMaxCpuFreqKHz += snapshot.totalOnlineMaxCpuFreqKHz; + } + // The cache must have at least 2 snapshots within the given duration and + // the {@link earliestSeenUptimeMillis} must be earlier than (i,e., less than) the given + // {@link earliestUptimeMillis}. Otherwise, the cache doesn't have enough data to + // calculate the cumulative average for the given duration. + // TODO(b/267500110): Investigate whether the cumulative average duration should be + // shrunk when not enough data points are available. + if (earliestSeenUptimeMillis > earliestUptimeMillis || totalAccountedSnapshots < 2) { + return CpuAvailabilityInfo.MISSING_CPU_AVAILABILITY_PERCENT; + } + return (int) ((totalAvailableCpuFreqKHz * 100.0) / totalOnlineMaxCpuFreqKHz); + } + + public void clear() { + mLatestCpuAvailabilityInfo = null; + mSnapshotsByUptime.clear(); + } + + @Override + public String toString() { + return "CpusetInfo{cpuset = " + CpuAvailabilityMonitoringConfig.toCpusetString(cpuset) + + ", mSnapshotsByUptime = " + mSnapshotsByUptime + + ", mLatestCpuAvailabilityInfo = " + mLatestCpuAvailabilityInfo + '}'; + } + + private static final class Snapshot { + public final long uptimeMillis; + public int totalOnlineCpus; + public int totalOfflineCpus; + public long totalNormalizedAvailableCpuFreqKHz; + public long totalOnlineMaxCpuFreqKHz; + public long totalOfflineMaxCpuFreqKHz; + + Snapshot(long uptimeMillis) { + this.uptimeMillis = uptimeMillis; + } + + public void appendCpuInfo(CpuInfoReader.CpuInfo cpuInfo) { + if (!cpuInfo.isOnline) { + totalOfflineCpus++; + totalOfflineMaxCpuFreqKHz += cpuInfo.maxCpuFreqKHz; + return; + } + ++totalOnlineCpus; + totalNormalizedAvailableCpuFreqKHz += cpuInfo.getNormalizedAvailableCpuFreqKHz(); + totalOnlineMaxCpuFreqKHz += cpuInfo.maxCpuFreqKHz; + } + + public int getAverageAvailableCpuFreqPercent() { + return (int) ((totalNormalizedAvailableCpuFreqKHz * 100.0) + / totalOnlineMaxCpuFreqKHz); + } + + @Override + public String toString() { + return "Snapshot{uptimeMillis = " + uptimeMillis + ", totalOnlineCpus = " + + totalOnlineCpus + ", totalOfflineCpus = " + totalOfflineCpus + + ", totalNormalizedAvailableCpuFreqKHz = " + + totalNormalizedAvailableCpuFreqKHz + + ", totalOnlineMaxCpuFreqKHz = " + totalOnlineMaxCpuFreqKHz + + ", totalOfflineMaxCpuFreqKHz = " + totalOfflineMaxCpuFreqKHz + '}'; + } + } + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java index b21478753301..04f6f8b2e9f0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java @@ -92,6 +92,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, + /* normalizedAvailableCpuFreqKHz= */ 2_402_267, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -101,6 +102,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, + /* normalizedAvailableCpuFreqKHz= */ 2_693_525, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -111,6 +113,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_901_608, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, @@ -121,6 +124,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_907_125, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, @@ -139,6 +143,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 419_354, + /* normalizedAvailableCpuFreqKHz= */ 2_425_919, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, @@ -148,6 +153,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 429_032, + /* normalizedAvailableCpuFreqKHz= */ 2_403_009, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, @@ -158,6 +164,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 403_225, + /* normalizedAvailableCpuFreqKHz= */ 1_688_209, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, @@ -168,6 +175,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ false, /* curCpuFreqKHz= */ MISSING_FREQUENCY, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ MISSING_FREQUENCY, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, @@ -189,6 +197,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2_253_713, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -198,6 +207,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2_492_687, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -208,6 +218,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 1_788_079, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, @@ -218,6 +229,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 1_799_962, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, @@ -237,6 +249,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2323347, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, @@ -246,6 +259,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 209111, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, @@ -256,6 +270,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 453514, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, @@ -266,6 +281,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 37728, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, @@ -323,38 +339,8 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>(); - expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 3_000_000, - /* maxCpuFreqKHz= */ 1_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, - /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, - /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, - /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130, - /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 9, /* maxCpuFreqKHz= */ 2, - /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, - /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, - /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, - /* irqTimeMillis= */ 14_796_940, /* softirqTimeMillis= */ 1_478_130, - /* stealTimeMillis= */ 88_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 9, /* maxCpuFreqKHz= */ 2, - /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, - /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, - /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, - /* irqTimeMillis= */ 8_136_740, /* softirqTimeMillis= */ 438_970, - /* stealTimeMillis= */ 71_950, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - compareCpuInfos("CPU infos with corrupted CPU frequency", expectedCpuInfos, - actualCpuInfos); + compareCpuInfos("CPU infos with corrupted CPU frequency", expectedCpuInfos, actualCpuInfos); } @Test @@ -368,6 +354,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, + /* normalizedAvailableCpuFreqKHz= */ 2_402_267, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -377,6 +364,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, + /* normalizedAvailableCpuFreqKHz= */ 2_693_525, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -393,7 +381,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { assertWithMessage("Make empty dir %s", emptyDir).that(emptyDir.mkdir()).isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(emptyDir, getCacheFile( VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), - getCacheFile(VALID_PROC_STAT)); + getCacheFile(VALID_PROC_STAT), /* minReadIntervalMillis= */0); assertWithMessage("Init CPU reader info").that(cpuInfoReader.init()).isFalse(); @@ -406,7 +394,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { File emptyDir = getCacheFile(EMPTY_DIR); assertWithMessage("Make empty dir %s", emptyDir).that(emptyDir.mkdir()).isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), emptyDir, - getCacheFile(VALID_PROC_STAT)); + getCacheFile(VALID_PROC_STAT), /* minReadIntervalMillis= */0); assertWithMessage("Init CPU reader info").that(cpuInfoReader.init()).isFalse(); @@ -420,12 +408,32 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { assertWithMessage("Create empty file %s", emptyFile).that(emptyFile.createNewFile()) .isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), - getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(EMPTY_FILE)); + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(EMPTY_FILE), + /* minReadIntervalMillis= */0); assertWithMessage("Cpu infos with empty proc stat").that(cpuInfoReader.readCpuInfos()) .isNull(); } + @Test + public void testReadingTooFrequentlyReturnsLastReadCpuInfos() throws Exception { + CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT), + /* minReadIntervalMillis= */ 60_000); + assertWithMessage("Initialize CPU info reader").that(cpuInfoReader.init()).isTrue(); + + SparseArray<CpuInfoReader.CpuInfo> firstCpuInfos = cpuInfoReader.readCpuInfos(); + assertWithMessage("CPU infos first snapshot").that(firstCpuInfos).isNotNull(); + assertWithMessage("CPU infos first snapshot size").that(firstCpuInfos.size()) + .isGreaterThan(0); + + SparseArray<CpuInfoReader.CpuInfo> secondCpuInfos = cpuInfoReader.readCpuInfos(); + compareCpuInfos("CPU infos second snapshot", firstCpuInfos, secondCpuInfos); + + SparseArray<CpuInfoReader.CpuInfo> thirdCpuInfos = cpuInfoReader.readCpuInfos(); + compareCpuInfos("CPU infos third snapshot", firstCpuInfos, thirdCpuInfos); + } + private void compareCpuInfos(String message, SparseArray<CpuInfoReader.CpuInfo> expected, SparseArray<CpuInfoReader.CpuInfo> actual) { @@ -462,7 +470,8 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { private static CpuInfoReader newCpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile) { - CpuInfoReader cpuInfoReader = new CpuInfoReader(cpusetDir, cpuFreqDir, procStatFile); + CpuInfoReader cpuInfoReader = new CpuInfoReader(cpusetDir, cpuFreqDir, procStatFile, + /* minReadIntervalMillis= */ 0); assertWithMessage("Initialize CPU info reader").that(cpuInfoReader.init()).isTrue(); return cpuInfoReader; } diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java index 49a2cc696744..5a5f5256e37d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java @@ -17,105 +17,659 @@ package com.android.server.cpu; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.cpu.CpuAvailabilityInfo.MISSING_CPU_AVAILABILITY_PERCENT; import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.CpuInfo.MISSING_FREQUENCY; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; +import static com.android.server.cpu.CpuMonitorService.DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.when; import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.Looper; import android.os.ServiceManager; +import android.util.ArraySet; +import android.util.SparseArray; import com.android.server.ExtendedMockitoRule; import com.android.server.LocalServices; +import com.android.server.Watchdog; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.stubbing.OngoingStubbing; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public final class CpuMonitorServiceTest { - private static final CpuAvailabilityMonitoringConfig TEST_CPU_AVAILABILITY_MONITORING_CONFIG = + private static final String TAG = CpuMonitorServiceTest.class.getSimpleName(); + private static final String USER_BUILD_TAG = TAG + "UserBuild"; + private static final long ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS = + TimeUnit.SECONDS.toMillis(1); + private static final long HANDLER_THREAD_SYNC_TIMEOUT_MILLISECONDS = + TimeUnit.SECONDS.toMillis(5); + private static final long TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS = 100; + private static final long TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS = 150; + private static final long TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS = 300; + private static final CpuAvailabilityMonitoringConfig TEST_MONITORING_CONFIG_ALL_CPUSET = new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL) .addThreshold(30).addThreshold(70).build(); + private static final CpuAvailabilityMonitoringConfig TEST_MONITORING_CONFIG_BG_CPUSET = + new CpuAvailabilityMonitoringConfig.Builder(CPUSET_BACKGROUND) + .addThreshold(50).addThreshold(90).build(); + private static final List<StaticCpuInfo> STATIC_CPU_INFOS = List.of( + new StaticCpuInfo(/* cpuCore= */ 0, + /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP, + /* maxCpuFreqKHz= */ 4000), + new StaticCpuInfo(/* cpuCore= */ 1, + /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP, + /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 2, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 3, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 4, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 2000)); + private static final ArraySet<Integer> NO_OFFLINE_CORES = new ArraySet<>(); - private static final CpuAvailabilityMonitoringConfig TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2 = - new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL) - .addThreshold(10).addThreshold(90).build(); - @Mock - private Context mContext; + private Context mMockContext; + @Mock + private CpuInfoReader mMockCpuInfoReader; + @Captor + private ArgumentCaptor<CpuAvailabilityInfo> mCpuAvailabilityInfoCaptor; + private HandlerThread mServiceHandlerThread; + private Handler mServiceHandler; private CpuMonitorService mService; - private HandlerExecutor mHandlerExecutor; private CpuMonitorInternal mLocalService; @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) .mockStatic(ServiceManager.class) + .mockStatic(Watchdog.class) .build(); @Before - public void setUp() { - mService = new CpuMonitorService(mContext); - mHandlerExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper())); + public void setUp() throws Exception { + mServiceHandlerThread = new HandlerThread(TAG); + mService = new CpuMonitorService(mMockContext, mMockCpuInfoReader, mServiceHandlerThread, + /* shouldDebugMonitor= */ true, TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, + TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + doNothing().when(() -> ServiceManager.addService(eq("cpu_monitor"), any(Binder.class), anyBoolean(), anyInt())); - mService.onStart(); - mLocalService = LocalServices.getService(CpuMonitorInternal.class); + doReturn(mock(Watchdog.class)).when(Watchdog::getInstance); + when(mMockCpuInfoReader.init()).thenReturn(true); + when(mMockCpuInfoReader.readCpuInfos()).thenReturn(new SparseArray<>()); + + startService(); } @After - public void tearDown() { - // The CpuMonitorInternal.class service is added by the mService.onStart call. - // Remove the service to ensure the setUp procedure can add this service again. - LocalServices.removeServiceForTest(CpuMonitorInternal.class); + public void tearDown() throws Exception { + terminateService(); } @Test - public void testAddRemoveCpuAvailabilityCallback() { + public void testAddRemoveCpuAvailabilityCallbackOnDebugBuild() throws Exception { CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback); + mLocalService.addCpuAvailabilityCallback(/* executor= */ null, + TEST_MONITORING_CONFIG_ALL_CPUSET, mockCallback); + + assertWithMessage("Monitoring interval after adding a client callback") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + // Monitoring interval changed notification is sent asynchronously from the handler thread. + // So, sync with this thread before verifying the client call. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); - // TODO(b/242722241): Verify that {@link mockCallback.onAvailabilityChanged} and - // {@link mockCallback.onMonitoringIntervalChanged} are called when the callback is added. + verify(mockCallback, never()).onAvailabilityChanged(any()); mLocalService.removeCpuAvailabilityCallback(mockCallback); - } + assertWithMessage("Monitoring interval after removing all client callbacks") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS); + } @Test - public void testDuplicateAddCpuAvailabilityCallback() { + public void testAddRemoveCpuAvailabilityCallbackOnUserBuild() throws Exception { + // The default service instantiated during test setUp has the debug monitoring enabled. + // But on a user build, debug monitoring is disabled. So, replace the default service with + // an equivalent user build service. + replaceServiceWithUserBuildService(); + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback); + mLocalService.addCpuAvailabilityCallback(/* executor= */ null, + TEST_MONITORING_CONFIG_ALL_CPUSET, mockCallback); + + assertWithMessage("Monitoring interval after adding a client callback") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2, mockCallback); + // Monitoring interval changed notification is sent asynchronously from the handler thread. + // So, sync with this thread before verifying the client call. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); - // TODO(b/242722241): Verify that {@link mockCallback} is called only when CPU availability - // thresholds cross the bounds specified in the - // {@link TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2} config. + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + verify(mockCallback, never()).onAvailabilityChanged(any()); mLocalService.removeCpuAvailabilityCallback(mockCallback); + + assertWithMessage("Monitoring interval after removing all client callbacks") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(DEFAULT_MONITORING_INTERVAL_MILLISECONDS); } @Test - public void testRemoveInvalidCpuAvailabilityCallback() { + public void testRemoveInvalidCpuAvailabilityCallback() throws Exception { CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); mLocalService.removeCpuAvailabilityCallback(mockCallback); } + + @Test + public void testReceiveCpuAvailabilityCallbackOnAddingFirstCallback() throws Exception { + // Debug monitoring is in progress but the default {@link CpuInfoReader.CpuInfo} returned by + // the {@link CpuInfoReader.readCpuInfos} is empty, so the client won't be notified when + // adding a callback. Inject {@link CpuInfoReader.CpuInfo}, so the client callback is + // notified on adding a callback. + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES))); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 10, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testReceiveCpuAvailabilityCallbackOnAddingMultipleCallbacks() throws Exception { + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES))); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 10, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithSingleCallback() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 82.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(4)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, + /* pastNMillisAvgAvailabilityPercent= */ 45, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(3).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 82, + /* pastNMillisAvgAvailabilityPercent= */ 57, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithMultipleCallbacks() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockAllCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockBgCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 5.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 20.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 75.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES))); + + verify(mockAllCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 75, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 60, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_ALL callback").that(actual) + .isEqualTo(expected); + + ArgumentCaptor<CpuAvailabilityInfo> bgCpusetAvailabilityInfoCaptor = + ArgumentCaptor.forClass(CpuAvailabilityInfo.class); + + verify(mockBgCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(bgCpusetAvailabilityInfoCaptor.capture()); + + actual = bgCpusetAvailabilityInfoCaptor.getAllValues(); + expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, + /* pastNMillisAvgAvailabilityPercent= */ 36, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, + /* pastNMillisAvgAvailabilityPercent= */ 75, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 60, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_BACKGROUND callback").that(actual) + .isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithOfflineCores() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockAllCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockBgCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + // Disable one top-app and one all cpuset core. + ArraySet<Integer> offlineCoresA = new ArraySet<>(); + offlineCoresA.add(1); + offlineCoresA.add(3); + + // Disable two all cpuset cores. + ArraySet<Integer> offlineCoresB = new ArraySet<>(); + offlineCoresB.add(2); + offlineCoresB.add(4); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 5.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 20.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 75.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + offlineCoresA))); + + verify(mockAllCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 75, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 61, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_ALL callback").that(actual) + .isEqualTo(expected); + + ArgumentCaptor<CpuAvailabilityInfo> bgCpusetAvailabilityInfoCaptor = + ArgumentCaptor.forClass(CpuAvailabilityInfo.class); + + verify(mockBgCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(bgCpusetAvailabilityInfoCaptor.capture()); + + actual = bgCpusetAvailabilityInfoCaptor.getAllValues(); + expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, + /* pastNMillisAvgAvailabilityPercent= */ 35, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, + /* pastNMillisAvgAvailabilityPercent= */ 75, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_BACKGROUND callback").that(actual) + .isEqualTo(expected); + } + + @Test + public void testReceiveCpuAvailabilityCallbacksOnExecutorThread() throws Exception { + Handler testHandler = new Handler(Looper.getMainLooper()); + + assertWithMessage("Test main handler").that(testHandler).isNotNull(); + + HandlerExecutor testExecutor = new HandlerExecutor(testHandler); + + assertWithMessage("Test main executor").that(testExecutor).isNotNull(); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(testHandler, testExecutor, + TEST_MONITORING_CONFIG_ALL_CPUSET); + + // CPU monitoring is started on the service handler thread. Sync with this thread before + // proceeding. Otherwise, debug monitoring may consume the injected CPU infos and cause + // the test to be flaky. Because the {@link addCpuAvailabilityCallback} syncs only with + // the passed handler, the test must explicitly sync with the service handler. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); + + injectCpuInfosAndWait(testHandler, List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 82.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(4)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, + /* pastNMillisAvgAvailabilityPercent= */ 45, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(3).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 82, + /* pastNMillisAvgAvailabilityPercent= */ 57, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testDuplicateAddCpuAvailabilityCallback() throws Exception { + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 40.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 80.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 95.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(2)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + // Verify that the callback is called for the last added monitoring config. + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 95, + /* pastNMillisAvgAvailabilityPercent= */ 78, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testHeavyCpuLoadMonitoring() throws Exception { + // TODO(b/267500110): Once heavy CPU load detection logic is added, add unittest. + } + + private void startService() { + mService.onStart(); + mServiceHandler = mServiceHandlerThread.getThreadHandler(); + + assertWithMessage("Service thread handler").that(mServiceHandler).isNotNull(); + + mLocalService = LocalServices.getService(CpuMonitorInternal.class); + + assertWithMessage("CpuMonitorInternal local service").that(mLocalService).isNotNull(); + } + + private void terminateService() { + // The CpuMonitorInternal.class service is added by the {@link CpuMonitorService#onStart} + // call. Remove the service to ensure this service can be added again during + // the {@link CpuMonitorService#onStart} call. + LocalServices.removeServiceForTest(CpuMonitorInternal.class); + if (mServiceHandlerThread != null && mServiceHandlerThread.isAlive()) { + mServiceHandlerThread.quitSafely(); + } + } + + private void replaceServiceWithUserBuildService() { + terminateService(); + mServiceHandlerThread = new HandlerThread(USER_BUILD_TAG); + mService = new CpuMonitorService(mMockContext, mMockCpuInfoReader, + mServiceHandlerThread, /* shouldDebugMonitor= */ false, + TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, + TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + + startService(); + } + + private CpuMonitorInternal.CpuAvailabilityCallback addCpuAvailabilityCallback( + CpuAvailabilityMonitoringConfig config) throws Exception { + return addCpuAvailabilityCallback(mServiceHandler, /* executor= */ null, config); + } + + private CpuMonitorInternal.CpuAvailabilityCallback addCpuAvailabilityCallback(Handler handler, + HandlerExecutor executor, CpuAvailabilityMonitoringConfig config) throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( + CpuMonitorInternal.CpuAvailabilityCallback.class); + + mLocalService.addCpuAvailabilityCallback(executor, config, mockCallback); + + // Monitoring interval changed notification is sent asynchronously from the given handler. + // So, sync with this thread before verifying the client call. + syncWithHandler(handler, /* delayMillis= */ 0); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + return mockCallback; + } + + private void injectCpuInfosAndWait(List<SparseArray<CpuInfoReader.CpuInfo>> cpuInfos) + throws Exception { + injectCpuInfosAndWait(mServiceHandler, cpuInfos); + } + + private void injectCpuInfosAndWait(Handler handler, + List<SparseArray<CpuInfoReader.CpuInfo>> cpuInfos) throws Exception { + assertWithMessage("CPU info configs").that(cpuInfos).isNotEmpty(); + + OngoingStubbing<SparseArray<CpuInfoReader.CpuInfo>> ongoingStubbing = + when(mMockCpuInfoReader.readCpuInfos()); + for (SparseArray<CpuInfoReader.CpuInfo> cpuInfo : cpuInfos) { + ongoingStubbing = ongoingStubbing.thenReturn(cpuInfo); + } + + // CPU infos are read asynchronously on a separate handler thread. So, wait based on + // the current monitoring interval and the number of CPU infos were injected. + syncWithHandler(handler, + /* delayMillis= */ mService.getCurrentMonitoringIntervalMillis() * cpuInfos.size()); + } + + private void syncWithHandler(Handler handler, long delayMillis) throws Exception { + AtomicBoolean didRun = new AtomicBoolean(false); + handler.postDelayed(() -> { + synchronized (didRun) { + didRun.set(true); + didRun.notifyAll(); + } + }, delayMillis); + synchronized (didRun) { + while (!didRun.get()) { + didRun.wait(HANDLER_THREAD_SYNC_TIMEOUT_MILLISECONDS); + } + } + } + + private static SparseArray<CpuInfoReader.CpuInfo> generateCpuInfosForAvailability( + double cpuAvailabilityPercent, ArraySet<Integer> offlineCores) { + SparseArray<CpuInfoReader.CpuInfo> cpuInfos = new SparseArray<>(STATIC_CPU_INFOS.size()); + for (StaticCpuInfo staticCpuInfo : STATIC_CPU_INFOS) { + boolean isOnline = !offlineCores.contains(staticCpuInfo.cpuCore); + cpuInfos.append(staticCpuInfo.cpuCore, constructCpuInfo(staticCpuInfo.cpuCore, + staticCpuInfo.cpusetCategories, isOnline, staticCpuInfo.maxCpuFreqKHz, + cpuAvailabilityPercent)); + } + return cpuInfos; + } + + private static CpuInfoReader.CpuInfo constructCpuInfo(int cpuCore, + @CpuInfoReader.CpusetCategory int cpusetCategories, boolean isOnline, + long maxCpuFreqKHz, double cpuAvailabilityPercent) { + long availCpuFreqKHz = (long) (maxCpuFreqKHz * (cpuAvailabilityPercent / 100.0)); + long curCpuFreqKHz = maxCpuFreqKHz - availCpuFreqKHz; + return new CpuInfoReader.CpuInfo(cpuCore, cpusetCategories, isOnline, + isOnline ? curCpuFreqKHz : MISSING_FREQUENCY, maxCpuFreqKHz, + /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + isOnline ? availCpuFreqKHz : MISSING_FREQUENCY, + /* latestCpuUsageStats= */ null); + } + + private static final class StaticCpuInfo { + public final int cpuCore; + public final int cpusetCategories; + public final int maxCpuFreqKHz; + + StaticCpuInfo(int cpuCore, @CpuInfoReader.CpusetCategory int cpusetCategories, + int maxCpuFreqKHz) { + this.cpuCore = cpuCore; + this.cpusetCategories = cpusetCategories; + this.maxCpuFreqKHz = maxCpuFreqKHz; + } + + @Override + public String toString() { + return "StaticCpuInfo{cpuCore=" + cpuCore + ", cpusetCategories=" + cpusetCategories + + ", maxCpuFreqKHz=" + maxCpuFreqKHz + '}'; + } + } } |