Create a helper to parse the free memory.

FreeMemHelper is a helper to parse the free memory based on total memory available from
proc/meminfo, private dirty and private clean information of the cached processes from dumpsys
meminfo.

Bug: 137019980
Test: atest CollectorsHelperTest:com.android.helpers.tests.FreeMemHelperTest
Change-Id: I9ad9a9d656f4f09c9e96b7ca91bf6cdba24adc6d
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
new file mode 100644
index 0000000..2450390
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 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.helpers;
+
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * FreeMemHelper is a helper to parse the free memory based on total memory available from
+ * proc/meminfo, private dirty and private clean information of the cached processes from dumpsys
+ * meminfo.
+ *
+ * Example Usage:
+ * freeMemHelper.startCollecting();
+ * freeMemHelper.getMetrics();
+ * freeMemHelper.stopCollecting();
+ */
+public final class FreeMemHelper implements ICollectorHelper<Long> {
+    private static final String TAG = FreeMemHelper.class.getSimpleName();
+    private static final String SEPARATOR = "\\s+";
+    private static final String CACHED_PROCESSES =
+            "dumpsys meminfo|awk '/Total PSS by category:"
+                    + "/{found=0} {if(found) print} /: Cached/{found=1}'|tr -d ' '";
+    private static final String PROC_MEMINFO = "cat /proc/meminfo";
+    private static final String LINE_SEPARATOR = "\\n";
+    private static final String MEM_AVAILABLE_PATTERN = "^MemAvailable.*";
+    private static final Pattern PID_PATTERN = Pattern.compile("^.*pid(?<processid>[0-9]*).*$");
+    private static final String DUMPSYS_PROCESS = "dumpsys meminfo %s";
+    private static final String MEM_TOTAL = "^\\s+TOTAL\\s+.*";
+    private static final String PROCESS_ID = "processid";
+    public static final String MEM_AVAILABLE_CACHE_PROC_DIRTY = "MemAvailable_CacheProcDirty_kb";
+
+    private UiDevice mUiDevice;
+
+    @Override
+    public boolean startCollecting() {
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        return true;
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Long> getMetrics() {
+        String memInfo;
+        try {
+            memInfo = mUiDevice.executeShellCommand(PROC_MEMINFO);
+        } catch (IOException ioe) {
+            Log.e(TAG, "Failed to read " + PROC_MEMINFO + ".", ioe);
+            return null;
+        }
+
+        Pattern memAvailablePattern = Pattern.compile(MEM_AVAILABLE_PATTERN, Pattern.MULTILINE);
+        Matcher memAvailableMatcher = memAvailablePattern.matcher(memInfo);
+
+        String[] memAvailable = null;
+        if (memAvailableMatcher.find()) {
+            memAvailable = memAvailableMatcher.group(0).split(SEPARATOR);
+        }
+
+        if (memAvailable == null) {
+            Log.e(TAG, "MemAvailable is null.");
+            return null;
+        }
+        long cacheProcDirty = Long.parseLong(memAvailable[1]);
+
+        String cachedProcesses;
+        try {
+            cachedProcesses = mUiDevice.executeShellCommand(CACHED_PROCESSES);
+        } catch (IOException ioe) {
+            Log.e(TAG, "Failed to find cached processes.", ioe);
+            return null;
+        }
+
+        String[] processes = cachedProcesses.split(LINE_SEPARATOR);
+
+        for (String process : processes) {
+            Matcher match;
+            if (((match = matches(PID_PATTERN, process))) != null) {
+                String processId = match.group(PROCESS_ID);
+                String processDumpSysMemInfo = String.format(DUMPSYS_PROCESS, processId);
+                String processInfoStr;
+
+                try {
+                    processInfoStr = mUiDevice.executeShellCommand(processDumpSysMemInfo);
+                } catch (IOException ioe) {
+                    Log.e(TAG, "Failed to get " + processDumpSysMemInfo + ".", ioe);
+                    return null;
+                }
+
+                Pattern memTotalPattern = Pattern.compile(MEM_TOTAL, Pattern.MULTILINE);
+                Matcher memTotalMatcher = memTotalPattern.matcher(processInfoStr);
+
+                String[] processInfo = null;
+                if (memTotalMatcher.find()) {
+                    processInfo = memTotalMatcher.group(0).split(LINE_SEPARATOR);
+                }
+                if (processInfo != null && processInfo.length > 0) {
+                    String[] procDetails = processInfo[0].trim().split(SEPARATOR);
+                    int privateDirty = Integer.parseInt(procDetails[2].trim());
+                    int privateClean = Integer.parseInt(procDetails[3].trim());
+                    cacheProcDirty = cacheProcDirty + privateDirty + privateClean;
+                    Log.d(TAG, "Cached process: " + process + " Private Dirty: "
+                            + privateDirty + " Private Clean: " + privateClean);
+                }
+            }
+        }
+
+        HashMap<String, Long> memAvailableCacheProcDirty = new HashMap<>(1);
+        memAvailableCacheProcDirty.put(MEM_AVAILABLE_CACHE_PROC_DIRTY, cacheProcDirty);
+        return memAvailableCacheProcDirty;
+    }
+
+    /**
+     * Checks whether {@code line} matches the given {@link Pattern}.
+     *
+     * @return The resulting {@link Matcher} obtained by matching the {@code line} against {@code
+     * pattern}, or null if the {@code line} does not match.
+     */
+    private static Matcher matches(Pattern pattern, String line) {
+        Matcher ret = pattern.matcher(line);
+        return ret.matches() ? ret : null;
+    }
+}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
new file mode 100644
index 0000000..a6bac08
--- /dev/null
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 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.helpers.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.FreeMemHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android Unit tests for {@link FreeMemHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.FreeMemHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class FreeMemHelperTest {
+    private FreeMemHelper mFreeMemHelper;
+
+    @Before
+    public void setUp() {
+        mFreeMemHelper = new FreeMemHelper();
+    }
+
+    @Test
+    public void testGetMetrics() {
+        assertTrue(mFreeMemHelper.startCollecting());
+        Map<String, Long> freeMemMetrics = mFreeMemHelper.getMetrics();
+        assertFalse(freeMemMetrics.isEmpty());
+        assertTrue(freeMemMetrics.containsKey(FreeMemHelper.MEM_AVAILABLE_CACHE_PROC_DIRTY));
+        assertTrue(freeMemMetrics.get(FreeMemHelper.MEM_AVAILABLE_CACHE_PROC_DIRTY) > 0);
+    }
+}