Rule to diagnose test timeout

Bug: 202567877
Test: presubmit
Change-Id: I871bdcd638a5217e4d892edafd0ea0850de9da81
diff --git a/libraries/health/rules/src/android/platform/test/rule/SamplerRule.java b/libraries/health/rules/src/android/platform/test/rule/SamplerRule.java
new file mode 100644
index 0000000..61d836f
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/SamplerRule.java
@@ -0,0 +1,117 @@
+/*
+ * 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 android.platform.test.rule;
+
+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. If the process was terminated, the test leaves an
+ * artifact with stack traces of all threads, every second. This will help understanding where we
+ * stuck.
+ */
+public class SamplerRule extends TestWatcher {
+
+    private static boolean sEnabled;
+
+    public static void enable(boolean enabled) {
+        // The rule need to be explicitly enabled to avoid slowing down performance tests.
+        sEnabled = enabled;
+    }
+
+    public static Thread startThread(Description description) {
+        Thread thread =
+                new Thread() {
+                    @Override
+                    public void run() {
+                        // Write all-threads stack stace every second 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();
+
+                        try (final OutputStreamWriter outputStreamWriter =
+                                new OutputStreamWriter(
+                                        new BufferedOutputStream(new FileOutputStream(file)))) {
+                            writeSampleEverySecond(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.
+                            file.delete();
+                        }
+                    }
+
+                    private File getFile() {
+                        final String strDate = new SimpleDateFormat("HH:mm:ss").format(new Date());
+
+                        final String descStr = description.getTestClass().getSimpleName();
+                        return ArtifactSaver.artifactFile(
+                                "ThreadStackSamples-" + strDate + "-" + descStr + ".txt");
+                    }
+
+                    private void writeSampleEverySecond(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(1000);
+                        }
+                    }
+                };
+
+        thread.start();
+        return thread;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        if (!sEnabled) return base;
+
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                final Thread traceThread = startThread(description);
+                try {
+                    SamplerRule.super.apply(base, description).evaluate();
+                } finally {
+                    traceThread.interrupt();
+                    traceThread.join();
+                }
+            }
+        };
+    }
+}
diff --git a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Functional.java b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Functional.java
index cd7df4e..de73e30 100644
--- a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Functional.java
+++ b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Functional.java
@@ -16,6 +16,7 @@
 package android.platform.test.microbenchmark;
 
 import android.platform.test.rule.ArtifactSaver;
+import android.platform.test.rule.SamplerRule;
 
 import org.junit.internal.AssumptionViolatedException;
 import org.junit.internal.runners.statements.RunAfters;
@@ -117,8 +118,14 @@
     protected Statement classBlock(RunNotifier notifier) {
         // Error artifact saver for exceptions thrown outside class befores and afters, i.e. in
         // class rules.
-        final Statement statement =
-                artifactSaver(super.classBlock(notifier), getChildren().stream());
+        final Statement parentStatement;
+        try {
+            SamplerRule.enable(true);
+            parentStatement = super.classBlock(notifier);
+        } finally {
+            SamplerRule.enable(false);
+        }
+        final Statement statement = artifactSaver(parentStatement, getChildren().stream());
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {