1 /* 2 * Copyright (C) 2018 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.app.UiAutomation; 20 import android.os.ParcelFileDescriptor; 21 import android.os.SystemClock; 22 import android.util.Log; 23 24 import androidx.annotation.VisibleForTesting; 25 import androidx.test.InstrumentationRegistry; 26 import androidx.test.uiautomator.UiDevice; 27 28 import java.io.File; 29 import java.io.FileNotFoundException; 30 import java.io.IOException; 31 import java.io.PrintWriter; 32 import java.nio.file.Path; 33 import java.nio.file.Paths; 34 import java.util.HashSet; 35 import java.util.Set; 36 37 /** 38 * PerfettoHelper is used to start and stop the perfetto tracing and move the 39 * output perfetto trace file to destination folder. 40 */ 41 public class PerfettoHelper { 42 43 private static final String LOG_TAG = PerfettoHelper.class.getSimpleName(); 44 // Command to start the perfetto tracing in the background. The "perfetto" process will wait 45 // until tracing is fully started (i.e. all data sources are active) before backgrounding and 46 // returning from the original shell invocation. 47 // perfetto --background-wait -c /data/misc/perfetto-traces/trace_config.pb -o 48 // /data/misc/perfetto-traces/trace_output.perfetto-trace 49 private static final String PERFETTO_START_BG_WAIT_CMD = 50 "perfetto --background-wait -c %s%s -o %s"; 51 private static final String PERFETTO_START_CMD = "perfetto --background -c %s%s -o %s"; 52 private static final String PERFETTO_TMP_OUTPUT_FILE = 53 "/data/misc/perfetto-traces/trace_output.perfetto-trace"; 54 // Additional arg to indicate that the perfetto config file is text format. 55 private static final String PERFETTO_TXT_PROTO_ARG = " --txt"; 56 // Command to stop (i.e kill) the perfetto tracing. 57 private static final String PERFETTO_STOP_CMD = "kill %d"; 58 // Command to return the process details if it is still running otherwise returns empty string. 59 private static final String PERFETTO_PROC_ID_EXIST_CHECK = "ls -l /proc/%d/exe"; 60 // Remove the trace output file /data/misc/perfetto-traces/trace_output.perfetto-trace 61 private static final String REMOVE_CMD = "rm %s"; 62 // Add the trace output file /data/misc/perfetto-traces/trace_output.perfetto-trace 63 private static final String CREATE_FILE_CMD = "touch %s"; 64 // Command to move the perfetto output trace file to given folder. 65 private static final String MOVE_CMD = "mv %s %s"; 66 // Max wait count for checking if perfetto is stopped successfully 67 private static final int PERFETTO_KILL_WAIT_COUNT = 12; 68 // Check if perfetto is stopped every 5 secs. 69 private static final long PERFETTO_KILL_WAIT_TIME = 5000; 70 private static final String PERFETTO_PID_FILE_PREFIX = "perfetto_pid_"; 71 72 private static Set<Integer> sPerfettoProcessIds = new HashSet<>(); 73 74 private UiDevice mUIDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 75 76 private String mConfigRootDir; 77 78 private boolean mPerfettoStartBgWait; 79 80 private int mPerfettoProcId = 0; 81 82 private String mTextProtoConfig; 83 private String mConfigFileName; 84 private boolean mIsTextProtoConfig; 85 private boolean mTrackPerfettoPidFlag; 86 private String mTrackPerfettoRootDir = "sdcard/"; 87 private File mPerfettoPidFile; 88 89 /** Set content of the perfetto configuration to be used when tracing */ setTextProtoConfig(String value)90 public PerfettoHelper setTextProtoConfig(String value) { 91 mTextProtoConfig = value; 92 return this; 93 } 94 95 /** Set file name of the perfetto configuration to be used when tracing */ setConfigFileName(String value)96 public PerfettoHelper setConfigFileName(String value) { 97 mConfigFileName = value; 98 return this; 99 } 100 101 /** Set if the configuration is in text proto format */ setIsTextProtoConfig(boolean value)102 public PerfettoHelper setIsTextProtoConfig(boolean value) { 103 mIsTextProtoConfig = value; 104 return this; 105 } 106 107 /** 108 * Start the perfetto tracing in background using the given config file or config, and write the 109 * output to /data/misc/perfetto-traces/trace_output.perfetto-trace. If both config file and 110 * config are received, use config file 111 * 112 * @throws IllegalStateException if neither a config or a config file is set 113 * @return true if trace collection started successfully otherwise return false. 114 */ startCollecting()115 public boolean startCollecting() { 116 String textProtoConfig = mTextProtoConfig != null ? mTextProtoConfig : ""; 117 String configFileName = mConfigFileName != null ? mConfigFileName : ""; 118 if (textProtoConfig.isEmpty() && configFileName.isEmpty()) { 119 throw new IllegalStateException( 120 "Perfetto helper not configured. Set a configuration " 121 + "or a configuration file before start tracing"); 122 } 123 124 if (!textProtoConfig.isEmpty()) { 125 return startCollectingFromConfig(mTextProtoConfig); 126 } 127 128 return startCollectingFromConfigFile(mConfigFileName, mIsTextProtoConfig); 129 } 130 131 /** 132 * Start the perfetto tracing in background using the given config and write the output to 133 * /data/misc/perfetto-traces/trace_output.perfetto-trace. 134 * 135 * @param textProtoConfig configuration in text proto format to pass to perfetto 136 * @return true if trace collection started successfully otherwise return false. 137 */ 138 @VisibleForTesting startCollectingFromConfig(String textProtoConfig)139 public boolean startCollectingFromConfig(String textProtoConfig) { 140 mPerfettoPidFile = null; 141 String startOutput = null; 142 if (textProtoConfig == null || textProtoConfig.isEmpty()) { 143 Log.e(LOG_TAG, "Perfetto config is null or empty."); 144 return false; 145 } 146 147 try { 148 if (!canSetupBeforeStartCollecting()) { 149 return false; 150 } 151 152 String perfettoCmd = 153 String.format( 154 mPerfettoStartBgWait ? PERFETTO_START_BG_WAIT_CMD : PERFETTO_START_CMD, 155 "- ", 156 "--txt", 157 PERFETTO_TMP_OUTPUT_FILE); 158 159 // Start perfetto tracing. 160 Log.i(LOG_TAG, "Starting perfetto tracing."); 161 UiAutomation uiAutomation = 162 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 163 ParcelFileDescriptor[] fileDescriptor = uiAutomation.executeShellCommandRw(perfettoCmd); 164 ParcelFileDescriptor inputStreamDescriptor = fileDescriptor[0]; 165 ParcelFileDescriptor outputStreamDescriptor = fileDescriptor[1]; 166 167 try (ParcelFileDescriptor.AutoCloseOutputStream outputStream = 168 new ParcelFileDescriptor.AutoCloseOutputStream(outputStreamDescriptor)) { 169 outputStream.write(textProtoConfig.getBytes()); 170 } 171 172 try (ParcelFileDescriptor.AutoCloseInputStream inputStream = 173 new ParcelFileDescriptor.AutoCloseInputStream(inputStreamDescriptor)) { 174 startOutput = new String(inputStream.readAllBytes()); 175 // Persist perfetto pid in a file and use it for cleanup if the instrumentation 176 // crashes. 177 if (mTrackPerfettoPidFlag) { 178 mPerfettoPidFile = writePidToFile(startOutput); 179 } 180 if (!canUpdateAfterStartCollecting(startOutput)) { 181 return false; 182 } 183 } 184 } catch (FileNotFoundException fnf) { 185 Log.e(LOG_TAG, "Unable to write perfetto process id to a file :" + fnf.getMessage()); 186 Log.i(LOG_TAG, "Stopping perfetto tracing because perfetto id is not tracked."); 187 try { 188 stopPerfetto(Integer.parseInt(startOutput.trim())); 189 } catch (IOException ie) { 190 Log.e(LOG_TAG, "Unable to stop perfetto process output file." + ie.getMessage()); 191 } 192 return false; 193 } catch (IOException ioe) { 194 Log.e(LOG_TAG, "Unable to start the perfetto tracing due to :" + ioe.getMessage()); 195 return false; 196 } 197 Log.i(LOG_TAG, "Perfetto tracing started successfully."); 198 return true; 199 } 200 201 /** 202 * Start the perfetto tracing in background using the given config file and write the ouput to 203 * /data/misc/perfetto-traces/trace_output.perfetto-trace. Perfetto has access only to 204 * /data/misc/perfetto-traces/ folder. So the config file has to be under 205 * /data/misc/perfetto-traces/ folder in the device. 206 * 207 * @param configFileName used for collecting the perfetto trace. 208 * @param isTextProtoConfig true if the config file is textproto format otherwise false. 209 * @return true if trace collection started successfully otherwise return false. 210 */ 211 @VisibleForTesting startCollectingFromConfigFile(String configFileName, boolean isTextProtoConfig)212 public boolean startCollectingFromConfigFile(String configFileName, boolean isTextProtoConfig) { 213 mPerfettoPidFile = null; 214 String startOutput = null; 215 if (configFileName == null || configFileName.isEmpty()) { 216 Log.e(LOG_TAG, "Perfetto config file name is null or empty."); 217 return false; 218 } 219 220 if (mConfigRootDir == null || mConfigRootDir.isEmpty()) { 221 Log.e(LOG_TAG, "Perfetto trace config root directory name is null or empty."); 222 return false; 223 } 224 225 try { 226 if (!canSetupBeforeStartCollecting()) { 227 return false; 228 } 229 230 String perfettoCmd = 231 String.format( 232 mPerfettoStartBgWait ? PERFETTO_START_BG_WAIT_CMD : PERFETTO_START_CMD, 233 mConfigRootDir, 234 configFileName, 235 PERFETTO_TMP_OUTPUT_FILE); 236 237 if (isTextProtoConfig) { 238 perfettoCmd = perfettoCmd + PERFETTO_TXT_PROTO_ARG; 239 } 240 241 // Start perfetto tracing. 242 Log.i(LOG_TAG, "Starting perfetto tracing."); 243 startOutput = mUIDevice.executeShellCommand(perfettoCmd); 244 if (mTrackPerfettoPidFlag) { 245 // Persist perfetto pid in a file and use it for cleanup if the instrumentation 246 // crashes. 247 mPerfettoPidFile = writePidToFile(startOutput); 248 } 249 Log.i(LOG_TAG, String.format("Perfetto start command output - %s", startOutput)); 250 251 if (!canUpdateAfterStartCollecting(startOutput)) { 252 return false; 253 } 254 } catch (FileNotFoundException fnf) { 255 Log.e(LOG_TAG, "Unable to write perfetto process id to a file :" + fnf.getMessage()); 256 Log.i(LOG_TAG, "Stopping perfetto tracing because perfetto id is not tracked."); 257 try { 258 stopPerfetto(Integer.parseInt(startOutput.trim())); 259 } catch (IOException ie) { 260 Log.e(LOG_TAG, "Unable to stop perfetto process output file." + ie.getMessage()); 261 } 262 return false; 263 } catch (IOException ioe) { 264 Log.e(LOG_TAG, "Unable to start the perfetto tracing due to :" + ioe.getMessage()); 265 return false; 266 } 267 Log.i(LOG_TAG, "Perfetto tracing started successfully."); 268 return true; 269 } 270 canSetupBeforeStartCollecting()271 private boolean canSetupBeforeStartCollecting() throws IOException { 272 // Remove already existing temporary output trace file if any. 273 String output = 274 mUIDevice.executeShellCommand(String.format(REMOVE_CMD, PERFETTO_TMP_OUTPUT_FILE)); 275 Log.i(LOG_TAG, String.format("Perfetto output file cleanup - %s", output)); 276 277 // Create new temporary output trace file before tracing. 278 output = 279 mUIDevice.executeShellCommand( 280 String.format(CREATE_FILE_CMD, PERFETTO_TMP_OUTPUT_FILE)); 281 if (output.isEmpty()) { 282 Log.i(LOG_TAG, "Perfetto output file create success."); 283 } else { 284 Log.e(LOG_TAG, String.format("Unable to create Perfetto output file - %s", output)); 285 return false; 286 } 287 288 return true; 289 } 290 canUpdateAfterStartCollecting(String startOutput)291 private boolean canUpdateAfterStartCollecting(String startOutput) { 292 Log.i(LOG_TAG, String.format("Perfetto start command output - %s", startOutput)); 293 294 if (!startOutput.isEmpty()) { 295 mPerfettoProcId = Integer.parseInt(startOutput.trim()); 296 sPerfettoProcessIds.add(mPerfettoProcId); 297 Log.i( 298 LOG_TAG, 299 String.format("Perfetto process id %d added for tracking", mPerfettoProcId)); 300 } 301 302 // If the perfetto background wait option is not used then add a explicit wait after 303 // starting the perfetto trace. 304 if (!mPerfettoStartBgWait) { 305 SystemClock.sleep(1000); 306 } 307 308 if (!isTestPerfettoRunning(mPerfettoProcId)) { 309 return false; 310 } 311 312 return true; 313 } 314 315 /** 316 * Stop the perfetto trace collection and redirect the output to 317 * /data/misc/perfetto-traces/trace_output.perfetto-trace after waiting for given time in msecs 318 * and copy the output to the destination file. 319 * @param waitTimeInMsecs time to wait in msecs before stopping the trace collection. 320 * @param destinationFile file to copy the perfetto output trace. 321 * @return true if the trace collection is successfull otherwise false. 322 */ stopCollecting(long waitTimeInMsecs, String destinationFile)323 public boolean stopCollecting(long waitTimeInMsecs, String destinationFile) { 324 // Wait for the dump interval before stopping the trace. 325 Log.i(LOG_TAG, String.format( 326 "Waiting for %d msecs before stopping perfetto.", waitTimeInMsecs)); 327 SystemClock.sleep(waitTimeInMsecs); 328 329 // Stop the perfetto and copy the output file. 330 Log.i(LOG_TAG, "Stopping perfetto."); 331 try { 332 if (stopPerfetto(mPerfettoProcId)) { 333 if (!copyFileOutput(destinationFile)) { 334 return false; 335 } 336 } else { 337 Log.e(LOG_TAG, "Perfetto failed to stop."); 338 return false; 339 } 340 } catch (IOException ioe) { 341 Log.e(LOG_TAG, "Unable to stop the perfetto tracing due to " + ioe.getMessage()); 342 return false; 343 } 344 // Delete the perfetto process id file if the perfetto tracing successfully ended. 345 if (mTrackPerfettoPidFlag) { 346 if (mPerfettoPidFile.exists()) { 347 Log.i( 348 LOG_TAG, 349 String.format( 350 "Deleting Perfetto process id file %s .", 351 mPerfettoPidFile.toString())); 352 mPerfettoPidFile.delete(); 353 } 354 } 355 return true; 356 } 357 358 /** 359 * Utility method for stopping perfetto. 360 * 361 * @param perfettoProcId perfetto process id. 362 * @return true if perfetto is stopped successfully. 363 */ stopPerfetto(int perfettoProcId)364 public boolean stopPerfetto(int perfettoProcId) throws IOException { 365 Log.i(LOG_TAG, String.format("Killing the process id - %d", perfettoProcId)); 366 String stopOutput = 367 mUIDevice.executeShellCommand(String.format(PERFETTO_STOP_CMD, perfettoProcId)); 368 Log.i(LOG_TAG, String.format("Perfetto stop command output - %s", stopOutput)); 369 int waitCount = 0; 370 while (isTestPerfettoRunning(perfettoProcId)) { 371 // 60 secs timeout for perfetto shutdown. 372 if (waitCount < PERFETTO_KILL_WAIT_COUNT) { 373 // Check every 5 secs if perfetto stopped successfully. 374 SystemClock.sleep(PERFETTO_KILL_WAIT_TIME); 375 waitCount++; 376 continue; 377 } 378 Log.i(LOG_TAG, "Perfetto did not stop."); 379 return false; 380 } 381 Log.i(LOG_TAG, "Perfetto stopped successfully."); 382 boolean isRemoved = sPerfettoProcessIds.remove(perfettoProcId); 383 Log.i(LOG_TAG, String.format("Process id removed status %s", Boolean.toString(isRemoved))); 384 Log.i( 385 LOG_TAG, 386 String.format("Perfetto process id %d removed for tracking", perfettoProcId)); 387 return true; 388 } 389 390 /** 391 * Utility method for writing perfetto pid to a file. 392 * 393 * @param perfettoStartOutput perfetto process id. 394 * @return File with perfetto process id written in it. 395 */ writePidToFile(String perfettoStartOutput)396 private File writePidToFile(String perfettoStartOutput) 397 throws IOException, FileNotFoundException { 398 File perfettoPidFile = 399 new File( 400 String.format( 401 "%s%s%s.txt", 402 getTrackPerfettoRootDir(), 403 PERFETTO_PID_FILE_PREFIX, 404 System.currentTimeMillis())); 405 perfettoPidFile.createNewFile(); 406 try (PrintWriter out = new PrintWriter(perfettoPidFile)) { 407 out.println(perfettoStartOutput); 408 Log.i( 409 LOG_TAG, 410 String.format("Perfetto Process id file output %s", perfettoStartOutput)); 411 } 412 Log.i( 413 LOG_TAG, 414 String.format("Perfetto Process id file %s created.", perfettoPidFile.toString())); 415 return perfettoPidFile; 416 } 417 418 /** 419 * Stop all the perfetto process from the given set. 420 * 421 * @param processIds set of perfetto process ids. 422 * @return true if all the perfetto process is stopped otherwise false. 423 */ stopPerfettoProcesses(Set<Integer> processIds)424 public boolean stopPerfettoProcesses(Set<Integer> processIds) throws IOException { 425 boolean stopSuccess = true; 426 for (int processId : processIds) { 427 if (!stopPerfetto(processId)) { 428 Log.i( 429 LOG_TAG, 430 String.format("Failed to stop the perfetto process id - %d", processId)); 431 stopSuccess = false; 432 } else { 433 Log.i( 434 LOG_TAG, 435 String.format( 436 "Successfully stopped the perfetto process id - %d", processId)); 437 } 438 } 439 return stopSuccess; 440 } 441 442 /** 443 * Check if perfetto process is running or not. 444 * 445 * @param perfettoProcId perfetto process id. 446 * @return true if perfetto is running otherwise false. 447 */ isTestPerfettoRunning(int perfettoProcId)448 private boolean isTestPerfettoRunning(int perfettoProcId) { 449 try { 450 String perfettoProcStatus = 451 mUIDevice.executeShellCommand( 452 String.format(PERFETTO_PROC_ID_EXIST_CHECK, perfettoProcId)); 453 Log.i(LOG_TAG, String.format("Perfetto process id status check - %s", 454 perfettoProcStatus)); 455 // If proc details not empty then process is still running. 456 if (!perfettoProcStatus.isEmpty()) { 457 return true; 458 } 459 } catch (IOException ioe) { 460 Log.e(LOG_TAG, "Not able to check the perfetto status due to:" + ioe.getMessage()); 461 return false; 462 } 463 return false; 464 } 465 466 /** 467 * Copy the temporary perfetto trace output file from /data/misc/perfetto-traces/ to given 468 * destinationFile. 469 * 470 * @param destinationFile file to copy the perfetto output trace. 471 * @return true if the trace file copied successfully otherwise false. 472 */ copyFileOutput(String destinationFile)473 private boolean copyFileOutput(String destinationFile) { 474 Path path = Paths.get(destinationFile); 475 String destDirectory = path.getParent().toString(); 476 // Check if the directory already exists 477 File directory = new File(destDirectory); 478 if (!directory.exists()) { 479 boolean success = directory.mkdirs(); 480 if (!success) { 481 Log.e(LOG_TAG, String.format( 482 "Result output directory %s not created successfully.", destDirectory)); 483 return false; 484 } 485 } 486 487 // Copy the collected trace from /data/misc/perfetto-traces/trace_output.perfetto-trace to 488 // destinationFile 489 try { 490 String moveResult = mUIDevice.executeShellCommand(String.format( 491 MOVE_CMD, PERFETTO_TMP_OUTPUT_FILE, destinationFile)); 492 if (!moveResult.isEmpty()) { 493 Log.e(LOG_TAG, String.format( 494 "Unable to move perfetto output file from %s to %s due to %s", 495 PERFETTO_TMP_OUTPUT_FILE, destinationFile, moveResult)); 496 return false; 497 } 498 } catch (IOException ioe) { 499 Log.e(LOG_TAG, 500 "Unable to move the perfetto trace file to destination file." 501 + ioe.getMessage()); 502 return false; 503 } 504 return true; 505 } 506 setPerfettoConfigRootDir(String rootDir)507 public void setPerfettoConfigRootDir(String rootDir) { 508 mConfigRootDir = rootDir; 509 } 510 setPerfettoStartBgWait(boolean perfettoStartBgWait)511 public void setPerfettoStartBgWait(boolean perfettoStartBgWait) { 512 mPerfettoStartBgWait = perfettoStartBgWait; 513 } 514 getPerfettoPid()515 public int getPerfettoPid() { 516 return mPerfettoProcId; 517 } 518 getPerfettoPids()519 public Set<Integer> getPerfettoPids() { 520 return sPerfettoProcessIds; 521 } 522 setTrackPerfettoPidFlag(boolean trackPerfettoPidFlag)523 public void setTrackPerfettoPidFlag(boolean trackPerfettoPidFlag) { 524 mTrackPerfettoPidFlag = trackPerfettoPidFlag; 525 } 526 getTrackPerfettoPidFlag()527 public boolean getTrackPerfettoPidFlag() { 528 return mTrackPerfettoPidFlag; 529 } 530 setTrackPerfettoRootDir(String rootDir)531 public void setTrackPerfettoRootDir(String rootDir) { 532 mTrackPerfettoRootDir = rootDir; 533 } 534 getTrackPerfettoRootDir()535 public String getTrackPerfettoRootDir() { 536 return mTrackPerfettoRootDir; 537 } 538 getPerfettoPidFile()539 public File getPerfettoPidFile() { 540 return mPerfettoPidFile; 541 } 542 getPerfettoFilePrefix()543 public String getPerfettoFilePrefix() { 544 return PERFETTO_PID_FILE_PREFIX; 545 } 546 } 547