Snap for 10075933 from f94feb634598b889847d1ac1cec474562067470f to mainline-mediaprovider-release

Change-Id: Ib1268622988aaba0220460d4a143591b7275d99e
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
index 7587e6c..644cda0 100644
--- a/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
@@ -19,12 +19,17 @@
 import static com.android.helpers.MetricUtility.constructKey;
 
 import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
+
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.InputMismatchException;
@@ -49,7 +54,13 @@
     public static final String ALL_PROCESSES_CMD = "ps -A";
     private static final String SHOWMAP_CMD = "showmap -v %d";
     private static final String CHILD_PROCESSES_CMD = "ps -A --ppid %d";
-
+    @VisibleForTesting public static final String OOM_SCORE_ADJ_CMD = "cat /proc/%d/oom_score_adj";
+    private static final String THREADS_FILE_PATH = "/sdcard/countThreads.sh";
+    @VisibleForTesting public static final String THREADS_CMD = "sh /sdcard/countThreads.sh";
+    private static final String THREADS_EXEC_SCRIPT =
+            "for i in $(ls /proc | grep -E [0-9]+); do echo \"threads_count_$(cat"
+                    + " /proc/$i/cmdline) : $(ls /proc/$i/task | wc -l)\"; done;";
+    public static final String THREADS_PATTERN = "(?<key>^threads_count_.+) : (?<value>[0-9]+)";
     public static final String OUTPUT_METRIC_PATTERN = "showmap_%s_bytes";
     public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
     public static final String PROCESS_COUNT = "process_count";
@@ -57,10 +68,11 @@
     public static final String OUTPUT_CHILD_PROCESS_COUNT_KEY = CHILD_PROCESS_COUNT_PREFIX + "_%s";
     public static final String PROCESS_WITH_CHILD_PROCESS_COUNT =
             "process_with_child_process_count";
-    private static final String CHILD_PROCESS_NAME_REGEX = "(\\S+)$";
     private static final String METRIC_VALUE_SEPARATOR = "_";
     public static final String PARENT_PROCESS_STRING = "parent_process";
     public static final String CHILD_PROCESS_STRING = "child_process";
+    // The reason to skip the process: b/272181398#comment24
+    private static final Set<String> SKIP_PROCESS = new HashSet<>(Arrays.asList("logcat", "sh"));
 
     private String[] mProcessNames = null;
     private String mTestOutputDir = null;
@@ -70,6 +82,7 @@
     private boolean mCollectForAllProcesses = false;
     private UiDevice mUiDevice;
     private boolean mRunGcPrecollection;
+    private boolean mRunCountThreads;
 
     // Map to maintain per-process memory info
     private Map<String, String> mMemoryMap = new HashMap<>();
@@ -79,11 +92,12 @@
     private Map<String, List<Integer>> mMetricNameIndexMap = new HashMap<>();
 
     public void setUp(String testOutputDir, String... processNames) {
-      mProcessNames = processNames;
-      mTestOutputDir = testOutputDir;
-      mDropCacheOption = 0;
-      mRunGcPrecollection = false;
-      mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mProcessNames = processNames;
+        mTestOutputDir = testOutputDir;
+        mDropCacheOption = 0;
+        mRunGcPrecollection = false;
+        mRunCountThreads = false;
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
     }
 
     @Override
