Merge "Add CpuMonitorService to monitor CPU stats."
diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java
new file mode 100644
index 0000000..06b45bf
--- /dev/null
+++ b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cpu;
+
+import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL;
+import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND;
+
+import com.android.internal.util.Preconditions;
+
+/** CPU availability information. */
+public final class CpuAvailabilityInfo {
+ /** Constant to indicate missing CPU availability percent. */
+ public static final int MISSING_CPU_AVAILABILITY_PERCENT = -1;
+
+ /**
+ * The CPUSET whose availability info is recorded in this object.
+ *
+ * <p>The contained value is one of the CPUSET_* constants from the
+ * {@link CpuAvailabilityMonitoringConfig}.
+ */
+ @CpuAvailabilityMonitoringConfig.Cpuset
+ public final int cpuset;
+
+ /** The latest average CPU availability percent. */
+ public final int latestAvgAvailabilityPercent;
+
+ /** The past N-second average CPU availability percent. */
+ public final int pastNSecAvgAvailabilityPercent;
+
+ /** The duration over which the {@link pastNSecAvgAvailabilityPercent} was calculated. */
+ public final int avgAvailabilityDurationSec;
+
+ @Override
+ public String toString() {
+ return "CpuAvailabilityInfo{" + "cpuset=" + cpuset + ", latestAvgAvailabilityPercent="
+ + latestAvgAvailabilityPercent + ", pastNSecAvgAvailabilityPercent="
+ + pastNSecAvgAvailabilityPercent + ", avgAvailabilityDurationSec="
+ + avgAvailabilityDurationSec + '}';
+ }
+
+ CpuAvailabilityInfo(int cpuset, int latestAvgAvailabilityPercent,
+ int pastNSecAvgAvailabilityPercent, int avgAvailabilityDurationSec) {
+ this.cpuset = Preconditions.checkArgumentInRange(cpuset, CPUSET_ALL, CPUSET_BACKGROUND,
+ "cpuset");
+ this.latestAvgAvailabilityPercent = latestAvgAvailabilityPercent;
+ this.pastNSecAvgAvailabilityPercent = pastNSecAvgAvailabilityPercent;
+ this.avgAvailabilityDurationSec = avgAvailabilityDurationSec;
+ }
+}
diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java
new file mode 100644
index 0000000..a3c4c9e
--- /dev/null
+++ b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cpu;
+
+import android.annotation.IntDef;
+import android.util.IntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** CPU availability monitoring config. */
+public final class CpuAvailabilityMonitoringConfig {
+ /** Constant to monitor all cpusets. */
+ public static final int CPUSET_ALL = 1;
+
+ /** Constant to monitor background cpusets. */
+ public static final int CPUSET_BACKGROUND = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"CPUSET_"}, value = {
+ CPUSET_ALL,
+ CPUSET_BACKGROUND
+ })
+ public @interface Cpuset {
+ }
+
+ /**
+ * The CPUSET to monitor.
+ *
+ * <p>The value must be one of the {@code CPUSET_*} constants.
+ */
+ @Cpuset
+ public final int cpuset;
+
+ /**
+ * CPU availability percent thresholds.
+ *
+ * <p>CPU availability change notifications are sent when the latest or last N seconds average
+ * CPU availability percent crosses any of these thresholds since the last notification.
+ */
+ private final IntArray mThresholds;
+
+ public IntArray getThresholds() {
+ return mThresholds;
+ }
+
+ /**
+ * Builder for the construction of {@link CpuAvailabilityMonitoringConfig} objects.
+ *
+ * <p>The builder must contain at least one threshold before calling {@link build}.
+ */
+ public static final class Builder {
+ private final int mCpuset;
+ private final IntArray mThresholds = new IntArray();
+
+ public Builder(int cpuset, int... thresholds) {
+ mCpuset = cpuset;
+ for (int threshold : thresholds) {
+ addThreshold(threshold);
+ }
+ }
+
+ /** Adds the given threshold to the builder object. */
+ public Builder addThreshold(int threshold) {
+ if (mThresholds.indexOf(threshold) == -1) {
+ mThresholds.add(threshold);
+ }
+ return this;
+ }
+
+ /** Returns the {@link CpuAvailabilityMonitoringConfig} object. */
+ public CpuAvailabilityMonitoringConfig build() {
+ return new CpuAvailabilityMonitoringConfig(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "CpuAvailabilityMonitoringConfig{cpuset=" + cpuset + ", mThresholds=" + mThresholds
+ + ')';
+ }
+
+ private CpuAvailabilityMonitoringConfig(Builder builder) {
+ if (builder.mCpuset != CPUSET_ALL && builder.mCpuset != CPUSET_BACKGROUND) {
+ throw new IllegalStateException("Cpuset must be either CPUSET_ALL (" + CPUSET_ALL
+ + ") or CPUSET_BACKGROUND (" + CPUSET_BACKGROUND + "). Builder contains "
+ + builder.mCpuset);
+ }
+ if (builder.mThresholds.size() == 0) {
+ throw new IllegalStateException("Must provide at least one threshold");
+ }
+ this.cpuset = builder.mCpuset;
+ this.mThresholds = builder.mThresholds.clone();
+ }
+}
diff --git a/services/core/java/com/android/server/cpu/CpuMonitorInternal.java b/services/core/java/com/android/server/cpu/CpuMonitorInternal.java
new file mode 100644
index 0000000..849a20b
--- /dev/null
+++ b/services/core/java/com/android/server/cpu/CpuMonitorInternal.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cpu;
+
+import android.annotation.CallbackExecutor;
+
+import java.util.concurrent.Executor;
+
+/** CpuMonitorInternal hosts internal APIs to monitor CPU. */
+public abstract class CpuMonitorInternal {
+ /** Callback to get CPU availability change notifications. */
+ public interface CpuAvailabilityCallback {
+ /**
+ * Called when the CPU availability crosses the provided thresholds.
+ *
+ * <p>Called when the latest or past N-second (which will be specified in the
+ * {@link CpuAvailabilityInfo}) average CPU availability percent has crossed
+ * (either goes above or drop below) the {@link CpuAvailabilityMonitoringConfig#thresholds}
+ * since the last notification. Also called when a callback is added to the service.
+ *
+ * <p>The callback is called at the executor which is specified in
+ * {@link addCpuAvailabilityCallback} or at the service handler thread.
+ *
+ * @param info CPU availability information.
+ */
+ void onAvailabilityChanged(CpuAvailabilityInfo info);
+
+ /**
+ * Called when the CPU monitoring interval changes.
+ *
+ * <p>Also called when a callback is added to the service.
+ *
+ * @param intervalMilliseconds CPU monitoring interval in milliseconds.
+ */
+ void onMonitoringIntervalChanged(long intervalMilliseconds);
+ }
+
+ /**
+ * Adds the {@link CpuAvailabilityCallback} for the caller.
+ *
+ * <p>When the callback is added, the callback will be called to notify the current CPU
+ * availability and monitoring interval.
+ *
+ * <p>When the client needs to update the {@link config} for a previously added callback,
+ * the client has to remove the callback and add the callback with a new {@link config}.
+ *
+ * @param executor Executor to execute the callback. If an executor is not provided,
+ * the callback will be executed on the service handler thread.
+ * @param config CPU availability monitoring config.
+ * @param callback Callback implementing {@link CpuAvailabilityCallback}
+ * interface.
+ *
+ * @throws IllegalStateException if {@code callback} is already added.
+ */
+ public abstract void addCpuAvailabilityCallback(@CallbackExecutor Executor executor,
+ CpuAvailabilityMonitoringConfig config, CpuAvailabilityCallback callback);
+
+ /**
+ * Removes the {@link CpuAvailabilityCallback} for the caller.
+ *
+ * @param callback Callback implementing {@link CpuAvailabilityCallback}
+ * interface.
+ *
+ * @throws IllegalArgumentException if {@code callback} is not previously added.
+ */
+ public abstract void removeCpuAvailabilityCallback(CpuAvailabilityCallback callback);
+}
diff --git a/services/core/java/com/android/server/cpu/CpuMonitorService.java b/services/core/java/com/android/server/cpu/CpuMonitorService.java
new file mode 100644
index 0000000..b0dfb84
--- /dev/null
+++ b/services/core/java/com/android/server/cpu/CpuMonitorService.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cpu;
+
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
+
+import android.content.Context;
+import android.os.Binder;
+import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.DumpUtils;
+import com.android.server.SystemService;
+import com.android.server.utils.PriorityDump;
+import com.android.server.utils.Slogf;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** Service to monitor CPU availability and usage. */
+public final class CpuMonitorService extends SystemService {
+ static final String TAG = CpuMonitorService.class.getSimpleName();
+ static final boolean DEBUG = Slogf.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;
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final ArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, CpuAvailabilityCallbackInfo>
+ mCpuAvailabilityCallbackInfoByCallbacks = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private long mMonitoringIntervalMilliseconds = DEFAULT_CPU_MONITORING_INTERVAL_MILLISECONDS;
+
+ private final CpuMonitorInternal mLocalService = new CpuMonitorInternal() {
+ @Override
+ public void addCpuAvailabilityCallback(Executor executor,
+ CpuAvailabilityMonitoringConfig config, CpuAvailabilityCallback callback) {
+ Objects.requireNonNull(callback, "Callback must be non-null");
+ Objects.requireNonNull(config, "Config must be non-null");
+ 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);
+ }
+ }
+ // 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);
+ }
+ }
+ // TODO(b/242722241): Increase CPU monitoring interval when all callbacks are removed.
+ }
+ };
+
+ public CpuMonitorService(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public void onStart() {
+ publishLocalService(CpuMonitorInternal.class, mLocalService);
+ publishBinderService("cpu_monitor", new CpuMonitorBinder(), /* allowIsolated= */ false,
+ DUMP_FLAG_PRIORITY_CRITICAL);
+ }
+
+ private void doDump(IndentingPrintWriter writer) {
+ writer.printf("*%s*\n", getClass().getSimpleName());
+ writer.increaseIndent();
+ synchronized (mLock) {
+ writer.printf("CPU monitoring interval: %d ms\n", mMonitoringIntervalMilliseconds);
+ if (!mCpuAvailabilityCallbackInfoByCallbacks.isEmpty()) {
+ 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));
+ }
+ writer.decreaseIndent();
+ }
+ }
+ // TODO(b/242722241): Print the recent past CPU stats.
+ writer.decreaseIndent();
+ }
+
+ private static final class CpuAvailabilityCallbackInfo {
+ public final CpuAvailabilityMonitoringConfig config;
+ public final Executor executor;
+
+ CpuAvailabilityCallbackInfo(CpuAvailabilityMonitoringConfig config,
+ Executor executor) {
+ this.config = config;
+ this.executor = executor;
+ }
+
+ @Override
+ public String toString() {
+ return "CpuAvailabilityCallbackInfo{" + "config=" + config + ", mExecutor=" + executor
+ + '}';
+ }
+ }
+
+ private final class CpuMonitorBinder extends Binder {
+ private final PriorityDump.PriorityDumper mPriorityDumper =
+ new PriorityDump.PriorityDumper() {
+ @Override
+ public void dumpCritical(FileDescriptor fd, PrintWriter pw, String[] args,
+ boolean asProto) {
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)
+ || asProto) {
+ return;
+ }
+ try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw)) {
+ doDump(ipw);
+ }
+ }
+ };
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ PriorityDump.dump(mPriorityDumper, fd, pw, args);
+ }
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java
new file mode 100644
index 0000000..7ab1363
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cpu;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL;
+
+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 android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Looper;
+import android.os.ServiceManager;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+import com.android.server.ExtendedMockitoTestCase;
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public final class CpuMonitorServiceTest extends ExtendedMockitoTestCase {
+ private static final CpuAvailabilityMonitoringConfig TEST_CPU_AVAILABILITY_MONITORING_CONFIG =
+ new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL)
+ .addThreshold(30).addThreshold(70).build();
+
+ 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 CpuMonitorService mService;
+ private HandlerExecutor mHandlerExecutor;
+ private CpuMonitorInternal mLocalService;
+
+ @Override
+ protected void initializeSession(StaticMockitoSessionBuilder builder) {
+ builder.mockStatic(ServiceManager.class);
+ }
+
+ @Before
+ public void setUp() {
+ mService = new CpuMonitorService(mContext);
+ mHandlerExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
+ doNothing().when(() -> ServiceManager.addService(eq("cpu_monitor"), any(Binder.class),
+ anyBoolean(), anyInt()));
+ mService.onStart();
+ mLocalService = LocalServices.getService(CpuMonitorInternal.class);
+ }
+
+ @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);
+ }
+
+ @Test
+ public void testAddRemoveCpuAvailabilityCallback() {
+ CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock(
+ CpuMonitorInternal.CpuAvailabilityCallback.class);
+
+ mLocalService.addCpuAvailabilityCallback(mHandlerExecutor,
+ TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback);
+
+ // TODO(b/242722241): Verify that {@link mockCallback.onAvailabilityChanged} and
+ // {@link mockCallback.onMonitoringIntervalChanged} are called when the callback is added.
+
+ mLocalService.removeCpuAvailabilityCallback(mockCallback);
+ }
+
+
+ @Test
+ public void testDuplicateAddCpuAvailabilityCallback() {
+ CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock(
+ CpuMonitorInternal.CpuAvailabilityCallback.class);
+
+ mLocalService.addCpuAvailabilityCallback(mHandlerExecutor,
+ TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback);
+
+ mLocalService.addCpuAvailabilityCallback(mHandlerExecutor,
+ TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2, mockCallback);
+
+ // 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.
+
+ mLocalService.removeCpuAvailabilityCallback(mockCallback);
+ }
+
+ @Test
+ public void testRemoveInvalidCpuAvailabilityCallback() {
+ CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock(
+ CpuMonitorInternal.CpuAvailabilityCallback.class);
+
+ mLocalService.removeCpuAvailabilityCallback(mockCallback);
+ }
+}