Sampling too long Launcher tests

A test that takes > 3 min will generate an artifact file containing stacks of all threads of the test process taken every 3 sec. This artifact will be also generated if the test process is killed, for example, by timeout.

This artifact should help EngProd's effort to speed up presubmits.

Bug: 225186335
Test: local runs
Change-Id: I721779bfbe5bc6289315998ed2660f5f46165611
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index ca6712f..f7600ff 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
+import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.quickstep.views.RecentsView;
 
@@ -111,7 +112,8 @@
         }
 
         mOrderSensitiveRules = RuleChain
-                .outerRule(new NavigationModeSwitchRule(mLauncher))
+                .outerRule(new SamplerRule())
+                .around(new NavigationModeSwitchRule(mLauncher))
                 .around(new FailureWatcher(mDevice, mLauncher));
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
diff --git a/tests/Android.bp b/tests/Android.bp
index 7542d04..54cded0 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -37,6 +37,7 @@
       "src/com/android/launcher3/util/WidgetUtils.java",
       "src/com/android/launcher3/util/rule/FailureWatcher.java",
       "src/com/android/launcher3/util/rule/LauncherActivityRule.java",
+      "src/com/android/launcher3/util/rule/SamplerRule.java",
       "src/com/android/launcher3/util/rule/ScreenRecordRule.java",
       "src/com/android/launcher3/util/rule/ShellCommandRule.java",
       "src/com/android/launcher3/util/rule/SimpleActivityRule.java",
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 136f115..7080c85 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -68,6 +68,7 @@
 import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.LauncherActivityRule;
+import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
@@ -227,7 +228,8 @@
 
     @Rule
     public TestRule mOrderSensitiveRules = RuleChain
-            .outerRule(new TestStabilityRule())
+            .outerRule(new SamplerRule())
+            .around(new TestStabilityRule())
             .around(mActivityMonitor)
             .around(getRulesInsideActivityMonitor());
 
diff --git a/tests/src/com/android/launcher3/util/rule/SamplerRule.java b/tests/src/com/android/launcher3/util/rule/SamplerRule.java
new file mode 100644
index 0000000..6125f2a
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/SamplerRule.java
@@ -0,0 +1,129 @@
+/*
+ * 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.launcher3.util.rule;
+
+import android.os.SystemClock;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * A rule that generates a file that helps diagnosing cases when the test process was terminated
+ * because the test execution took too long, and tests that ran for too long even without being
+ * terminated. If the process was terminated or the test was long, the test leaves an artifact with
+ * stack traces of all threads, every SAMPLE_INTERVAL_MS. This will help understanding where we
+ * stuck.
+ */
+public class SamplerRule implements TestRule {
+    private static final int TOO_LONG_TEST_MS = 180000;
+    private static final int SAMPLE_INTERVAL_MS = 3000;
+
+    public static Thread startThread(Description description) {
+        Thread thread =
+                new Thread() {
+                    @Override
+                    public void run() {
+                        // Write all-threads stack stace every SAMPLE_INTERVAL_MS while the test
+                        // is running.
+                        // After the test finishes, delete that file. If the test process is
+                        // terminated due to timeout, the trace file won't be deleted.
+                        final File file = getFile();
+
+                        final long startTime = SystemClock.elapsedRealtime();
+                        try (OutputStreamWriter outputStreamWriter =
+                                     new OutputStreamWriter(
+                                             new BufferedOutputStream(
+                                                     new FileOutputStream(file)))) {
+                            writeSamples(outputStreamWriter);
+                        } catch (IOException | InterruptedException e) {
+                            // Simply suppressing the exceptions, nothing to do here.
+                        } finally {
+                            // If the process is not killed, then there was no test timeout, and
+                            // we are not interested in the trace file, unless the test ran too
+                            // long.
+                            if (SystemClock.elapsedRealtime() - startTime < TOO_LONG_TEST_MS) {
+                                file.delete();
+                            }
+                        }
+                    }
+
+                    private File getFile() {
+                        final String strDate = new SimpleDateFormat("HH:mm:ss").format(new Date());
+
+                        final String descStr = description.getTestClass().getSimpleName() + "."
+                                + description.getMethodName();
+                        return artifactFile(
+                                "ThreadStackSamples-" + strDate + "-" + descStr + ".txt");
+                    }
+
+                    private void writeSamples(OutputStreamWriter writer)
+                            throws IOException, InterruptedException {
+                        int count = 0;
+                        while (true) {
+                            writer.write(
+                                    "#"
+                                            + (count++)
+                                            + " =============================================\r\n");
+                            for (StackTraceElement[] stack : getAllStackTraces().values()) {
+                                writer.write("---------------------\r\n");
+                                for (StackTraceElement frame : stack) {
+                                    writer.write(frame.toString() + "\r\n");
+                                }
+                            }
+                            writer.flush();
+
+                            sleep(SAMPLE_INTERVAL_MS);
+                        }
+                    }
+                };
+
+        thread.start();
+        return thread;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                final Thread traceThread = startThread(description);
+                try {
+                    base.evaluate();
+                } finally {
+                    traceThread.interrupt();
+                    traceThread.join();
+                }
+            }
+        };
+    }
+
+    private static File artifactFile(String fileName) {
+        return new File(
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir(),
+                fileName);
+    }
+}