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