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