CTS to enforce lmkd emitting statsd events for S+ (4.19+) devices

As part of this change, also fix the test flakiness. There are two
sources of flakiness:
- If we allocate too quickly, the OOM reaper might get to the process
first
- If we allocate too slowly from a background service, we might run out
of time and other background restrictions can kick in.

I switched the strategy to spawn a separate victim process that idles,
and have a fg process that allocates mem.

Bug: 177985094
Test: atest UidAtomTests
Change-Id: I8cb123b9488fbc6e88863c2f0e75f1422bcd282e
(cherry picked from commit e91f888636209772644d64ed4d40852c6d7bb466)
diff --git a/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml b/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml
index 20a9763..1ffa00a 100644
--- a/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml
+++ b/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml
@@ -45,6 +45,9 @@
 
         <service android:name=".StatsdCtsBackgroundService"
              android:exported="true"/>
+        <service android:name=".LmkVictimBackgroundService"
+             android:process=":lmk_victim"
+             android:exported="true"/>
         <activity android:name=".StatsdCtsForegroundActivity"
              android:exported="true"/>
         <service android:name=".StatsdCtsForegroundService"
diff --git a/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp b/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp
index 5417245..0a005af 100644
--- a/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp
+++ b/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp
@@ -31,12 +31,11 @@
 
 using namespace std;
 
-size_t s = 4 * (1 << 20); // 4 MB
+size_t s = 8 * (1 << 20); // 8 MB
 void *gptr;
-extern "C"
-JNIEXPORT void JNICALL
-Java_com_android_server_cts_device_statsdatom_StatsdCtsBackgroundService_cmain(JNIEnv* , jobject /* this */)
-{
+extern "C" JNIEXPORT void JNICALL
+Java_com_android_server_cts_device_statsdatom_StatsdCtsForegroundActivity_cmain(
+        JNIEnv *, jobject /* this */) {
     long long allocCount = 0;
     while (1) {
         char *ptr = (char *)malloc(s);
@@ -44,10 +43,14 @@
         for (int i = 0; i < s; i += 4096) {
             *((long long *)&ptr[i]) = allocCount + i;
         }
+        allocCount += s;
         std::stringstream ss;
         ss << "total alloc: " << allocCount / (1 << 20);
         LOG(ss.str().c_str());
         gptr = ptr;
-        allocCount += s;
+
+        // If we are too aggressive allocating, we will end up triggering the
+        // OOM reaper instead of LMKd.
+        usleep(1000);
     }
-}
\ No newline at end of file
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java
new file mode 100644
index 0000000..0e97dbe
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 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.cts.device.statsdatom;
+
+public class LmkVictimBackgroundService extends StatsdCtsBackgroundService {}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java
index 8cc2cb0..d7ac653 100644
--- a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java
@@ -27,14 +27,9 @@
     public static final String KEY_ACTION = "action";
     public static final String ACTION_BACKGROUND_SLEEP = "action.background_sleep";
     public static final String ACTION_END_IMMEDIATELY = "action.end_immediately";
-    public static final String ACTION_LMK = "action.lmk";
 
     public static final int SLEEP_OF_ACTION_BACKGROUND_SLEEP = 2_000;
 
-    static {
-        System.loadLibrary("lmkhelper_statsdatom");
-    }
-
     public StatsdCtsBackgroundService() {
         super(StatsdCtsBackgroundService.class.getName());
     }
@@ -50,16 +45,8 @@
                 break;
             case ACTION_END_IMMEDIATELY:
                 break;
-            case ACTION_LMK:
-                new Thread(this::cmain).start();
-                break;
             default:
                 Log.e(TAG, "Intent had invalid action");
         }
     }