@@ -133,11 +147,13 @@
     @Override
     public Map<String, String> getMetrics() {
         try {
+            if (mRunCountThreads) {
+                mMemoryMap.putAll(execCountThreads());
+            }
             // Drop cache if requested
             if (mDropCacheOption > 0) {
                 dropCache(mDropCacheOption);
             }
-
             if (mCollectForAllProcesses) {
                 Log.i(TAG, "Collecting memory metrics for all processes.");
                 mProcessNames = getAllProcessNames();
@@ -148,7 +164,6 @@
                 return mMemoryMap;
             }
             HashSet<Integer> zygoteChildrenPids = getZygoteChildrenPids();
-
             FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
             for (String processName : mProcessNames) {
                 List<Integer> pids = new ArrayList<>();
@@ -156,31 +171,37 @@
                 try {
                     pids = getPids(processName);
                     for (Integer pid : pids) {
-                      // Force Garbage collect to trim transient objects before taking memory
-                      // measurements as memory tests aim to track persistent memory regression
-                      // instead of transient memory which also allows for de-noising and reducing
-                      // likelihood of false alerts.
-                      if (mRunGcPrecollection && zygoteChildrenPids.contains(pid)) {
-                        // Skip native processes from sending GC signal.
-                        android.os.Trace.beginSection("IssueGCForPid: " + pid);
-                        // Perform a synchronous GC which happens when we request meminfo
-                        // This save us the need of setting up timeouts that may or may not
-                        // match with the end time of GC.
-                        mUiDevice.executeShellCommand("dumpsys meminfo -a " + pid);
-                        android.os.Trace.endSection();
-                      }
+                        // Force Garbage collect to trim transient objects before taking memory
+                        // measurements as memory tests aim to track persistent memory regression
+                        // instead of transient memory which also allows for de-noising and reducing
+                        // likelihood of false alerts.
+                        if (mRunGcPrecollection && zygoteChildrenPids.contains(pid)) {
+                            // Skip native processes from sending GC signal.
+                            android.os.Trace.beginSection("IssueGCForPid: " + pid);
+                            // Perform a synchronous GC which happens when we request meminfo
+                            // This save us the need of setting up timeouts that may or may not
+                            // match with the end time of GC.
+                            mUiDevice.executeShellCommand("dumpsys meminfo -a " + pid);
+                            android.os.Trace.endSection();
+                        }
 
-                      android.os.Trace.beginSection("ExecuteShowmap");
-                      String showmapOutput = execShowMap(processName, pid);
-                      android.os.Trace.endSection();
-                      parseAndUpdateMemoryInfo(processName, showmapOutput);
-                      // Store showmap output into file. If there are more than one process
-                      // with same name write the individual showmap associated with pid.
-                      storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
-                      // Parse number of child processes for the given pid and update the
-                      // total number of child process count for the process name that pid
-                      // is associated with.
-                      updateChildProcessesDetails(processName, pid);
+                        // Skip the cached process for showmap and child process count
+                        if (isCachedProcess(processName, pid)) {
+                            Log.i(TAG, String.format("Skip the cached process %s.", processName));
+                            continue;
+                        }
+
+                        android.os.Trace.beginSection("ExecuteShowmap");
+                        String showmapOutput = execShowMap(processName, pid);
+                        android.os.Trace.endSection();
+                        parseAndUpdateMemoryInfo(processName, showmapOutput);
+                        // Store showmap output into file. If there are more than one process
+                        // with same name write the individual showmap associated with pid.
+                        storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
+                        // Parse number of child processes for the given pid and update the
+                        // total number of child process count for the process name that pid
+                        // is associated with.
+                        updateChildProcessesDetails(processName, pid);
                     }
                 } catch (RuntimeException e) {
                     Log.e(TAG, e.getMessage(), e.getCause());
@@ -206,7 +227,6 @@
         } catch (IOException e) {
             Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
         }
-
         return mMemoryMap;
     }
 
@@ -223,8 +243,9 @@
         String childrenCmdOutput = "";
         try {
             // Execute shell does not support shell substitution so it has to be executed twice.
-            childrenCmdOutput = mUiDevice.executeShellCommand(
-                "pgrep -P " + mUiDevice.executeShellCommand("pidof " + processName));
+            childrenCmdOutput =
+                    mUiDevice.executeShellCommand(
+                            "pgrep -P " + mUiDevice.executeShellCommand("pidof " + processName));
         } catch (IOException e) {
             Log.e(TAG, "Exception occurred reading children for process " + processName);
         }
@@ -257,6 +278,15 @@
     }
 
     /**
+     * Sets option for counting the threads for all processes.
+     *
+     * @param shouldCountThreads whether it should run count threads
+     */
+    public void setCountThreadsOption(boolean shouldCountThreads) {
+        mRunCountThreads = shouldCountThreads;
+    }
+
+    /**
      * Set drop cache option.
      *
      * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
@@ -327,6 +357,41 @@
     }
 
     /**
+     * Executes counting threads command for the process.
+     *
+     * @param processName name of the process to run showmap for
+     * @param pid pid of the process to run showmap for
+     * @return the output of showmap command
+     */
+    private Map<String, String> execCountThreads() throws IOException {
+        String countOutput;
+        Map<String, String> countResults = new HashMap<>();
+        try {
+            File execTempFile = new File(THREADS_FILE_PATH);
+            execTempFile.setWritable(true);
+            execTempFile.setExecutable(true, /*ownersOnly*/ false);
+            String countThreadsScriptPath = execTempFile.getAbsolutePath();
+            BufferedWriter writer = new BufferedWriter(new FileWriter(countThreadsScriptPath));
+            writer.write(THREADS_EXEC_SCRIPT);
+            writer.close();
+            countOutput = executeShellCommand(THREADS_CMD);
+            Pattern pattern =
+                    Pattern.compile(THREADS_PATTERN, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
+            String[] lines = countOutput.split("\n");
+            for (String line : lines) {
+                Matcher matcher = pattern.matcher(line);
+                boolean matchFound = matcher.find();
+                if (matchFound) {
+                    countResults.put(matcher.group(1), matcher.group(2));
+                }
+            }
+            return countResults;
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to execute counting threads command", e);
+        }
+    }
+
+    /**
      * Extract memory metrics from showmap command output for the process with {@code processName}
      * name.
      *
@@ -417,6 +482,22 @@
         Log.i(TAG, String.format("Metric Name index map size %s", mMetricNameIndexMap.size()));
     }
 
+    /** Return true if the giving process is a cached process */
+    public boolean isCachedProcess(String processName, long pid) {
+        try {
+            String score = executeShellCommand(String.format(OOM_SCORE_ADJ_CMD, pid));
+            boolean result = Integer.parseInt(score.trim()) >= 900;
+            if (result) {
+                Log.i(TAG, String.format("The process %s with pid %d is cached", processName, pid));
+            }
+            return result;
+        } catch (IOException e) {
+            Log.e(TAG, String.format("Unable to get process oom_score_adj for %s", processName), e);
+            // We don't know the process is cached or not, still collect it
+            return false;
+        }
+    }
+
     /**
      * Retrieves the number of child processes for the given process id and updates the total
      * process count and adds a child process metric for the process name that pid is associated
@@ -427,8 +508,8 @@
      */
     private void updateChildProcessesDetails(String processName, long pid) {
         String childProcessName;
+        String childPID;
         String completeChildProcessMetric;
-        Pattern childProcessPattern = Pattern.compile(CHILD_PROCESS_NAME_REGEX);
         try {
             Log.i(TAG,
                     String.format("Retrieving child processes count for process name: %s with"
@@ -436,38 +517,47 @@
             String childProcessesStr = mUiDevice
                     .executeShellCommand(String.format(CHILD_PROCESSES_CMD, pid));
             Log.i(TAG, String.format("Child processes cmd output: %s", childProcessesStr));
-            String[] childProcessStrSplit = childProcessesStr.split("\\n");
-            // To discard the header line in the command output.
-            int childProcessCount = childProcessStrSplit.length - 1;
-            String childCountMetricKey = String.format(OUTPUT_CHILD_PROCESS_COUNT_KEY, processName);
 
+            int childProcessCount = 0;
+            String[] childProcessStrSplit = childProcessesStr.split("\\n");
+            for (String line : childProcessStrSplit) {
+                // To discard the header line in the command output.
+                if (Objects.equals(line, childProcessStrSplit[0])) continue;
+                String[] childProcessSplit = line.trim().split("\\s+");
+                /**
+                 * final metric will be of following format
+                 * parent_process_<process>_child_process_<process>
+                 * parent_process_zygote64_child_process_system_server
+                 */
+                childPID = childProcessSplit[1];
+                childProcessName = childProcessSplit[8];
+                // Skip the logcat and sh processes in child process count
+                if (SKIP_PROCESS.contains(childProcessName)
+                        || isCachedProcess(childProcessName, Long.parseLong(childPID))) {
+                    Log.i(
+                            TAG,
+                            String.format(
+                                    "Skip the child process %s in the parent process %s.",
+                                    childProcessName, processName));
+                    continue;
+                }
+                childProcessCount++;
+                completeChildProcessMetric =
+                        String.join(
+                                METRIC_VALUE_SEPARATOR,
+                                PARENT_PROCESS_STRING,
+                                processName,
+                                CHILD_PROCESS_STRING,
+                                childProcessName);
+                mMemoryMap.put(completeChildProcessMetric, "1");
+            }
+            String childCountMetricKey = String.format(OUTPUT_CHILD_PROCESS_COUNT_KEY, processName);
             if (childProcessCount > 0) {
                 mMemoryMap.put(childCountMetricKey,
                         Long.toString(
                                 Long.parseLong(mMemoryMap.getOrDefault(childCountMetricKey, "0"))
                                         + childProcessCount));
             }
-            for (String line : childProcessStrSplit) {
-                // To discard the header line in the command output.
-                if (Objects.equals(line, childProcessStrSplit[0])) continue;
-                Matcher childProcessMatcher = childProcessPattern.matcher(line);
-                if (childProcessMatcher.find()) {
-                    /**
-                     * final metric will be of following format
-                     * parent_process_<process>_child_process_<process>
-                     * parent_process_zygote64_child_process_system_server
-                     */
-                    childProcessName = childProcessMatcher.group(1);
-                    completeChildProcessMetric =
-                            String.join(
-                                    METRIC_VALUE_SEPARATOR,
-                                    PARENT_PROCESS_STRING,
-                                    processName,
-                                    CHILD_PROCESS_STRING,
-                                    childProcessName);
-                    mMemoryMap.put(completeChildProcessMetric, "1");
-                }
-            }
         } catch (IOException e) {
             throw new RuntimeException("Unable to run child process command.", e);
         }
@@ -513,4 +603,10 @@
         }
         return allProcessNames.toArray(new String[0]);
     }
+
+    /* Execute a shell command and return its output. */
+    @VisibleForTesting
+    public String executeShellCommand(String command) throws IOException {
+        return mUiDevice.executeShellCommand(command);
+    }
 }
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
index d2e7d5b..9adc31c 100644
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
@@ -13,30 +13,40 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.helpers.tests;
 
 import static com.android.helpers.MetricUtility.constructKey;
-import static org.junit.Assert.fail;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 import androidx.test.runner.AndroidJUnit4;
+
 import com.android.helpers.ShowmapSnapshotHelper;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Android Unit tests for {@link ShowmapSnapshotHelper}.
  *
- * To run:
- * atest CollectorsHelperTest:com.android.helpers.tests.ShowmapSnapshotHelperTest
+ * <p>To run: atest CollectorsHelperAospTest:ShowmapSnapshotHelperTest
  */
 @RunWith(AndroidJUnit4.class)
 public class ShowmapSnapshotHelperTest {
@@ -63,15 +73,25 @@
     private static final String[] TWO_PROCESS_LIST = {
             "com.android.systemui", "system_server"
     };
+    private static final String[] MIXED_PROCESS_LIST = {
+        "com.android.systemui",
+        "system_server",
+        "com.google.android.googlequicksearchbox:search",
+        "android.process.acore"
+    };
+    private static final String[] CACHED_PROCESS_LIST = {
+        "com.google.android.googlequicksearchbox:search", "android.process.acore"
+    };
     private static final String[] NO_PROCESS_LIST = {
             null
     };
 
-    private ShowmapSnapshotHelper mShowmapSnapshotHelper;
+    private @Spy ShowmapSnapshotHelper mShowmapSnapshotHelper;
 
     @Before
     public void setUp() {
         mShowmapSnapshotHelper = new ShowmapSnapshotHelper();
+        MockitoAnnotations.initMocks(this);
     }
 
     /**
@@ -146,6 +166,71 @@
         testProcessList(METRIC_INDEX_STR, TWO_PROCESS_LIST);
     }
 
+    /** Test that cached processes are skipped for showmap metrics. */
+    @Test
+    public void testGetMetrics_MixedProcess() {
+        doReturn(true)
+                .when(mShowmapSnapshotHelper)
+                .isCachedProcess(eq("com.google.android.googlequicksearchbox:search"), anyLong());
+        doReturn(true)
+                .when(mShowmapSnapshotHelper)
+                .isCachedProcess(eq("android.process.acore"), anyLong());
+        doReturn(false)
+                .when(mShowmapSnapshotHelper)
+                .isCachedProcess(eq("com.android.systemui"), anyLong());
+        doReturn(false)
+                .when(mShowmapSnapshotHelper)
+                .isCachedProcess(eq("system_server"), anyLong());
+        mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, MIXED_PROCESS_LIST);
+        mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INDEX_STR);
+        assertTrue(mShowmapSnapshotHelper.startCollecting());
+        Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+        assertFalse(metrics.isEmpty());
+        for (String processName : CACHED_PROCESS_LIST) {
+            assertFalse(
+                    metrics.containsKey(
+                            constructKey(
+                                    String.format(
+                                            ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "rss"),
+                                    processName)));
+            assertFalse(
+                    metrics.containsKey(
+                            constructKey(
+                                    String.format(
+                                            ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "pss"),
+                                    processName)));
+        }
+        assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+    }
+
+    /** Test isCachedProcess() from cached process only. */
+    @Test
+    public void testGetMetrics_CachedProcess() throws IOException {
+        doReturn("1000")
+                .when(mShowmapSnapshotHelper)
+                .executeShellCommand(matches(mShowmapSnapshotHelper.OOM_SCORE_ADJ_CMD));
+        mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, CACHED_PROCESS_LIST);
+        mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INDEX_STR);
+        assertTrue(mShowmapSnapshotHelper.startCollecting());
+        Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+        assertFalse(metrics.isEmpty());
+        for (String processName : CACHED_PROCESS_LIST) {
+            assertFalse(
+                    metrics.containsKey(
+                            constructKey(
+                                    String.format(
+                                            ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "rss"),
+                                    processName)));
+            assertFalse(
+                    metrics.containsKey(
+                            constructKey(
+                                    String.format(
+                                            ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "pss"),
+                                    processName)));
+        }
+        assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+    }
+
     /**
      * Test all process flag return more than 2 processes metrics at least.
      */
