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