1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.helpers;
18 
19 import static com.android.helpers.MetricUtility.constructKey;
20 
21 import android.util.Log;
22 
23 import androidx.annotation.VisibleForTesting;
24 import androidx.test.InstrumentationRegistry;
25 import androidx.test.uiautomator.UiDevice;
26 
27 import java.io.BufferedWriter;
28 import java.io.File;
29 import java.io.FileWriter;
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.InputMismatchException;
36 import java.util.LinkedHashSet;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Set;
41 import java.util.UUID;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import java.util.stream.Collectors;
45 
46 /**
47  * Helper to collect memory information for a list of processes from showmap.
48  */
49 public class ShowmapSnapshotHelper implements ICollectorHelper<String> {
50     private static final String TAG = ShowmapSnapshotHelper.class.getSimpleName();
51     private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
52     private static final String PIDOF_CMD = "pidof %s";
53     public static final String ALL_PROCESSES_CMD = "ps -A";
54     private static final String SHOWMAP_CMD = "showmap -v %d";
55     private static final String CHILD_PROCESSES_CMD = "ps -A --ppid %d";
56     private static final String ACTIVITY_LRU_CMD = "dumpsys activity lru";
57     private static final String DUMP_SYS_THREADS_CMD = "ps -ATw -o pid,tid,ppid,name,cmd,cmdline";
58     @VisibleForTesting public static final String OOM_SCORE_ADJ_CMD = "cat /proc/%d/oom_score_adj";
59     @VisibleForTesting public static final String COUNT_THREADS_CMD = "sh /sdcard/countThreads.sh";
60     private static final int PROCESS_OOM_SCORE_IMPERCEPTIBLE = 200;
61     private static final int PROCESS_OOM_SCORE_CACHED = 899;
62     private static final String COUNT_THREADS_FILE_PATH = "/sdcard/countThreads.sh";
63     private static final String COUNT_THREADS_EXEC_SCRIPT =
64             "for i in $(ls /proc | grep -E [0-9]+); do echo \"threads_count_$(cat"
65                     + " /proc/$i/cmdline) : $(ls /proc/$i/task | wc -l)\"; done;";
66     public static final String THREADS_PATTERN = "(?<key>^threads_count_.+) : (?<value>[0-9]+)";
67     public static final String OUTPUT_METRIC_PATTERN = "showmap_%s_bytes";
68     public static final String OUTPUT_IMPERCEPTIBLE_METRIC_PATTERN =
69             "showmap_%s_bytes_imperceptible";
70     public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
71     public static final String SYSTEM_THREADS_FILE_PATH_KEY = "system_threads_output_file";
72     public static final String PROCESS_COUNT = "process_count";
73     public static final String CHILD_PROCESS_COUNT_PREFIX = "child_processes_count";
74     public static final String OUTPUT_CHILD_PROCESS_COUNT_KEY = CHILD_PROCESS_COUNT_PREFIX + "_%s";
75     public static final String PROCESS_WITH_CHILD_PROCESS_COUNT =
76             "process_with_child_process_count";
77     private static final String METRIC_VALUE_SEPARATOR = "_";
78     public static final String PARENT_PROCESS_STRING = "parent_process";
79     public static final String CHILD_PROCESS_STRING = "child_process";
80     // The reason to skip the process: b/272181398#comment24
81     private static final Set<String> SKIP_PROCESS = new HashSet<>(Arrays.asList("logcat", "sh"));
82 
83     private String[] mProcessNames = null;
84     private String mTestOutputDir = null;
85     private String mTestOutputFile = null;
86     private String mSysThreadsDebugFile = null;
87     private int mDropCacheOption;
88     private boolean mCollectForAllProcesses = false;
89     private UiDevice mUiDevice;
90     private boolean mRunGcPrecollection;
91     private boolean mRunCountThreads;
92 
93     // Map to maintain per-process memory info
94     private Map<String, String> mMemoryMap = new HashMap<>();
95 
96     // Maintain metric name and the index it corresponds to in the showmap output
97     // summary
98     private Map<String, List<Integer>> mMetricNameIndexMap = new HashMap<>();
99 
setUp(String testOutputDir, String... processNames)100     public void setUp(String testOutputDir, String... processNames) {
101         mProcessNames = processNames;
102         mTestOutputDir = testOutputDir;
103         mDropCacheOption = 0;
104         mRunGcPrecollection = false;
105         mRunCountThreads = false;
106         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
107     }
108 
109     @Override
startCollecting()110     public boolean startCollecting() {
111         if (mTestOutputDir == null) {
112             Log.e(TAG, String.format("Invalid test setup"));
113             return false;
114         }
115         mMemoryMap.clear();
116 
117         File directory = new File(mTestOutputDir);
118         String filePath = String.format("%s/showmap_snapshot%d.txt", mTestOutputDir,
119                 UUID.randomUUID().hashCode());
120         File file = new File(filePath);
121 
122         // Make sure directory exists and file does not
123         if (directory.exists()) {
124             if (file.exists() && !file.delete()) {
125                 Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
126                 return false;
127             }
128         } else {
129             if (!directory.mkdirs()) {
130                 Log.e(TAG, String.format("Failed to create result output directory %s",
131                         mTestOutputDir));
132                 return false;
133             }
134         }
135 
136         // Create an empty file to fail early in case there are no write permissions
137         try {
138             if (!file.createNewFile()) {
139                 // This should not happen unless someone created the file right after we deleted it
140                 Log.e(TAG,
141                         String.format("Race with another user of result output file %s", filePath));
142                 return false;
143             }
144         } catch (IOException e) {
145             Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
146             return false;
147         }
148 
149         mTestOutputFile = filePath;
150 
151         if (mRunCountThreads) {
152             // Prepare system threads output debugging file
153             String sysThreadsDebugFilePath =
154                     String.format(
155                             "%s/system_threads_snapshot%d.txt",
156                             mTestOutputDir, UUID.randomUUID().hashCode());
157             File sysThreadsDebugFile = new File(sysThreadsDebugFilePath);
158 
159             // Make sure directory exists and file does not
160             if (directory.exists()) {
161                 if (sysThreadsDebugFile.exists() && !sysThreadsDebugFile.delete()) {
162                     Log.e(
163                             TAG,
164                             String.format(
165                                     "Failed to delete threads debugging files %s",
166                                     sysThreadsDebugFile));
167                     return false;
168                 }
169             } else {
170                 if (!directory.mkdirs()) {
171                     Log.e(
172                             TAG,
173                             String.format(
174                                     "Failed to create result output directory %s", mTestOutputDir));
175                     return false;
176                 }
177             }
178 
179             // Create an empty file to fail early in case there are no write permissions
180             try {
181                 if (!sysThreadsDebugFile.createNewFile()) {
182                     // This should not happen unless someone created the file right after we deleted
183                     // it
184                     Log.e(
185                             TAG,
186                             String.format(
187                                     "Race with another user of threads debugging files %s",
188                                     sysThreadsDebugFile));
189                     return false;
190                 }
191             } catch (IOException e) {
192                 Log.e(
193                         TAG,
194                         String.format(
195                                 "Failed to create threads debugging files %s", sysThreadsDebugFile),
196                         e);
197                 return false;
198             }
199             mSysThreadsDebugFile = sysThreadsDebugFilePath;
200         }
201 
202         return true;
203     }
204 
205     @Override
getMetrics()206     public Map<String, String> getMetrics() {
207         try {
208             if (mRunCountThreads) {
209                 mMemoryMap.putAll(execCountThreads());
210             }
211             // Drop cache if requested
212             if (mDropCacheOption > 0) {
213                 dropCache(mDropCacheOption);
214             }
215             if (mCollectForAllProcesses) {
216                 Log.i(TAG, "Collecting memory metrics for all processes.");
217                 mProcessNames = getAllProcessNames();
218             } else if (mProcessNames.length > 0) {
219                 Log.i(TAG, "Collecting memory only for given list of process");
220             } else if (mProcessNames.length == 0) {
221                 // No processes specified, just return empty map
222                 return mMemoryMap;
223             }
224             HashSet<Integer> zygoteChildrenPids = getZygoteChildrenPids();
225             FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
226 
227             try {
228                 // dump the activity lru to better understand the process state
229                 String activityLRU = executeShellCommand(ACTIVITY_LRU_CMD);
230                 Log.d(TAG, String.format("Dumpsys activity lru output: %s", activityLRU));
231             } catch (IOException e) {
232                 Log.e(TAG, String.format("Failed to execute %s", ACTIVITY_LRU_CMD));
233             }
234 
235             for (String processName : mProcessNames) {
236                 List<Integer> pids = new ArrayList<>();
237                 // Collect required data
238                 try {
239                     pids = getPids(processName);
240                     for (Integer pid : pids) {
241                         // Force Garbage collect to trim transient objects before taking memory
242                         // measurements as memory tests aim to track persistent memory regression
243                         // instead of transient memory which also allows for de-noising and reducing
244                         // likelihood of false alerts.
245                         if (mRunGcPrecollection && zygoteChildrenPids.contains(pid)) {
246                             // Skip native processes from sending GC signal.
247                             android.os.Trace.beginSection("IssueGCForPid: " + pid);
248                             // Perform a synchronous GC which happens when we request meminfo
249                             // This save us the need of setting up timeouts that may or may not
250                             // match with the end time of GC.
251                             mUiDevice.executeShellCommand("dumpsys meminfo -a " + pid);
252                             android.os.Trace.endSection();
253                         }
254 
255                         android.os.Trace.beginSection("ExecuteShowmap");
256                         String showmapOutput = execShowMap(processName, pid);
257                         android.os.Trace.endSection();
258                         // Mark the imperceptible process for showmap and child process count
259                         if (isProcessOomScoreAbove(
260                                 processName, pid, PROCESS_OOM_SCORE_IMPERCEPTIBLE)) {
261                             Log.i(
262                                     TAG,
263                                     String.format(
264                                             "This process is imperceptible: %s", processName));
265                             parseAndUpdateMemoryInfo(
266                                     processName,
267                                     showmapOutput,
268                                     OUTPUT_IMPERCEPTIBLE_METRIC_PATTERN);
269                         } else {
270                             parseAndUpdateMemoryInfo(
271                                     processName, showmapOutput, OUTPUT_METRIC_PATTERN);
272                         }
273 
274                         // Store showmap output into file. If there are more than one process
275                         // with same name write the individual showmap associated with pid.
276                         storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
277                         // Parse number of child processes for the given pid and update the
278                         // total number of child process count for the process name that pid
279                         // is associated with.
280                         updateChildProcessesDetails(processName, pid);
281                     }
282                 } catch (RuntimeException e) {
283                     Log.e(TAG, e.getMessage(), e.getCause());
284                     // Skip this process and continue with the next one
285                     continue;
286                 }
287             }
288             // To track total number of process with child processes.
289             if (mMemoryMap.size() != 0) {
290                 Set<String> parentWithChildProcessSet = mMemoryMap.keySet()
291                         .stream()
292                         .filter(s -> s.startsWith(CHILD_PROCESS_COUNT_PREFIX))
293                         .collect(Collectors.toSet());
294                 mMemoryMap.put(PROCESS_WITH_CHILD_PROCESS_COUNT,
295                         Long.toString(parentWithChildProcessSet.size()));
296             }
297             // Store the unique process count. -1 to exclude the "ps" process name.
298             mMemoryMap.put(PROCESS_COUNT, Integer.toString(mProcessNames.length - 1));
299             writer.close();
300             mMemoryMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
301         } catch (RuntimeException e) {
302             Log.e(TAG, e.getMessage(), e.getCause());
303         } catch (IOException e) {
304             Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
305         }
306         return mMemoryMap;
307     }
308 
getZygoteChildrenPids()309     public HashSet<Integer> getZygoteChildrenPids() {
310         HashSet<Integer> allZygoteChildren;
311         allZygoteChildren = getChildrenPids("zygote");
312         HashSet<Integer> zyg64children = getChildrenPids("zygote64");
313         allZygoteChildren.addAll(zyg64children);
314         return allZygoteChildren;
315     }
316 
getChildrenPids(String processName)317     public HashSet<Integer> getChildrenPids(String processName) {
318         HashSet<Integer> childrenPids = new HashSet<>();
319         String childrenCmdOutput = "";
320         try {
321             // Execute shell does not support shell substitution so it has to be executed twice.
322             childrenCmdOutput = mUiDevice.executeShellCommand(
323                 "pgrep -P " + mUiDevice.executeShellCommand("pidof " + processName));
324         } catch (IOException e) {
325             Log.e(TAG, "Exception occurred reading children for process " + processName);
326         }
327         String[] lines = childrenCmdOutput.split("\\R");
328         for (String line : lines) {
329             try {
330                 int pid = Integer.parseInt(line);
331                 childrenPids.add(pid);
332             } catch (NumberFormatException e) {
333                 // If the process does not exist or the shell command fails
334                 // just skip the pid, this is because there could be some
335                 // devices that contain a process while others do not.
336             }
337         }
338         return childrenPids;
339     }
340 
341     @Override
stopCollecting()342     public boolean stopCollecting() {
343         return true;
344     }
345 
346     /**
347      * Sets option for running GC prior to collection.
348      *
349      * @param shouldGcOnPrecollect whether it should run GC prior to showmap collection
350      */
setGcOnPrecollectOption(boolean shouldGcOnPrecollect)351     public void setGcOnPrecollectOption(boolean shouldGcOnPrecollect) {
352         mRunGcPrecollection = shouldGcOnPrecollect;
353     }
354 
355     /**
356      * Sets option for counting the threads for all processes.
357      *
358      * @param shouldCountThreads whether it should run count threads
359      */
setCountThreadsOption(boolean shouldCountThreads)360     public void setCountThreadsOption(boolean shouldCountThreads) {
361         mRunCountThreads = shouldCountThreads;
362     }
363 
364     /**
365      * Set drop cache option.
366      *
367      * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
368      * @return true on success, false if input option is invalid
369      */
setDropCacheOption(int dropCacheOption)370     public boolean setDropCacheOption(int dropCacheOption) {
371         // Valid values are 1..3
372         if (dropCacheOption < 1 || dropCacheOption > 3) {
373             return false;
374         }
375 
376         mDropCacheOption = dropCacheOption;
377         return true;
378     }
379 
380     /**
381      * Drops kernel memory cache.
382      *
383      * @param cacheOption drop pagecache (1), slab (2) or all (3) caches
384      */
dropCache(int cacheOption)385     private void dropCache(int cacheOption) throws RuntimeException {
386         try {
387             mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
388         } catch (IOException e) {
389             throw new RuntimeException("Unable to drop caches", e);
390         }
391     }
392 
393     /**
394      * Get pid's of the process with {@code processName} name.
395      *
396      * @param processName name of the process to get pid
397      * @return pid's of the specified process
398      */
getPids(String processName)399     private List<Integer> getPids(String processName) throws RuntimeException {
400         try {
401             String pidofOutput = mUiDevice
402                     .executeShellCommand(String.format(PIDOF_CMD, processName));
403 
404             // Sample output for the process with more than 1 pid.
405             // Sample command : "pidof init"
406             // Sample output : 1 559
407             String[] pids = pidofOutput.split("\\s+");
408             List<Integer> pidList = new ArrayList<>();
409             for (String pid : pids) {
410                 pidList.add(Integer.parseInt(pid.trim()));
411             }
412             return pidList;
413         } catch (IOException e) {
414             throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
415         }
416     }
417 
418     /**
419      * Executes showmap command for the process with {@code processName} name and {@code pid} pid.
420      *
421      * @param processName name of the process to run showmap for
422      * @param pid pid of the process to run showmap for
423      * @return the output of showmap command
424      */
execShowMap(String processName, long pid)425     private String execShowMap(String processName, long pid) throws IOException {
426         try {
427             return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
428         } catch (IOException e) {
429             throw new RuntimeException(
430                     String.format("Unable to execute showmap command for %s ", processName), e);
431         }
432     }
433 
434     /**
435      * Executes counting threads command for the process.
436      *
437      * @param processName name of the process to run showmap for
438      * @param pid pid of the process to run showmap for
439      * @return the output of showmap command
440      */
execCountThreads()441     private Map<String, String> execCountThreads() throws IOException {
442         String countOutput;
443         Map<String, String> countResults = new HashMap<>();
444         try {
445             // Run ps -AT into file for debugging
446             try (FileWriter sysThreadsDebugFileWriter =
447                     new FileWriter(new File(mSysThreadsDebugFile), true)) {
448                 sysThreadsDebugFileWriter.write(executeShellCommand(DUMP_SYS_THREADS_CMD));
449             }
450             countResults.put(SYSTEM_THREADS_FILE_PATH_KEY, mSysThreadsDebugFile);
451 
452             // Run count threads command and save it to metrics map
453             File execTempFile = new File(COUNT_THREADS_FILE_PATH);
454             execTempFile.setWritable(true);
455             execTempFile.setExecutable(true, /*ownersOnly*/ false);
456             String countThreadsScriptPath = execTempFile.getAbsolutePath();
457             BufferedWriter writer = new BufferedWriter(new FileWriter(countThreadsScriptPath));
458             writer.write(COUNT_THREADS_EXEC_SCRIPT);
459             writer.close();
460             countOutput = executeShellCommand(COUNT_THREADS_CMD);
461             Pattern pattern =
462                     Pattern.compile(THREADS_PATTERN, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
463             String[] lines = countOutput.split("\n");
464             for (String line : lines) {
465                 Matcher matcher = pattern.matcher(line);
466                 boolean matchFound = matcher.find();
467                 if (matchFound) {
468                     countResults.put(matcher.group(1), matcher.group(2));
469                 }
470             }
471             return countResults;
472         } catch (IOException e) {
473             throw new RuntimeException("Unable to execute counting threads command", e);
474         }
475     }
476 
477     /**
478      * Extract memory metrics from showmap command output for the process with {@code processName}
479      * name.
480      *
481      * @param processName name of the process to extract memory info for
482      * @param showmapOutput showmap command output
483      */
parseAndUpdateMemoryInfo( String processName, String showmapOutput, String metricPattern)484     private void parseAndUpdateMemoryInfo(
485             String processName, String showmapOutput, String metricPattern)
486             throws RuntimeException {
487         try {
488 
489             // -------- -------- -------- -------- -------- -------- -------- -------- ----- ------
490             // ----
491             // virtual shared shared private private
492             // size RSS PSS clean dirty clean dirty swap swapPSS flags object
493             // ------- -------- -------- -------- -------- -------- -------- -------- ------ -----
494             // ----
495             // 10810272 5400 1585 3800 168 264 1168 0 0 TOTAL
496 
497             int pos = showmapOutput.lastIndexOf("----");
498             String summarySplit[] = showmapOutput.substring(pos).trim().split("\\s+");
499 
500             for (Map.Entry<String, List<Integer>> entry : mMetricNameIndexMap.entrySet()) {
501                 Long metricValue = 0L;
502                 String metricKey =
503                         constructKey(String.format(metricPattern, entry.getKey()), processName);
504                 for (int index = 0; index < entry.getValue().size(); index++) {
505                     metricValue += Long.parseLong(summarySplit[entry.getValue().get(index) + 1]);
506                 }
507                 // If there are multiple pids associated with the process name then update the
508                 // existing entry in the map otherwise add new entry in the map.
509                 if (mMemoryMap.containsKey(metricKey)) {
510                     long currValue = Long.parseLong(mMemoryMap.get(metricKey));
511                     mMemoryMap.put(metricKey, Long.toString(currValue + metricValue * 1024));
512                 } else {
513                     mMemoryMap.put(metricKey, Long.toString(metricValue * 1024));
514                 }
515             }
516         } catch (IndexOutOfBoundsException | InputMismatchException e) {
517             throw new RuntimeException(
518                     String.format("Unexpected showmap format for %s ", processName), e);
519         }
520     }
521 
522     /**
523      * Store test results for one process into file.
524      *
525      * @param fileName name of the file being written
526      * @param processName name of the process
527      * @param pid pid of the process
528      * @param showmapOutput showmap command output
529      * @param writer file writer to write the data
530      */
storeToFile(String fileName, String processName, long pid, String showmapOutput, FileWriter writer)531     private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
532             FileWriter writer) throws RuntimeException {
533         try {
534             writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
535             writer.write(showmapOutput);
536             writer.write('\n');
537         } catch (IOException e) {
538             throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
539         }
540     }
541 
542     /**
543      * Set the memory metric name and corresponding index to parse from the showmap output summary.
544      *
545      * @param metricNameIndexStr comma separated metric_name:index TODO: Pre-process the string into
546      *            map and pass the map to this method.
547      */
setMetricNameIndex(String metricNameIndexStr)548     public void setMetricNameIndex(String metricNameIndexStr) {
549         /**
550          * example: metricNameIndexStr rss:1,pss:2,privatedirty:6:7
551          * converted to Map: {'rss': [1], 'pss': [2], 'privatedirty': [6, 7]}
552          */
553         Log.i(TAG, String.format("Metric Name index %s", metricNameIndexStr));
554         String metricDetails[] = metricNameIndexStr.split(",");
555         for (String metricDetail : metricDetails) {
556             List<Integer> indexList = new ArrayList<>();
557             String metricDetailsSplit[] = metricDetail.split(":");
558             for (int index = 1; index < metricDetailsSplit.length; index++) {
559                 indexList.add(Integer.parseInt(metricDetailsSplit[index]));
560             }
561             if (!indexList.isEmpty()) {
562                 mMetricNameIndexMap.put(metricDetailsSplit[0], indexList);
563             }
564         }
565         Log.i(TAG, String.format("Metric Name index map size %s", mMetricNameIndexMap.size()));
566     }
567 
568     /**
569      * Return true if the giving process is imperceptible. If the OOM adjustment score is in [900,
570      * 1000), the process is cached. If the OOM adjustment score is in (-1000, 200], the process is
571      * perceptible. If the OOM adjustment score is in (200, 1000), the process is imperceptible
572      */
isProcessOomScoreAbove(String processName, long pid, int threshold)573     public boolean isProcessOomScoreAbove(String processName, long pid, int threshold) {
574         try {
575             String score = executeShellCommand(String.format(OOM_SCORE_ADJ_CMD, pid));
576             boolean result = Integer.parseInt(score.trim()) > threshold;
577             Log.i(
578                     TAG,
579                     String.format(
580                             "The OOM adjustment score for process %s is %s", processName, score));
581             return result;
582         } catch (IOException e) {
583             Log.e(TAG, String.format("Unable to get process oom_score_adj for %s", processName), e);
584             // We don't know the process is cached or not, still collect it
585             return false;
586         }
587     }
588 
589     /**
590      * Retrieves the number of child processes for the given process id and updates the total
591      * process count and adds a child process metric for the process name that pid is associated
592      * with.
593      *
594      * @param processName
595      * @param pid
596      */
updateChildProcessesDetails(String processName, long pid)597     private void updateChildProcessesDetails(String processName, long pid) {
598         String childProcessName;
599         String childPID;
600         String completeChildProcessMetric;
601         try {
602             Log.i(TAG,
603                     String.format("Retrieving child processes count for process name: %s with"
604                             + " process id %d.", processName, pid));
605             String childProcessesStr = mUiDevice
606                     .executeShellCommand(String.format(CHILD_PROCESSES_CMD, pid));
607             Log.i(TAG, String.format("Child processes cmd output: %s", childProcessesStr));
608 
609             int childProcessCount = 0;
610             String[] childProcessStrSplit = childProcessesStr.split("\\n");
611             for (String line : childProcessStrSplit) {
612                 // To discard the header line in the command output.
613                 if (Objects.equals(line, childProcessStrSplit[0])) continue;
614                 String[] childProcessSplit = line.trim().split("\\s+");
615                 /**
616                  * final metric will be of following format
617                  * parent_process_<process>_child_process_<process>
618                  * parent_process_zygote64_child_process_system_server
619                  */
620                 childPID = childProcessSplit[1];
621                 childProcessName = childProcessSplit[8];
622                 // Skip the logcat and sh processes in child process count
623                 if (SKIP_PROCESS.contains(childProcessName)
624                         || isProcessOomScoreAbove(
625                                 childProcessName,
626                                 Long.parseLong(childPID),
627                                 PROCESS_OOM_SCORE_CACHED)) {
628                     Log.i(
629                             TAG,
630                             String.format(
631                                     "Skip the child process %s in the parent process %s.",
632                                     childProcessName, processName));
633                     continue;
634                 }
635                 childProcessCount++;
636                 completeChildProcessMetric =
637                         String.join(
638                                 METRIC_VALUE_SEPARATOR,
639                                 PARENT_PROCESS_STRING,
640                                 processName,
641                                 CHILD_PROCESS_STRING,
642                                 childProcessName);
643                 mMemoryMap.put(completeChildProcessMetric, "1");
644             }
645             String childCountMetricKey = String.format(OUTPUT_CHILD_PROCESS_COUNT_KEY, processName);
646             if (childProcessCount > 0) {
647                 mMemoryMap.put(childCountMetricKey,
648                         Long.toString(
649                                 Long.parseLong(mMemoryMap.getOrDefault(childCountMetricKey, "0"))
650                                         + childProcessCount));
651             }
652         } catch (IOException e) {
653             throw new RuntimeException("Unable to run child process command.", e);
654         }
655     }
656 
657     /**
658      * Enables memory collection for all processes.
659      */
setAllProcesses()660     public void setAllProcesses() {
661         mCollectForAllProcesses = true;
662     }
663 
664     /**
665      * Get all process names running in the system.
666      */
getAllProcessNames()667     private String[] getAllProcessNames() {
668         Set<String> allProcessNames = new LinkedHashSet<>();
669         try {
670             String psOutput = mUiDevice.executeShellCommand(ALL_PROCESSES_CMD);
671             // Split the lines
672             String allProcesses[] = psOutput.split("\\n");
673             for (String invidualProcessDetails : allProcesses) {
674                 Log.i(TAG, String.format("Process detail: %s", invidualProcessDetails));
675                 // Sample process detail line
676                 // system 603 1 41532 5396 SyS_epoll+ 0 S servicemanager
677                 String processSplit[] = invidualProcessDetails.split("\\s+");
678                 // Parse process name
679                 String processName = processSplit[processSplit.length - 1].trim();
680                 // Include the process name which are not enclosed in [].
681                 if (!processName.startsWith("[") && !processName.endsWith("]")) {
682                     // Skip the first (i.e header) line from "ps -A" output.
683                     if (processName.equalsIgnoreCase("NAME")) {
684                         continue;
685                     }
686                     Log.i(TAG, String.format("Including the process %s", processName));
687                     allProcessNames.add(processName);
688                 }
689             }
690         } catch (IOException ioe) {
691             throw new RuntimeException(
692                     String.format("Unable execute all processes command %s ", ALL_PROCESSES_CMD),
693                     ioe);
694         }
695         return allProcessNames.toArray(new String[0]);
696     }
697 
698     /* Execute a shell command and return its output. */
699     @VisibleForTesting
executeShellCommand(String command)700     public String executeShellCommand(String command) throws IOException {
701         return mUiDevice.executeShellCommand(command);
702     }
703 }
704