-
-    /**
-     *  Keep allocating memory until the process is killed by LMKD.
-     **/
-    public native void cmain();
-}
\ No newline at end of file
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java
index 26ec79e..58db263 100644
--- a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java
@@ -50,6 +50,7 @@
     public static final String ACTION_SHOW_NOTIFICATION = "action.show_notification";
     public static final String ACTION_CREATE_CHANNEL_GROUP = "action.create_channel_group";
     public static final String ACTION_POLL_NETWORK_STATS = "action.poll_network_stats";
+    public static final String ACTION_LMK = "action.lmk";
 
     public static final int SLEEP_OF_ACTION_SLEEP_WHILE_TOP = 2_000;
     public static final int SLEEP_OF_ACTION_SHOW_APPLICATION_OVERLAY = 2_000;
@@ -57,6 +58,7 @@
 
     static {
         System.loadLibrary("crashhelper");
+        System.loadLibrary("lmkhelper_statsdatom");
     }
 
     @Override
@@ -100,6 +102,9 @@
             case ACTION_POLL_NETWORK_STATS:
                 doPollNetworkStats();
                 break;
+            case ACTION_LMK:
+                new Thread(this::cmain).start();
+                break;
             default:
                 Log.e(TAG, "Intent had invalid action " + action);
                 finish();
@@ -213,4 +218,9 @@
     }
 
     private native void segfault();
+
+    /**
+     *  Keep allocating memory until the process is killed by LMKD.
+     **/
+    public native void cmain();
 }
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java
index f2f4e1b..e937823 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.util.Pair;
 
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.MessageLite;
@@ -53,9 +54,6 @@
     public static final String STATSD_ATOM_TEST_APK = "CtsStatsdAtomApp.apk";
     public static final String STATSD_ATOM_TEST_PKG = "com.android.server.cts.device.statsdatom";
 
-    private static final String DEVICE_SIDE_BG_SERVICE_COMPONENT =
-            "com.android.server.cts.device.statsdatom/.StatsdCtsBackgroundService";
-
     private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
 
     private static final String KEY_ACTION = "action";
@@ -379,10 +377,19 @@
      */
     public static void executeBackgroundService(ITestDevice device, String actionValue)
             throws Exception {
+        executeServiceAction(device, "StatsdCtsBackgroundService", actionValue);
+    }
+
+    /**
+     * Runs the specified statsd package service to perform the given action.
+     * @param actionValue the action code constants indicating the desired action to perform.
+     */
+    public static void executeServiceAction(ITestDevice device, String service, String actionValue)
+            throws Exception {
         allowBackgroundServices(device);
         device.executeShellCommand(String.format(
-                "am startservice -n '%s' -e %s %s",
-                DEVICE_SIDE_BG_SERVICE_COMPONENT,
+                "am startservice -n '%s/.%s' -e %s %s",
+                STATSD_ATOM_TEST_PKG, service,
                 KEY_ACTION, actionValue));
     }
 
@@ -394,5 +401,25 @@
                 "cmd deviceidle tempwhitelist %s", STATSD_ATOM_TEST_PKG));
     }
 
+    /**
+     * Returns the kernel major version as a pair of ints.
+     */
+    public static Pair<Integer, Integer> getKernelVersion(ITestDevice device)
+            throws Exception {
+        String[] version = device.executeShellCommand("uname -r").split("\\.");
+        if (version.length < 2) {
+              throw new RuntimeException("Could not parse kernel version");
+        }
+        return Pair.create(Integer.parseInt(version[0]), Integer.parseInt(version[1]));
+    }
+
+    /** Returns if the device kernel version >= input kernel version. */
+    public static boolean isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version)
+            throws Exception {
+        Pair<Integer, Integer> kernelVersion = getKernelVersion(device);
+        return kernelVersion.first > version.first
+                || (kernelVersion.first == version.first && kernelVersion.second >= version.second);
+    }
+
     private DeviceUtils() {}
 }
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
index 2df9603..f106b84 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
@@ -26,6 +26,7 @@
 import android.os.WakeLockLevelEnum;
 import android.server.ErrorSource;
 