@@ -158,7 +243,6 @@
         Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
         assertTrue(metrics.size() > 2);
         assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
-
     }
 
     @Test
@@ -211,7 +295,6 @@
         mShowmapSnapshotHelper.setAllProcesses();
         assertTrue(mShowmapSnapshotHelper.startCollecting());
         Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
-
         assertTrue(metrics.size() != 0);
 
         // process count, process with child process count and path to snapshot file in the output
@@ -226,6 +309,19 @@
         // At least one process (i.e init) will have child process
         assertTrue(parentWithChildProcessSet.size() > 0);
         assertTrue(metrics.containsKey(ShowmapSnapshotHelper.CHILD_PROCESS_COUNT_PREFIX + "_init"));
+        // These assertions are for checking SKIP_PROCESS
+        assertFalse(
+                metrics.containsKey(
+                        ShowmapSnapshotHelper.PARENT_PROCESS_STRING
+                                + "_init_"
+                                + ShowmapSnapshotHelper.CHILD_PROCESS_STRING
+                                + "_logcat"));
+        assertFalse(
+                metrics.containsKey(
+                        ShowmapSnapshotHelper.PARENT_PROCESS_STRING
+                                + "_init_"
+                                + ShowmapSnapshotHelper.CHILD_PROCESS_STRING
+                                + "_sh"));
     }
 
     @Test
