Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26dc2895 authored by Lakshman Annadorai's avatar Lakshman Annadorai
Browse files

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
parent f2fd2ca1
Loading
Loading
Loading
Loading
+63 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+109 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+81 −0
Original line number Diff line number Diff line
/*
 * 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);
}
+173 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+121 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}