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