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