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