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.os.SystemClock;
20 import android.util.Log;
21 
22 import androidx.test.InstrumentationRegistry;
23 import androidx.test.uiautomator.UiDevice;
24 
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 
30 /**
31  * PerfettoHelper is used to start and stop the perfetto tracing and move the
32  * output perfetto trace file to destination folder.
33  */
34 public class PerfettoHelper {
35 
36     private static final String LOG_TAG = PerfettoHelper.class.getSimpleName();
37     // Command to start the perfetto tracing in the background.
38     // perfetto -b -c /data/misc/perfetto-traces/trace_config.pb -o
39     // /data/misc/perfetto-traces/trace_output.pb
40     private static final String PERFETTO_START_CMD = "perfetto --background -c %s%s -o %s";
41     private static final String PERFETTO_TMP_OUTPUT_FILE =
42             "/data/misc/perfetto-traces/trace_output.pb";
43     // Additional arg to indicate that the perfetto config file is text format.
44     private static final String PERFETTO_TXT_PROTO_ARG = " --txt";
45     // Command to stop (i.e kill) the perfetto tracing.
46     private static final String PERFETTO_STOP_CMD = "pkill -INT perfetto";
47     // Command to check the perfetto process id.
48     private static final String PERFETTO_PROC_ID_CMD = "pidof perfetto";
49     // Remove the trace output file /data/misc/perfetto-traces/trace_output.pb
50     private static final String REMOVE_CMD = "rm %s";
51     // Command to move the perfetto output trace file to given folder.
52     private static final String MOVE_CMD = "mv %s %s";
53     // Max wait count for checking if perfetto is stopped successfully
54     private static final int PERFETTO_KILL_WAIT_COUNT = 12;
55     // Check if perfetto is stopped every 5 secs.
56     private static final long PERFETTO_KILL_WAIT_TIME = 5000;
57 
58     private UiDevice mUIDevice;
59 
60     private String mConfigRootDir;
61 
62     /**
63      * Start the perfetto tracing in background using the given config file and write the ouput to
64      * /data/misc/perfetto-traces/trace_output.pb. Perfetto has access only to
65      * /data/misc/perfetto-traces/ folder. So the config file has to be under
66      * /data/misc/perfetto-traces/ folder in the device.
67      *
68      * @param configFileName used for collecting the perfetto trace.
69      * @param isTextProtoConfig true if the config file is textproto format otherwise false.
70      * @return true if trace collection started successfully otherwise return false.
71      */
startCollecting(String configFileName, boolean isTextProtoConfig)72     public boolean startCollecting(String configFileName, boolean isTextProtoConfig) {
73         mUIDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
74         if (configFileName == null || configFileName.isEmpty()) {
75             Log.e(LOG_TAG, "Perfetto config file name is null or empty.");
76             return false;
77         }
78 
79         if (mConfigRootDir == null || mConfigRootDir.isEmpty()) {
80             Log.e(LOG_TAG, "Perfetto trace config root directory name is null or empty.");
81             return false;
82         }
83 
84         try {
85             // Cleanup already existing perfetto process.
86             Log.i(LOG_TAG, "Cleanup perfetto before starting.");
87             if (isPerfettoRunning()) {
88                 Log.i(LOG_TAG, "Perfetto tracing is already running. Stopping perfetto.");
89                 if (!stopPerfetto()) {
90                     return false;
91                 }
92             }
93 
94             // Remove already existing temporary output trace file if any.
95             String output = mUIDevice.executeShellCommand(String.format(REMOVE_CMD,
96                     PERFETTO_TMP_OUTPUT_FILE));
97             Log.i(LOG_TAG, String.format("Perfetto output file cleanup - %s", output));
98 
99             String perfettoCmd = String.format(PERFETTO_START_CMD,
100                     mConfigRootDir, configFileName, PERFETTO_TMP_OUTPUT_FILE);
101 
102             if(isTextProtoConfig) {
103                perfettoCmd = perfettoCmd + PERFETTO_TXT_PROTO_ARG;
104             }
105 
106             // Start perfetto tracing.
107             Log.i(LOG_TAG, "Starting perfetto tracing.");
108             String startOutput = mUIDevice.executeShellCommand(perfettoCmd);
109             Log.i(LOG_TAG, String.format("Perfetto start command output - %s", startOutput));
110             // TODO : Once the output status is available use that for additional validation.
111             if (!isPerfettoRunning()) {
112                 Log.e(LOG_TAG, "Perfetto tracing failed to start.");
113                 return false;
114             }
115         } catch (IOException ioe) {
116             Log.e(LOG_TAG, "Unable to start the perfetto tracing due to :" + ioe.getMessage());
117             return false;
118         }
119         Log.i(LOG_TAG, "Perfetto tracing started successfully.");
120         return true;
121     }
122 
123     /**
124      * Stop the perfetto trace collection under /data/misc/perfetto-traces/trace_output.pb after
125      * waiting for given time in msecs and copy the output to the destination file.
126      *
127      * @param waitTimeInMsecs time to wait in msecs before stopping the trace collection.
128      * @param destinationFile file to copy the perfetto output trace.
129      * @return true if the trace collection is successfull otherwise false.
130      */
stopCollecting(long waitTimeInMsecs, String destinationFile)131     public boolean stopCollecting(long waitTimeInMsecs, String destinationFile) {
132         // Wait for the dump interval before stopping the trace.
133         Log.i(LOG_TAG, String.format(
134                 "Waiting for %d msecs before stopping perfetto.", waitTimeInMsecs));
135         SystemClock.sleep(waitTimeInMsecs);
136 
137         // Stop the perfetto and copy the output file.
138         Log.i(LOG_TAG, "Stopping perfetto.");
139         try {
140             if (stopPerfetto()) {
141                 if (!copyFileOutput(destinationFile)) {
142                     return false;
143                 }
144             } else {
145                 Log.e(LOG_TAG, "Perfetto failed to stop.");
146                 return false;
147             }
148         } catch (IOException ioe) {
149             Log.e(LOG_TAG, "Unable to stop the perfetto tracing due to " + ioe.getMessage());
150             return false;
151         }
152         return true;
153     }
154 
155     /**
156      * Utility method for stopping perfetto.
157      *
158      * @return true if perfetto is stopped successfully.
159      */
stopPerfetto()160     public boolean stopPerfetto() throws IOException {
161         String stopOutput = mUIDevice.executeShellCommand(PERFETTO_STOP_CMD);
162         Log.i(LOG_TAG, String.format("Perfetto stop command output - %s", stopOutput));
163         int waitCount = 0;
164         while (isPerfettoRunning()) {
165             // 60 secs timeout for perfetto shutdown.
166             if (waitCount < PERFETTO_KILL_WAIT_COUNT) {
167                 // Check every 5 secs if perfetto stopped successfully.
168                 SystemClock.sleep(PERFETTO_KILL_WAIT_TIME);
169                 waitCount++;
170                 continue;
171             }
172             return false;
173         }
174         Log.e(LOG_TAG, "Perfetto stopped successfully.");
175         return true;
176     }
177 
178     /**
179      * Check if perfetto process is running or not.
180      *
181      * @return true if perfetto is running otherwise false.
182      */
isPerfettoRunning()183     private boolean isPerfettoRunning() {
184         try {
185             String perfettoProcId = mUIDevice.executeShellCommand(PERFETTO_PROC_ID_CMD);
186             Log.i(LOG_TAG, String.format("Perfetto process id - %s", perfettoProcId));
187             if (perfettoProcId.isEmpty()) {
188                 return false;
189             }
190         } catch (IOException ioe) {
191             Log.e(LOG_TAG, "Not able to check the perfetto status due to:" + ioe.getMessage());
192             return false;
193         }
194         return true;
195     }
196 
197     /**
198      * Copy the temporary perfetto trace output file from /data/misc/perfetto-traces/ to given
199      * destinationFile.
200      *
201      * @param destinationFile file to copy the perfetto output trace.
202      * @return true if the trace file copied successfully otherwise false.
203      */
copyFileOutput(String destinationFile)204     private boolean copyFileOutput(String destinationFile) {
205         Path path = Paths.get(destinationFile);
206         String destDirectory = path.getParent().toString();
207         // Check if the directory already exists
208         File directory = new File(destDirectory);
209         if (!directory.exists()) {
210             boolean success = directory.mkdirs();
211             if (!success) {
212                 Log.e(LOG_TAG, String.format(
213                         "Result output directory %s not created successfully.", destDirectory));
214                 return false;
215             }
216         }
217 
218         // Copy the collected trace from /data/misc/perfetto-traces/trace_output.pb to
219         // destinationFile
220         try {
221             String moveResult = mUIDevice.executeShellCommand(String.format(
222                     MOVE_CMD, PERFETTO_TMP_OUTPUT_FILE, destinationFile));
223             if (!moveResult.isEmpty()) {
224                 Log.e(LOG_TAG, String.format(
225                         "Unable to move perfetto output file from %s to %s due to %s",
226                         PERFETTO_TMP_OUTPUT_FILE, destinationFile, moveResult));
227                 return false;
228             }
229         } catch (IOException ioe) {
230             Log.e(LOG_TAG,
231                     "Unable to move the perfetto trace file to destination file."
232                             + ioe.getMessage());
233             return false;
234         }
235         return true;
236     }
237 
setPerfettoConfigRootDir(String rootDir)238     public void setPerfettoConfigRootDir(String rootDir) {
239         mConfigRootDir = rootDir;
240     }
241 }
242