1 /* 2 * Copyright (C) 2020 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 android.os.SystemClock; 20 import android.util.Log; 21 22 import androidx.annotation.VisibleForTesting; 23 import androidx.test.InstrumentationRegistry; 24 import androidx.test.uiautomator.UiDevice; 25 26 import java.io.BufferedReader; 27 import java.io.File; 28 import java.io.FileReader; 29 import java.io.IOException; 30 import java.nio.file.Path; 31 import java.nio.file.Paths; 32 import java.util.HashMap; 33 import java.util.Map; 34 35 /** 36 * SimpleperfHelper is used to start and stop simpleperf sample collection and move the output 37 * sample file to the destination folder. 38 */ 39 public class SimpleperfHelper { 40 41 private static final String LOG_TAG = SimpleperfHelper.class.getSimpleName(); 42 private static final String SIMPLEPERF_TMP_FILE_PATH = "/data/local/tmp/perf.data"; 43 private static final String SIMPLEPERF_REPORT_TMP_FILE_PATH = "/data/local/tmp/perf_report.txt"; 44 45 private static final String SIMPLEPERF_START_CMD = "simpleperf %s -o %s %s"; 46 private static final String SIMPLEPERF_STOP_CMD = "pkill -INT simpleperf"; 47 private static final String SIMPLEPERF_PROC_ID_CMD = "pidof simpleperf"; 48 private static final String REMOVE_CMD = "rm %s"; 49 private static final String MOVE_CMD = "mv %s %s"; 50 51 private static final int SIMPLEPERF_START_WAIT_COUNT = 3; 52 private static final int SIMPLEPERF_START_WAIT_TIME = 1000; 53 private static final int SIMPLEPERF_STOP_WAIT_COUNT = 60; 54 private static final long SIMPLEPERF_STOP_WAIT_TIME = 15000; 55 56 private final UiDevice mUiDevice; 57 58 /** Constructor to receive visible UiDevice. Should not be used except for testing. */ 59 @VisibleForTesting SimpleperfHelper(UiDevice uidevice)60 public SimpleperfHelper(UiDevice uidevice) { 61 mUiDevice = uidevice; 62 } 63 SimpleperfHelper()64 public SimpleperfHelper() { 65 mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 66 } 67 startCollecting(String subcommand, String arguments)68 public boolean startCollecting(String subcommand, String arguments) { 69 try { 70 // Cleanup any running simpleperf sessions. 71 Log.i(LOG_TAG, "Cleanup simpleperf before starting."); 72 if (isSimpleperfRunning()) { 73 Log.i(LOG_TAG, "Simpleperf is already running. Stopping simpleperf."); 74 if (!stopSimpleperf()) { 75 return false; 76 } 77 } 78 79 Log.i(LOG_TAG, String.format("Starting simpleperf")); 80 new Thread() { 81 @Override 82 public void run() { 83 String startCommand = 84 String.format( 85 SIMPLEPERF_START_CMD, 86 subcommand, 87 SIMPLEPERF_TMP_FILE_PATH, 88 arguments); 89 Log.i(LOG_TAG, String.format("Start command: %s", startCommand)); 90 try { 91 String startOutput = mUiDevice.executeShellCommand(startCommand); 92 Log.i( 93 LOG_TAG, 94 String.format("Simpleperf start command output - %s", startOutput)); 95 } catch (IOException e) { 96 Log.e(LOG_TAG, "Failed to start simpleperf."); 97 } 98 } 99 }.start(); 100 101 int waitCount = 0; 102 while (!isSimpleperfRunning()) { 103 if (waitCount < SIMPLEPERF_START_WAIT_COUNT) { 104 SystemClock.sleep(SIMPLEPERF_START_WAIT_TIME); 105 waitCount++; 106 continue; 107 } 108 Log.e(LOG_TAG, "Simpleperf sampling failed to start."); 109 return false; 110 } 111 } catch (IOException e) { 112 Log.e(LOG_TAG, "Unable to start simpleperf sampling due to :" + e.getMessage()); 113 return false; 114 } 115 Log.i(LOG_TAG, "Simpleperf sampling started successfully."); 116 return true; 117 } 118 119 /** 120 * Stop the simpleperf sample collection under /data/local/tmp/perf.data and copy the output to 121 * the destination file. 122 * 123 * @param destinationFile file to copy the simpleperf sample file to. 124 * @return true if the trace collection is successful otherwise false. 125 */ stopCollecting(String destinationFile)126 public boolean stopCollecting(String destinationFile) { 127 Log.i(LOG_TAG, "Stopping simpleperf."); 128 try { 129 if (stopSimpleperf()) { 130 if (!copyFileOutput(destinationFile)) { 131 return false; 132 } 133 } else { 134 Log.e(LOG_TAG, "Simpleperf failed to stop"); 135 return false; 136 } 137 } catch (IOException e) { 138 Log.e(LOG_TAG, "Unable to stop the simpleperf samping due to " + e.getMessage()); 139 return false; 140 } 141 return true; 142 } 143 144 /** 145 * Utility method for sending the signal to stop simpleperf. 146 * 147 * @return true if simpleperf is successfully stopped. 148 */ stopSimpleperf()149 public boolean stopSimpleperf() throws IOException { 150 if (!isSimpleperfRunning()) { 151 Log.e(LOG_TAG, "Simpleperf stop called, but simpleperf is not running."); 152 return false; 153 } 154 155 String stopOutput = mUiDevice.executeShellCommand(SIMPLEPERF_STOP_CMD); 156 Log.i(LOG_TAG, String.format("Simpleperf stop command ran: %s", SIMPLEPERF_STOP_CMD)); 157 int waitCount = 0; 158 while (isSimpleperfRunning()) { 159 if (waitCount < SIMPLEPERF_STOP_WAIT_COUNT) { 160 SystemClock.sleep(SIMPLEPERF_STOP_WAIT_TIME); 161 waitCount++; 162 continue; 163 } 164 Log.e(LOG_TAG, "Simpleperf failed to stop"); 165 return false; 166 } 167 Log.i(LOG_TAG, "Simpleperf stopped successfully."); 168 return true; 169 } 170 171 /** 172 * Method for generating simpleperf report and getting report metrics. 173 * 174 * @param path Path to read binary record from. 175 * @param processToPid Map with process names and PIDs to look for in record file. 176 * @param symbols Symbols to report events from the processes recorded 177 * @return Map containing recorded processes and nested map of symbols and event count for each 178 * symbol. 179 */ getSimpleperfReport( String path, Map.Entry<String, String> processToPid, Map<String, String> symbols, int testIterations)180 public Map<String /*event-process-symbol*/, String /*eventCount*/> getSimpleperfReport( 181 String path, 182 Map.Entry<String, String> processToPid, 183 Map<String, String> symbols, 184 int testIterations) { 185 try { 186 String reportCommand = 187 String.format( 188 "simpleperf report -i %s --pids %s --sort pid,symbol -o %s" 189 + " --print-event-count --children", 190 path, processToPid.getValue(), SIMPLEPERF_REPORT_TMP_FILE_PATH); 191 Log.i(LOG_TAG, String.format("Report command: %s", reportCommand)); 192 mUiDevice.executeShellCommand(reportCommand); 193 return getMetrics(processToPid.getKey(), symbols, testIterations); 194 } catch (IOException e) { 195 Log.e(LOG_TAG, "Could not generate report: " + e.getMessage()); 196 } 197 return new HashMap<>(); 198 } 199 200 /** 201 * Utility method for extracting metrics from given simpleperf report. 202 * 203 * @param process Individually extracted processes recorded in binary record file. 204 * @param symbols Symbols to report events from the processes recorded. 205 * @return Map containing recorded event counts from symbols within process 206 */ getMetrics( String process, Map<String, String> symbols, int testIterations)207 private Map<String, String> getMetrics( 208 String process, Map<String, String> symbols, int testIterations) { 209 Map<String, String> results = new HashMap<>(); 210 try { 211 String eventName = ""; 212 BufferedReader reader = 213 new BufferedReader( 214 new FileReader(SimpleperfHelper.SIMPLEPERF_REPORT_TMP_FILE_PATH)); 215 for (String line; (line = reader.readLine()) != null; ) { 216 // Checking for top of the report to find event name and event count. 217 // Event count: 3498520605 218 if (line.contains(": ")) { 219 String[] splitLine = line.split(": "); 220 if (splitLine[0].equals("Event")) { 221 eventName = splitLine[1].split(" ")[0]; 222 } else if (splitLine[0].equals("Event count")) { 223 String key = String.join("-", process, eventName); 224 long count = Long.parseLong(splitLine[1]) / testIterations; 225 results.put(key, String.valueOf(count)); 226 } 227 } 228 // Parsing lines for specific symbols in report to store with event count to results 229 // Children Self AccEventCount SelfEventCount Pid Symbol 230 // 54.20% 0.00% 122803507 0 2510 __start_thread 231 else if (line.contains("%")) { 232 final String[] splitLine = line.split("\\s+", 6); 233 final String parsedSymbol = splitLine[5].trim(); 234 final String matchedSymbol = getMatchingSymbol(symbols, parsedSymbol); 235 if (matchedSymbol == null) { 236 continue; 237 } 238 String key = String.join("-", process, matchedSymbol, eventName); 239 if (results.containsKey(key + "-percentage")) { 240 // We are searching for symbols with partial matches so only include the 241 // first hit if we get multiple matches. 242 continue; 243 } 244 245 // Remove trailing % 246 String percentage = splitLine[0].substring(0, splitLine[0].length() - 1); 247 results.put(key + "-percentage", percentage); 248 String eventCount = splitLine[2].trim(); 249 long count = Long.parseLong(eventCount) / testIterations; 250 results.put(key + "-count", String.valueOf(count)); 251 } 252 } 253 } catch (Exception e) { 254 Log.e(LOG_TAG, "Could not open report file: " + e.getMessage()); 255 } 256 return results; 257 } 258 getMatchingSymbol(Map<String, String> symbols, String parsedSymbol)259 private static String getMatchingSymbol(Map<String, String> symbols, String parsedSymbol) { 260 for (String candidate : symbols.keySet()) { 261 if (parsedSymbol.contains(candidate)) { 262 return symbols.get(candidate); 263 } 264 } 265 return null; 266 } 267 268 /** 269 * Convert process name into process ID usable for simpleperf commands 270 * 271 * @param process the name of a running process 272 * @return String containing the process ID 273 */ getPID(String process)274 public String getPID(String process) { 275 try { 276 return mUiDevice.executeShellCommand("pidof " + process).trim(); 277 } catch (Exception e) { 278 Log.e(LOG_TAG, "Could not resolve PID for " + process, e); 279 return ""; 280 } 281 } 282 283 /** 284 * Check if there is a simpleperf instance running. 285 * 286 * @return true if there is a running simpleperf instance, otherwise false. 287 */ isSimpleperfRunning()288 private boolean isSimpleperfRunning() { 289 try { 290 String simpleperfProcId = mUiDevice.executeShellCommand(SIMPLEPERF_PROC_ID_CMD); 291 Log.i(LOG_TAG, String.format("Simpleperf process id - %s", simpleperfProcId)); 292 if (simpleperfProcId.isEmpty()) { 293 return false; 294 } 295 } catch (IOException e) { 296 Log.e(LOG_TAG, "Unable to check simpleperf status: " + e.getMessage()); 297 return false; 298 } 299 return true; 300 } 301 302 /** 303 * Copy the temporary simpleperf output file to the given destinationFile. 304 * 305 * @param destinationFile file to copy simpleperf output into. 306 * @return true if the simpleperf file copied successfully, otherwise false. 307 */ copyFileOutput(String destinationFile)308 public boolean copyFileOutput(String destinationFile) { 309 Path path = Paths.get(destinationFile); 310 String destDirectory = path.getParent().toString(); 311 // Check if directory already exists 312 File directory = new File(destDirectory); 313 if (!directory.exists()) { 314 boolean success = directory.mkdirs(); 315 if (!success) { 316 Log.e( 317 LOG_TAG, 318 String.format( 319 "Result output directory %s not created successfully.", 320 destDirectory)); 321 return false; 322 } 323 } 324 325 // Copy the collected trace from /data/local/tmp to the destinationFile. 326 try { 327 String moveResult = 328 mUiDevice.executeShellCommand( 329 String.format(MOVE_CMD, SIMPLEPERF_TMP_FILE_PATH, destinationFile)); 330 if (!moveResult.isEmpty()) { 331 Log.e( 332 LOG_TAG, 333 String.format( 334 "Unable to move simpleperf output file from %s to %s due to %s", 335 SIMPLEPERF_TMP_FILE_PATH, destinationFile, moveResult)); 336 return false; 337 } 338 } catch (IOException e) { 339 Log.e( 340 LOG_TAG, 341 "Unable to move the simpleperf sample file to destination file." 342 + e.getMessage()); 343 return false; 344 } 345 return true; 346 } 347 } 348