+import com.android.compatibility.common.util.PropertyUtil;
 import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
 import com.android.os.AtomsProto;
@@ -58,6 +59,7 @@
 import com.android.tradefed.log.LogUtil;
 import com.android.tradefed.testtype.DeviceTestCase;
 import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.util.Pair;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -132,29 +134,54 @@
         assertThat(atom.getState()).isEqualTo(AppBreadcrumbReported.State.START);
     }
 
+    private boolean shouldTestLmkdStats() throws Exception {
+        boolean hasKernel = DeviceUtils.isKernelGreaterEqual(getDevice(), Pair.create(4, 19));
+        boolean hasFirstApiLevel = PropertyUtil.getFirstApiLevel(getDevice()) > 30;
+        return (hasKernel && hasFirstApiLevel)
+                || "true".equals(DeviceUtils.getProperty(getDevice(), "ro.lmk.log_stats"));
+    }
+
     public void testLmkKillOccurred() throws Exception {
-        if (!"true".equals(DeviceUtils.getProperty(getDevice(), "ro.lmk.log_stats"))) {
+        if (!shouldTestLmkdStats()) {
+            LogUtil.CLog.d("Skipping lmkd stats test.");
             return;
         }
 
-        final int atomTag = Atom.LMK_KILL_OCCURRED_FIELD_NUMBER;
-        final String actionLmk = "action.lmk";
         ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
-                atomTag,  /*uidInAttributionChain=*/false);
-
-        DeviceUtils.executeBackgroundService(getDevice(), actionLmk);
-        Thread.sleep(15_000);
-
-        // Sorted list of events in order in which they occurred.
-        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+                Atom.LMK_KILL_OCCURRED_FIELD_NUMBER,  /*uidInAttributionChain=*/false);
         int appUid = DeviceUtils.getStatsdTestAppUid(getDevice());
 
-        assertThat(data).hasSize(1);
-        assertThat(data.get(0).getAtom().hasLmkKillOccurred()).isTrue();
-        LmkKillOccurred atom = data.get(0).getAtom().getLmkKillOccurred();
-        assertThat(atom.getUid()).isEqualTo(appUid);
-        assertThat(atom.getProcessName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
-        assertThat(atom.getOomAdjScore()).isAtLeast(500);
+        // Start the victim process (service running in process :lmk_victim)
+        // We rely on a victim process (instead of expecting the allocating process to die)
+        // because it can be flaky and dependent on lmkd configuration
+        // (e.g. the OOM reaper can get to it first, depending on the allocation timings)
+        DeviceUtils.executeServiceAction(getDevice(), "LmkVictimBackgroundService",
+                "action.end_immediately");
+        // Start fg activity and allocate
+        try (AutoCloseable a = DeviceUtils.withActivity(
+                getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.lmk")) {
+            // Sorted list of events in order in which they occurred.
+            List<EventMetricData> data = null;
+            for (int i = 0; i < 60; ++i) {
+                Thread.sleep(1_000);
+                data = ReportUtils.getEventMetricDataList(getDevice());
+                if (!data.isEmpty()) {
+                  break;
+                }
+            }
+
+            assertThat(data).isNotEmpty();
+            // Even though both processes might have died, the non-fg one (victim)
+            // must have been first.
+            assertThat(data.get(0).getAtom().hasLmkKillOccurred()).isTrue();
+            LmkKillOccurred atom = data.get(0).getAtom().getLmkKillOccurred();
+            assertThat(atom.getUid()).isEqualTo(appUid);
+            assertThat(atom.getProcessName())
+                    .isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG + ":lmk_victim");
+            assertThat(atom.getOomAdjScore()).isAtLeast(500);
+            assertThat(atom.getRssInBytes() + atom.getSwapInBytes()).isGreaterThan(0);
+      }
     }
 
     public void testAppCrashOccurred() throws Exception {