Merge "Add CPU and memory load monitoring to SystemMonitor." into sc-v2-dev
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index 04a5808..87fd0b7 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -103,3 +103,6 @@
 allow carservice_app gpu_device:dir r_dir_perms;
 allow carservice_app gpu_service:service_manager find;
 binder_call(carservice_app, gpuservice)
+
+# Allow reading and writing /proc/loadavg/
+allow carservice_app proc_loadavg:file { open read getattr };
diff --git a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
index 65bc45b..e9b1cb4 100644
--- a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
+++ b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
@@ -16,13 +16,49 @@
 
 package com.android.car.telemetry.systemmonitor;
 
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Slog;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
 /**
  * SystemMonitor monitors system states and report to listeners when there are
  * important changes.
  */
 public class SystemMonitor {
 
-    private SystemMonitorCallback mCallback;
+    private static final int NUM_LOADAVG_VALS = 3;
+    private static final float HI_CPU_LOAD_PER_CORE_BASE_LEVEL = 1.0f;
+    private static final float MED_CPU_LOAD_PER_CORE_BASE_LEVEL = 0.5f;
+    private static final float HI_MEM_LOAD_BASE_LEVEL = 0.95f;
+    private static final float MED_MEM_LOAD_BASE_LEVEL = 0.80f;
+    private static final String LOADAVG_PATH = "/proc/loadavg";
+
+    private static final int POLL_INTERVAL_MILLIS = 60000;
+
+    private final Handler mWorkerHandler;
+
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final ActivityManager mActivityManager;
+    private final String mLoadavgPath;
+
+    @GuardedBy("mLock")
+    @Nullable private SystemMonitorCallback mCallback;
+    @GuardedBy("mLock")
+    private boolean mSystemMonitorRunning = false;
+
 
     /**
      * Interface for receiving notifications about system monitor changes.
@@ -37,11 +73,180 @@
     }
 
     /**
-     * Sets the callback to notify of system state changes.
+     * Creates a SystemMonitor instance set with default loadavg path.
+     *
+     * @param context the context this is running in.
+     * @param workerHandler a handler for running monitoring jobs.
+     * @return SystemMonitor instance.
+     */
+    public static SystemMonitor create(Context context, Handler workerHandler) {
+        return new SystemMonitor(context, workerHandler, LOADAVG_PATH);
+    }
+
+    @VisibleForTesting
+    SystemMonitor(Context context, Handler workerHandler, String loadavgPath) {
+        mContext = context;
+        mWorkerHandler = workerHandler;
+        mActivityManager = (ActivityManager)
+                mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        mLoadavgPath = loadavgPath;
+    }
+
+    /**
+     * Sets the {@link SystemMonitorCallback} to notify of system state changes.
      *
      * @param callback the callback to nofify state changes on.
      */
     public void setSystemMonitorCallback(SystemMonitorCallback callback) {
-        mCallback = callback;
+        synchronized (mLock) {
+            mCallback = callback;
+            if (!mWorkerHandler.hasCallbacks(this::getSystemLoadRepeated)) {
+                startSystemLoadMonitoring();
+            }
+        }
+    }
+
+    /**
+     * Unsets the {@link SystemMonitorCallback}.
+     */
+    public void unsetSystemMonitorCallback() {
+        synchronized (mLock) {
+            stopSystemLoadMonitoringLocked();
+            mCallback = null;
+        }
+    }
+
+    /**
+     * Gets the loadavg data from /proc/loadavg, getting the first 3 averages,
+     * which are 1-min, 5-min and 15-min moving averages respectively.
+     *
+     * Requires Selinux permissions 'open', 'read, 'getattr' to proc_loadavg,
+     * which is set in Car/car_product/sepolicy/private/carservice_app.te.
+     *
+     * @return the {@link CpuLoadavg}.
+     */
+    @VisibleForTesting
+    @Nullable
+    CpuLoadavg getCpuLoad() {
+        try (BufferedReader reader = new BufferedReader(new FileReader(mLoadavgPath))) {
+            String line = reader.readLine();
+            String[] vals = line.split("\\s+", NUM_LOADAVG_VALS + 1);
+            if (vals.length < NUM_LOADAVG_VALS) {
+                Slog.w(CarLog.TAG_TELEMETRY, "Loadavg wrong format");
+                return null;
+            }
+            CpuLoadavg cpuLoadavg = new CpuLoadavg();
+            cpuLoadavg.mOneMinuteVal = Float.parseFloat(vals[0]);
+            cpuLoadavg.mFiveMinutesVal = Float.parseFloat(vals[1]);
+            cpuLoadavg.mFifteenMinutesVal = Float.parseFloat(vals[2]);
+            return cpuLoadavg;
+        } catch (IOException | NumberFormatException ex) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to read loadavg file.", ex);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the {@link ActivityManager.MemoryInfo} for system memory pressure.
+     *
+     * Of the MemoryInfo fields, we will only be using availMem and totalMem,
+     * since lowMemory and threshold are likely deprecated.
+     *
+     * @return {@link MemoryInfo} for the system.
+     */
+    private MemoryInfo getMemoryLoad() {
+        MemoryInfo mi = new ActivityManager.MemoryInfo();
+        mActivityManager.getMemoryInfo(mi);
+        return mi;
+    }
+
+    /**
+     * Sets the CPU usage level for a {@link SystemMonitorEvent}.
+     *
+     * @param event the {@link SystemMonitorEvent}.
+     * @param cpuLoadPerCore the CPU load average per CPU core.
+     */
+    @VisibleForTesting
+    void setEventCpuUsageLevel(SystemMonitorEvent event, double cpuLoadPerCore) {
+        if (cpuLoadPerCore > HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+        } else if (cpuLoadPerCore > MED_CPU_LOAD_PER_CORE_BASE_LEVEL
+                   && cpuLoadPerCore <= HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+        } else {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        }
+    }
+
+    /**
+     * Sets the memory usage level for a {@link SystemMonitorEvent}.
+     *
+     * @param event the {@link SystemMonitorEvent}.
+     * @param memLoadRatio ratio of used memory to total memory.
+     */
+    @VisibleForTesting
+    void setEventMemUsageLevel(SystemMonitorEvent event, double memLoadRatio) {
+        if (memLoadRatio > HI_MEM_LOAD_BASE_LEVEL) {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+        } else if (memLoadRatio > MED_MEM_LOAD_BASE_LEVEL
+                   && memLoadRatio <= HI_MEM_LOAD_BASE_LEVEL) {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+        } else {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        }
+    }
+
+    /**
+     * The Runnable to repeatedly getting system load data with some interval.
+     */
+    private void getSystemLoadRepeated() {
+        synchronized (mLock) {
+            try {
+                CpuLoadavg cpuLoadAvg = getCpuLoad();
+                if (cpuLoadAvg == null) {
+                    return;
+                }
+                int numProcessors = Runtime.getRuntime().availableProcessors();
+
+                MemoryInfo memInfo = getMemoryLoad();
+
+                SystemMonitorEvent event = new SystemMonitorEvent();
+                setEventCpuUsageLevel(event, cpuLoadAvg.mOneMinuteVal / numProcessors);
+                setEventMemUsageLevel(event, 1 - memInfo.availMem / memInfo.totalMem);
+
+                mCallback.onSystemMonitorEvent(event);
+            } finally {
+                if (mSystemMonitorRunning) {
+                    mWorkerHandler.postDelayed(this::getSystemLoadRepeated, POLL_INTERVAL_MILLIS);
+                }
+            }
+        }
+    }
+
+    /**
+     * Starts system load monitoring.
+     */
+    private void startSystemLoadMonitoring() {
+        synchronized (mLock) {
+            mWorkerHandler.post(this::getSystemLoadRepeated);
+            mSystemMonitorRunning = true;
+        }
+    }
+
+    /**
+     * Stops system load monitoring.
+     */
+    @GuardedBy("mLock")
+    private void stopSystemLoadMonitoringLocked() {
+        synchronized (mLock) {
+            mWorkerHandler.removeCallbacks(this::getSystemLoadRepeated);
+            mSystemMonitorRunning = false;
+        }
+    }
+
+    static final class CpuLoadavg {
+        float mOneMinuteVal;
+        float mFiveMinutesVal;
+        float mFifteenMinutesVal;
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java
new file mode 100644
index 0000000..a16ea1e
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 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.car.telemetry.systemmonitor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SystemMonitorUnitTest {
+
+    @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private static final String TEST_LOADAVG = "1.2 3.4 2.2 123/1452 21348";
+    private static final String TEST_LOADAVG_BAD_FORMAT = "1.2 3.4";
+    private static final String TEST_LOADAVG_NOT_FLOAT = "1.2 abc 2.1 12/231 2";
+    private static final long TEST_AVAILMEM = 3_000_000_000L;
+    private static final long TEST_TOTALMEM = 8_000_000_000L;
+
+    @Mock private Context mMockContext;
+    @Mock private Handler mMockHandler; // it promptly executes the runnable in the same thread
+    @Mock private ActivityManager mMockActivityManager;
+    @Mock private SystemMonitor.SystemMonitorCallback mMockCallback;
+
+    @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
+    @Captor ArgumentCaptor<SystemMonitorEvent> mEventCaptor;
+
+    @Before
+    public void setup() {
+        when(mMockContext.getSystemService(anyString())).thenReturn(mMockActivityManager);
+        when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
+            ((Runnable) i.getArguments()[0]).run();
+            return true;
+        });
+    }
+
+    @Test
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForHighUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 1.5);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForMedUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.6);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+    }
+
+    @Test
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForLowUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.5);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForHighUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.98);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForMedUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.85);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForLowUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.80);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+    }
+
+    @Test
+    public void testAfterSetCallback_callbackCalled() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+        doAnswer(i -> {
+            Object[] args = i.getArguments();
+            MemoryInfo mi = (MemoryInfo) args[0];
+            mi.availMem = TEST_AVAILMEM;
+            mi.totalMem = TEST_TOTALMEM;
+            return null;
+        }).when(mMockActivityManager).getMemoryInfo(any(MemoryInfo.class));
+
+        systemMonitor.setSystemMonitorCallback(mMockCallback);
+
+        verify(mMockCallback, atLeastOnce()).onSystemMonitorEvent(mEventCaptor.capture());
+        SystemMonitorEvent event = mEventCaptor.getValue();
+        assertThat(event.getCpuUsageLevel()).isAnyOf(
+                SystemMonitorEvent.USAGE_LEVEL_LOW,
+                SystemMonitorEvent.USAGE_LEVEL_MED,
+                SystemMonitorEvent.USAGE_LEVEL_HI);
+        assertThat(event.getMemoryUsageLevel()).isAnyOf(
+                SystemMonitorEvent.USAGE_LEVEL_LOW,
+                SystemMonitorEvent.USAGE_LEVEL_MED,
+                SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testWhenLoadavgIsBadFormat_getCpuLoadReturnsNull() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_BAD_FORMAT));
+
+        assertThat(systemMonitor.getCpuLoad()).isNull();
+    }
+
+    @Test
+    public void testWhenLoadavgIsNotFloatParsable_getCpuLoadReturnsNull() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_NOT_FLOAT));
+
+        assertThat(systemMonitor.getCpuLoad()).isNull();
+    }
+
+    /**
+     * Creates and writes to the temp file, returns its path.
+     */
+    private String writeTempFile(String content) throws IOException {
+        File tempFile = temporaryFolder.newFile();
+        try (FileWriter fw = new FileWriter(tempFile)) {
+            fw.write(content);
+        }
+        return tempFile.getAbsolutePath();
+    }
+}