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