• 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