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