• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.simpleperf;
18 
19 import android.os.Build;
20 import android.system.Os;
21 import android.system.OsConstants;
22 
23 import android.support.annotation.NonNull;
24 import android.support.annotation.Nullable;
25 import android.support.annotation.RequiresApi;
26 
27 import java.io.BufferedReader;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.InputStreamReader;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.stream.Collectors;
36 
37 /**
38  * <p>
39  * This class uses `simpleperf record` cmd to generate a recording file.
40  * It allows users to start recording with some options, pause/resume recording
41  * to only profile interested code, and stop recording.
42  * </p>
43  *
44  * <p>
45  * Example:
46  *   RecordOptions options = new RecordOptions();
47  *   options.setDwarfCallGraph();
48  *   ProfileSession session = new ProfileSession();
49  *   session.StartRecording(options);
50  *   Thread.sleep(1000);
51  *   session.PauseRecording();
52  *   Thread.sleep(1000);
53  *   session.ResumeRecording();
54  *   Thread.sleep(1000);
55  *   session.StopRecording();
56  * </p>
57  *
58  * <p>
59  * It throws an Error when error happens. To read error messages of simpleperf record
60  * process, filter logcat with `simpleperf`.
61  * </p>
62  */
63 @RequiresApi(28)
64 public class ProfileSession {
65     private static final String SIMPLEPERF_PATH_IN_IMAGE = "/system/bin/simpleperf";
66 
67     enum State {
68         NOT_YET_STARTED,
69         STARTED,
70         PAUSED,
71         STOPPED,
72     }
73 
74     private State mState = State.NOT_YET_STARTED;
75     private final String mAppDataDir;
76     private String mSimpleperfPath;
77     private final String mSimpleperfDataDir;
78     private Process mSimpleperfProcess;
79     private boolean mTraceOffCpu = false;
80 
81     /**
82      * @param appDataDir the same as android.content.Context.getDataDir().
83      *                   ProfileSession stores profiling data in appDataDir/simpleperf_data/.
84      */
ProfileSession(@onNull String appDataDir)85     public ProfileSession(@NonNull String appDataDir) {
86         mAppDataDir = appDataDir;
87         mSimpleperfDataDir = appDataDir + "/simpleperf_data";
88     }
89 
90     /**
91      * ProfileSession assumes appDataDir as /data/data/app_package_name.
92      */
ProfileSession()93     public ProfileSession() {
94         String packageName;
95         try {
96             String s = readInputStream(new FileInputStream("/proc/self/cmdline"));
97             for (int i = 0; i < s.length(); i++) {
98                 if (s.charAt(i) == '\0') {
99                     s = s.substring(0, i);
100                     break;
101                 }
102             }
103             packageName = s;
104         } catch (IOException e) {
105             throw new Error("failed to find packageName: " + e.getMessage());
106         }
107         if (packageName.isEmpty()) {
108             throw new Error("failed to find packageName");
109         }
110         final int AID_USER_OFFSET = 100000;
111         int uid = Os.getuid();
112         if (uid >= AID_USER_OFFSET) {
113             int user_id = uid / AID_USER_OFFSET;
114             mAppDataDir = "/data/user/" + user_id + "/" + packageName;
115         } else {
116             mAppDataDir = "/data/data/" + packageName;
117         }
118         mSimpleperfDataDir = mAppDataDir + "/simpleperf_data";
119     }
120 
121     /**
122      * Start recording.
123      * @param options RecordOptions
124      */
startRecording(@onNull RecordOptions options)125     public void startRecording(@NonNull RecordOptions options) {
126         startRecording(options.toRecordArgs());
127     }
128 
129     /**
130      * Start recording.
131      * @param args arguments for `simpleperf record` cmd.
132      */
startRecording(@onNull List<String> args)133     public synchronized void startRecording(@NonNull List<String> args) {
134         if (mState != State.NOT_YET_STARTED) {
135             throw new IllegalStateException("startRecording: session in wrong state " + mState);
136         }
137         for (String arg : args) {
138             if (arg.equals("--trace-offcpu")) {
139                 mTraceOffCpu = true;
140             }
141         }
142         mSimpleperfPath = findSimpleperf();
143         checkIfPerfEnabled();
144         createSimpleperfDataDir();
145         createSimpleperfProcess(mSimpleperfPath, args);
146         mState = State.STARTED;
147     }
148 
149     /**
150      * Pause recording. No samples are generated in paused state.
151      */
pauseRecording()152     public synchronized void pauseRecording() {
153         if (mState != State.STARTED) {
154             throw new IllegalStateException("pauseRecording: session in wrong state " + mState);
155         }
156         if (mTraceOffCpu) {
157             throw new AssertionError(
158                     "--trace-offcpu option doesn't work well with pause/resume recording");
159         }
160         sendCmd("pause");
161         mState = State.PAUSED;
162     }
163 
164     /**
165      * Resume a paused session.
166      */
resumeRecording()167     public synchronized void resumeRecording() {
168         if (mState != State.PAUSED) {
169             throw new IllegalStateException("resumeRecording: session in wrong state " + mState);
170         }
171         sendCmd("resume");
172         mState = State.STARTED;
173     }
174 
175     /**
176      * Stop recording and generate a recording file under appDataDir/simpleperf_data/.
177      */
stopRecording()178     public synchronized void stopRecording() {
179         if (mState != State.STARTED && mState != State.PAUSED) {
180             throw new IllegalStateException("stopRecording: session in wrong state " + mState);
181         }
182         if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P + 1
183                 && mSimpleperfPath.equals(SIMPLEPERF_PATH_IN_IMAGE)) {
184             // The simpleperf shipped on Android Q contains a bug, which may make it abort if
185             // calling simpleperfProcess.destroy().
186             destroySimpleperfProcessWithoutClosingStdin();
187         } else {
188             mSimpleperfProcess.destroy();
189         }
190         try {
191             int exitCode = mSimpleperfProcess.waitFor();
192             if (exitCode != 0) {
193                 throw new AssertionError("simpleperf exited with error: " + exitCode);
194             }
195         } catch (InterruptedException e) {
196         }
197         mSimpleperfProcess = null;
198         mState = State.STOPPED;
199     }
200 
destroySimpleperfProcessWithoutClosingStdin()201     private void destroySimpleperfProcessWithoutClosingStdin() {
202         // In format "Process[pid=? ..."
203         String s = mSimpleperfProcess.toString();
204         final String prefix = "Process[pid=";
205         if (s.startsWith(prefix)) {
206             int startIndex = prefix.length();
207             int endIndex = s.indexOf(',');
208             if (endIndex > startIndex) {
209                 int pid = Integer.parseInt(s.substring(startIndex, endIndex).trim());
210                 android.os.Process.sendSignal(pid, OsConstants.SIGTERM);
211                 return;
212             }
213         }
214         mSimpleperfProcess.destroy();
215     }
216 
readInputStream(InputStream in)217     private String readInputStream(InputStream in) {
218         BufferedReader reader = new BufferedReader(new InputStreamReader(in));
219         String result = reader.lines().collect(Collectors.joining("\n"));
220         try {
221             reader.close();
222         } catch (IOException e) {
223         }
224         return result;
225     }
226 
findSimpleperf()227     private String findSimpleperf() {
228         // 1. Try /data/local/tmp/simpleperf. Probably it's newer than /system/bin/simpleperf.
229         String simpleperfPath = findSimpleperfInTempDir();
230         if (simpleperfPath != null) {
231             return simpleperfPath;
232         }
233         // 2. Try /system/bin/simpleperf, which is available on Android >= Q.
234         simpleperfPath = SIMPLEPERF_PATH_IN_IMAGE;
235         if (isExecutableFile(simpleperfPath)) {
236             return simpleperfPath;
237         }
238         throw new Error("can't find simpleperf on device. Please run api_profiler.py.");
239     }
240 
isExecutableFile(@onNull String path)241     private boolean isExecutableFile(@NonNull String path) {
242         File file = new File(path);
243         return file.canExecute();
244     }
245 
246     @Nullable
findSimpleperfInTempDir()247     private String findSimpleperfInTempDir() {
248         String path = "/data/local/tmp/simpleperf";
249         File file = new File(path);
250         if (!file.isFile()) {
251             return null;
252         }
253         // Copy it to app dir to execute it.
254         String toPath = mAppDataDir + "/simpleperf";
255         try {
256             Process process = new ProcessBuilder()
257                     .command("cp", path, toPath).start();
258             process.waitFor();
259         } catch (Exception e) {
260             return null;
261         }
262         if (!isExecutableFile(toPath)) {
263             return null;
264         }
265         // For apps with target sdk >= 29, executing app data file isn't allowed.
266         // For android R, app context isn't allowed to use perf_event_open.
267         // So test executing downloaded simpleperf.
268         try {
269             Process process = new ProcessBuilder().command(toPath, "list", "sw").start();
270             process.waitFor();
271             String data = readInputStream(process.getInputStream());
272             if (!data.contains("cpu-clock")) {
273                 return null;
274             }
275         } catch (Exception e) {
276             return null;
277         }
278         return toPath;
279     }
280 
checkIfPerfEnabled()281     private void checkIfPerfEnabled() {
282         if (getProperty("persist.simpleperf.profile_app_uid").equals("" + Os.getuid())) {
283             String timeStr = getProperty("persist.simpleperf.profile_app_expiration_time");
284             if (!timeStr.isEmpty()) {
285                 try {
286                     long expirationTime = Long.parseLong(timeStr);
287                     if (expirationTime > System.currentTimeMillis() / 1000) {
288                         return;
289                     }
290                 } catch (NumberFormatException e) {
291                 }
292             }
293         }
294         if (getProperty("security.perf_harden") == "1") {
295             throw new Error("Recording app isn't enabled on the device."
296                     + " Please run api_profiler.py.");
297         }
298     }
299 
getProperty(String name)300     private String getProperty(String name) {
301         String value;
302         Process process;
303         try {
304             process = new ProcessBuilder()
305                     .command("/system/bin/getprop", name).start();
306         } catch (IOException e) {
307             return "";
308         }
309         try {
310             process.waitFor();
311         } catch (InterruptedException e) {
312         }
313         return readInputStream(process.getInputStream());
314     }
315 
createSimpleperfDataDir()316     private void createSimpleperfDataDir() {
317         File file = new File(mSimpleperfDataDir);
318         if (!file.isDirectory()) {
319             file.mkdir();
320         }
321     }
322 
createSimpleperfProcess(String simpleperfPath, List<String> recordArgs)323     private void createSimpleperfProcess(String simpleperfPath, List<String> recordArgs) {
324         // 1. Prepare simpleperf arguments.
325         ArrayList<String> args = new ArrayList<>();
326         args.add(simpleperfPath);
327         args.add("record");
328         args.add("--log-to-android-buffer");
329         args.add("--log");
330         args.add("debug");
331         args.add("--stdio-controls-profiling");
332         args.add("--in-app");
333         args.add("--tracepoint-events");
334         args.add("/data/local/tmp/tracepoint_events");
335         args.addAll(recordArgs);
336 
337         // 2. Create the simpleperf process.
338         ProcessBuilder pb = new ProcessBuilder(args).directory(new File(mSimpleperfDataDir));
339         try {
340             mSimpleperfProcess = pb.start();
341         } catch (IOException e) {
342             throw new Error("failed to create simpleperf process: " + e.getMessage());
343         }
344 
345         // 3. Wait until simpleperf starts recording.
346         String startFlag = readReply();
347         if (!startFlag.equals("started")) {
348             throw new Error("failed to receive simpleperf start flag");
349         }
350     }
351 
sendCmd(@onNull String cmd)352     private void sendCmd(@NonNull String cmd) {
353         cmd += "\n";
354         try {
355             mSimpleperfProcess.getOutputStream().write(cmd.getBytes());
356             mSimpleperfProcess.getOutputStream().flush();
357         } catch (IOException e) {
358             throw new Error("failed to send cmd to simpleperf: " + e.getMessage());
359         }
360         if (!readReply().equals("ok")) {
361             throw new Error("failed to run cmd in simpleperf: " + cmd);
362         }
363     }
364 
365     @NonNull
readReply()366     private String readReply() {
367         // Read one byte at a time to stop at line break or EOF. BufferedReader will try to read
368         // more than available and make us blocking, so don't use it.
369         String s = "";
370         while (true) {
371             int c = -1;
372             try {
373                 c = mSimpleperfProcess.getInputStream().read();
374             } catch (IOException e) {
375             }
376             if (c == -1 || c == '\n') {
377                 break;
378             }
379             s += (char) c;
380         }
381         return s;
382     }
383 }
384