@@ -265,6 +361,7 @@
     }
 
     private void testProcessList(String metricIndexStr, String... processNames) {
+        doReturn(false).when(mShowmapSnapshotHelper).isCachedProcess(anyString(), anyLong());
         mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, processNames);
         mShowmapSnapshotHelper.setMetricNameIndex(metricIndexStr);
         assertTrue(mShowmapSnapshotHelper.startCollecting());
@@ -280,4 +377,35 @@
         }
         assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
     }
+
+    /** Test count threads. */
+    @Test
+    public void testCountThreads() throws IOException {
+        String countThreadsSampleOutput =
+                "threads_count_com.google.android.inputmethod.latin : 41\n"
+                        + "threads_count_com.google.android.ims : 35\n"
+                        + "threads_count_ : 1\n"
+                        + "threads_count_com.google.android.connectivitymonitor : 21\n";
+        doReturn(countThreadsSampleOutput)
+                .when(mShowmapSnapshotHelper)
+                .executeShellCommand(matches(mShowmapSnapshotHelper.THREADS_CMD));
+        mShowmapSnapshotHelper.setCountThreadsOption(true);
+        Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+        assertFalse(metrics.isEmpty());
+        assertTrue(metrics.size() == 3);
+        metrics.forEach(
+                (key, value) -> {
+                    assertTrue(key.contains("threads_count_"));
+                });
+    }
+
+    /** Test count threads. */
+    @Test
+    public void testNoCountThreads() throws IOException {
+        mShowmapSnapshotHelper.setCountThreadsOption(false);
+        Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+        assertTrue(metrics.isEmpty());
+        verify(mShowmapSnapshotHelper, never())
+                .executeShellCommand(matches(mShowmapSnapshotHelper.THREADS_CMD));
+    }
 }