diff options
5 files changed, 547 insertions, 0 deletions
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 000000000000..06b45bf0fb4b --- /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 000000000000..a3c4c9e828b4 --- /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 000000000000..849a20be0cf8 --- /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 000000000000..b0dfb8467fa7 --- /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 000000000000..7ab1363b5087 --- /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); + } +} |