Add CpuMonitorService to monitor CPU stats.

- This service will be published as a local system server service.
- Other system server services such as job scheduler can use this
  service to monitor CPU availability.
- Support for reading Kernel sysfs and procfs files and monitoring the
  CPU availability will be added in the following CLs.

Test: atest CpuMonitorServiceTest
Bug: 242722241
Change-Id: Ia2565a67a5f639a8d5975d42b0b84d95d08e099f
This commit is contained in:
Lakshman Annadorai
2022-11-29 13:05:40 -08:00
parent f2fd2ca10f
commit 26dc2895f6
5 changed files with 547 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}