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