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

Commit 784f2494 authored by Lakshman Annadorai's avatar Lakshman Annadorai Committed by Android (Google) Code Review
Browse files

Merge "Add CpuMonitorService to monitor CPU stats."

parents a6108fab 26dc2895
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);
    }
}