Simpleperf method sampling - initial support
Bug: 158303822
Test: ./gradlew benchmark:b-c:cC benchmark:b-j:cC
Test: ProfilerTest
Remaining simpleperf work to do:
- Additionally spit out .trace file for opening in Studio
- Use additionalOutputDir
- Enable this mode by default on supported devices (when methodSampling is selected)
- Avoid need for python prepare script (can just call ADB directly)
Additionally adds args for sample freq, and sampling duration.
The 'simpleperf' package is copied from the simpleperf app_api sample:
https://cs.android.com/android/platform/superproject/+/master:system/extras/simpleperf/app_api/
Change-Id: I5ccef4b434710ae6561c63c5ae1cd5d87ed77b9f
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
index 83788d9..e259ced 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
@@ -16,6 +16,8 @@
package androidx.benchmark
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -24,7 +26,6 @@
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
-import kotlin.test.fail
@SmallTest
@RunWith(JUnit4::class)
@@ -35,6 +36,7 @@
assertSame(MethodTracing, Profiler.getByName("MethodTracing"))
assertSame(ConnectedAllocation, Profiler.getByName("ConnectedAllocation"))
assertSame(ConnectedSampling, Profiler.getByName("ConnectedSampling"))
+ assertSame(MethodSamplingSimpleperf, Profiler.getByName("MethodSamplingSimpleperf"))
// Compat names
assertSame(MethodTracing, Profiler.getByName("Method"))
@@ -42,25 +44,16 @@
assertSame(ConnectedSampling, Profiler.getByName("ConnectedSampled"))
}
- private fun testRuntimeProfiler(profiler: Profiler) {
- assertTrue(profiler.requiresLibraryOutputDir)
-
- val traceFileName = "test"
- val traceType = when (profiler) {
- MethodTracing -> "methodTracing"
- MethodSampling -> "methodSampling"
- else -> fail("Profiler not supported by test")
- }
- val file = File(
- Arguments.testOutputDir,
- "$traceFileName-$traceType.trace"
- )
+ private fun verifyProfiler(
+ profiler: Profiler,
+ file: File
+ ) {
val deletedSuccessfully: Boolean
try {
file.delete() // clean up, if previous run left this behind
assertFalse(file.exists())
- profiler.start(traceFileName)
+ profiler.start("test")
profiler.stop()
assertTrue(file.exists(), "Profiler should create: ${file.absolutePath}")
} finally {
@@ -70,8 +63,22 @@
}
@Test
- fun methodSampling() = testRuntimeProfiler(MethodSampling)
+ fun methodSampling() = verifyProfiler(
+ profiler = MethodSampling,
+ file = File(Arguments.testOutputDir, "test-methodSampling.trace")
+ )
@Test
- fun methodTracing() = testRuntimeProfiler(MethodTracing)
+ fun methodTracing() = verifyProfiler(
+ profiler = MethodTracing,
+ file = File(Arguments.testOutputDir, "test-methodTracing.trace")
+ )
+
+ @FlakyTest // temporarily disabled in CI, since this currently requires external script setup
+ @SdkSuppress(minSdkVersion = 28)
+ @Test
+ fun methodSamplingSimpleperf() = verifyProfiler(
+ profiler = MethodSamplingSimpleperf,
+ file = File("/data/data/androidx.benchmark.test/simpleperf_data/test.data")
+ )
}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
index dcc2dd6..6463ac5 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
@@ -18,6 +18,7 @@
import android.os.Bundle
import android.os.Environment
+import android.util.Log
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
@@ -37,6 +38,8 @@
val dryRunMode: Boolean
val suppressedErrors: Set<String>
val profiler: Profiler?
+ val profilerSampleFrequency: Int
+ val profilerSampleDurationSeconds: Long
var error: String? = null
@@ -81,6 +84,16 @@
.toSet()
profiler = arguments.getProfiler(outputEnable)
+ profilerSampleFrequency =
+ arguments.getArgument("profiling.sampleFrequency")?.ifBlank { null }?.toInt() ?: 10000
+ profilerSampleDurationSeconds =
+ arguments.getArgument("profiling.sampleDurationSeconds")?.ifBlank { null }?.toLong()
+ ?: 5
+
+ if (profiler != null) {
+ Log.d(BenchmarkState.TAG, "Profiler ${profiler.javaClass.simpleName}, freq " +
+ "$profilerSampleFrequency, duration $profilerSampleDurationSeconds")
+ }
val additionalTestOutputDir = arguments.getString("additionalTestOutputDir")
@Suppress("DEPRECATION") // Legacy code path for versions of agp older than 3.6
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
index 9bf2427..b92c007 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -557,7 +557,8 @@
internal val REPEAT_DURATION_TARGET_NS = when (Arguments.profiler?.requiresExtraRuntime) {
// longer measurements while profiling to ensure we have enough data
true -> TimeUnit.MILLISECONDS.toNanos(50)
- else -> TimeUnit.MICROSECONDS.toNanos(500)
+ else -> TimeUnit.SECONDS.toNanos(Arguments.profilerSampleDurationSeconds) /
+ REPEAT_COUNT_TIME
}
internal const val MAX_TEST_ITERATIONS = 1_000_000
internal const val MIN_TEST_ITERATIONS = 1
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Errors.kt b/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
index d69eb74..a67d6de 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
@@ -178,6 +178,33 @@
| = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
""".trimMarginWrapNewlines()
}
+ if (Arguments.profiler == MethodSamplingSimpleperf) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ warningPrefix += "SIMPLEPERF_"
+ warningString += """
+ |ERROR: Cannot use Simpleperf on this device's API level (${Build.VERSION.SDK_INT})
+ | Simpleperf prior to API 28 (P) requires AOT compilation, and isn't
+ | currently supported by the benchmark library.
+ """.trimMarginWrapNewlines()
+ } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && !isDeviceRooted) {
+ warningPrefix += "SIMPLEPERF_"
+ warningString += """
+ |ERROR: Cannot use Simpleperf on this device's API level (${Build.VERSION.SDK_INT})
+ | without root. Simpleperf on API 28 (P) can only be used on a rooted device,
+ | or when the APK is debuggable. Debuggable performance measurements should
+ | be avoided, due to measurement inaccuracy.
+ """.trimMarginWrapNewlines()
+ } else if (
+ Build.VERSION.SDK_INT >= 29 && !context.applicationInfo.isProfileableByShell
+ ) {
+ warningPrefix += "SIMPLEPERF_"
+ warningString += """
+ |ERROR: Apk must be profileable to use simpleperf.
+ | ensure you put <profileable android:shell="true"/> within the
+ | <application ...> tag of your benchmark module
+ """.trimMarginWrapNewlines()
+ }
+ }
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryPercent = context.registerReceiver(null, filter)?.run {
@@ -216,7 +243,8 @@
.toSet()
val alwaysSuppressed = setOf("PROFILED")
- val suppressedWarnings = Arguments.suppressedErrors + alwaysSuppressed
+ val neverSuppressed = setOf("SIMPLEPERF")
+ val suppressedWarnings = Arguments.suppressedErrors + alwaysSuppressed - neverSuppressed
val unsuppressedWarningSet = warningSet - suppressedWarnings
UNSUPPRESSED_WARNING_MESSAGE = if (unsuppressedWarningSet.isNotEmpty()) {
"""
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
index 820bcda..e22268a 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
@@ -19,6 +19,9 @@
import android.os.Build
import android.os.Debug
import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.benchmark.simpleperf.ProfileSession
+import androidx.benchmark.simpleperf.RecordOptions
import java.io.File
/**
@@ -72,6 +75,8 @@
"ConnectedAllocation" to ConnectedAllocation,
"ConnectedSampling" to ConnectedSampling,
+ "MethodSamplingSimpleperf" to MethodSamplingSimpleperf,
+
// Below are compat codepaths for old names. Remove before 1.1 stable.
"Method" to MethodTracing,
"Sampled" to MethodSampling,
@@ -81,12 +86,8 @@
}
}
-internal fun startRuntimeMethodTracing(traceUniqueName: String, sampled: Boolean) {
- val traceType = if (sampled) "methodSampling" else "methodTracing"
- val path = File(
- Arguments.testOutputDir,
- "$traceUniqueName-$traceType.trace"
- ).absolutePath
+internal fun startRuntimeMethodTracing(traceFileName: String, sampled: Boolean) {
+ val path = File(Arguments.testOutputDir, traceFileName).absolutePath
Log.d(BenchmarkState.TAG, "Profiling output file: $path")
InstrumentationResults.reportAdditionalFileToCopy("profiling_trace", path)
@@ -95,7 +96,7 @@
if (sampled &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
) {
- Debug.startMethodTracingSampling(path, bufferSize, 100)
+ Debug.startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)
} else {
Debug.startMethodTracing(path, bufferSize, 0)
}
@@ -107,7 +108,10 @@
internal object MethodSampling : Profiler() {
override fun start(traceUniqueName: String) {
- startRuntimeMethodTracing(traceUniqueName = traceUniqueName, sampled = true)
+ startRuntimeMethodTracing(
+ traceFileName = "$traceUniqueName-methodSampling.trace",
+ sampled = true
+ )
}
override fun stop() {
@@ -119,7 +123,10 @@
internal object MethodTracing : Profiler() {
override fun start(traceUniqueName: String) {
- startRuntimeMethodTracing(traceUniqueName = traceUniqueName, sampled = false)
+ startRuntimeMethodTracing(
+ traceFileName = "$traceUniqueName-methodTracing.trace",
+ sampled = false
+ )
}
override fun stop() {
@@ -154,4 +161,31 @@
override val requiresDebuggable: Boolean = true
override val requiresLibraryOutputDir: Boolean = false
+}
+
+internal object MethodSamplingSimpleperf : Profiler() {
+ @RequiresApi(28)
+ private var session: ProfileSession? = null
+
+ @RequiresApi(28)
+ override fun start(traceUniqueName: String) {
+ session?.stopRecording() // stop previous
+ session = ProfileSession().also {
+ it.startRecording(
+ RecordOptions()
+ .setSampleFrequency(Arguments.profilerSampleFrequency)
+ .recordDwarfCallGraph() // enable Java/Kotlin callstacks
+ .traceOffCpu() // track time sleeping
+ .setOutputFilename("$traceUniqueName.data")
+ )
+ }
+ }
+
+ @RequiresApi(28)
+ override fun stop() {
+ session!!.stopRecording()
+ session = null
+ }
+
+ override val requiresLibraryOutputDir: Boolean = false
}
\ No newline at end of file
diff --git a/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
new file mode 100644
index 0000000..258d710
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.simpleperf;
+
+import android.os.Build;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * This class uses `simpleperf record` cmd to generate a recording file.
+ * It allows users to start recording with some options, pause/resume recording
+ * to only profile interested code, and stop recording.
+ * </p>
+ *
+ * <p>
+ * Example:
+ * RecordOptions options = new RecordOptions();
+ * options.setDwarfCallGraph();
+ * ProfileSession session = new ProfileSession();
+ * session.StartRecording(options);
+ * Thread.sleep(1000);
+ * session.PauseRecording();
+ * Thread.sleep(1000);
+ * session.ResumeRecording();
+ * Thread.sleep(1000);
+ * session.StopRecording();
+ * </p>
+ *
+ * <p>
+ * It throws an Error when error happens. To read error messages of simpleperf record
+ * process, filter logcat with `simpleperf`.
+ * </p>
+ *
+ * NOTE: copied from
+ * https://cs.android.com/android/platform/superproject/+/master:system/extras/simpleperf/app_api/
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ProfileSession {
+ private static final String SIMPLEPERF_PATH_IN_IMAGE = "/system/bin/simpleperf";
+
+ enum State {
+ NOT_YET_STARTED,
+ STARTED,
+ PAUSED,
+ STOPPED,
+ }
+
+ private State mState = State.NOT_YET_STARTED;
+ private String mAppDataDir;
+ private String mSimpleperfPath;
+ private String mSimpleperfDataDir;
+ private Process mSimpleperfProcess;
+ private boolean mTraceOffCpu = false;
+
+ /**
+ * @param appDataDir the same as android.content.Context.getDataDir().
+ * ProfileSession stores profiling data in appDataDir/simpleperf_data/.
+ */
+ public ProfileSession(@NonNull String appDataDir) {
+ mAppDataDir = appDataDir;
+ mSimpleperfDataDir = appDataDir + "/simpleperf_data";
+ }
+
+ /**
+ * ProfileSession assumes appDataDir as /data/data/app_package_name.
+ */
+ public ProfileSession() {
+ String packageName;
+ try {
+ String s = readInputStream(new FileInputStream("/proc/self/cmdline"));
+ for (int i = 0; i < s.length(); i++) {
+ if (s.charAt(i) == '\0') {
+ s = s.substring(0, i);
+ break;
+ }
+ }
+ packageName = s;
+ } catch (IOException e) {
+ throw new Error("failed to find packageName: " + e.getMessage());
+ }
+ if (packageName.isEmpty()) {
+ throw new Error("failed to find packageName");
+ }
+ mAppDataDir = "/data/data/" + packageName;
+ mSimpleperfDataDir = mAppDataDir + "/simpleperf_data";
+ }
+
+ /**
+ * Start recording.
+ * @param options RecordOptions
+ */
+ public void startRecording(@NonNull RecordOptions options) {
+ startRecording(options.toRecordArgs());
+ }
+
+ /**
+ * Start recording.
+ * @param args arguments for `simpleperf record` cmd.
+ */
+ public synchronized void startRecording(@NonNull List<String> args) {
+ if (mState != State.NOT_YET_STARTED) {
+ throw new AssertionError("startRecording: session in wrong state " + mState);
+ }
+ for (String arg : args) {
+ if (arg.equals("--trace-offcpu")) {
+ mTraceOffCpu = true;
+ }
+ }
+ mSimpleperfPath = findSimpleperf();
+ checkIfPerfEnabled();
+ createSimpleperfDataDir();
+ createSimpleperfProcess(mSimpleperfPath, args);
+ mState = State.STARTED;
+ }
+
+ /**
+ * Pause recording. No samples are generated in paused state.
+ */
+ public synchronized void pauseRecording() {
+ if (mState != State.STARTED) {
+ throw new AssertionError("pauseRecording: session in wrong state " + mState);
+ }
+ if (mTraceOffCpu) {
+ throw new AssertionError(
+ "--trace-offcpu option doesn't work well with pause/resume recording");
+ }
+ sendCmd("pause");
+ mState = State.PAUSED;
+ }
+
+ /**
+ * Resume a paused session.
+ */
+ public synchronized void resumeRecording() {
+ if (mState != State.PAUSED) {
+ throw new AssertionError("resumeRecording: session in wrong state " + mState);
+ }
+ sendCmd("resume");
+ mState = State.STARTED;
+ }
+
+ /**
+ * Stop recording and generate a recording file under appDataDir/simpleperf_data/.
+ */
+ public synchronized void stopRecording() {
+ if (mState != State.STARTED && mState != State.PAUSED) {
+ throw new AssertionError("stopRecording: session in wrong state " + mState);
+ }
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P + 1
+ && mSimpleperfPath.equals(SIMPLEPERF_PATH_IN_IMAGE)) {
+ // The simpleperf shipped on Android Q contains a bug, which may make it abort if
+ // calling simpleperfProcess.destroy().
+ destroySimpleperfProcessWithoutClosingStdin();
+ } else {
+ mSimpleperfProcess.destroy();
+ }
+ try {
+ int exitCode = mSimpleperfProcess.waitFor();
+ if (exitCode != 0) {
+ throw new AssertionError("simpleperf exited with error: " + exitCode);
+ }
+ } catch (InterruptedException e) {
+ }
+ mSimpleperfProcess = null;
+ mState = State.STOPPED;
+ }
+
+ private void destroySimpleperfProcessWithoutClosingStdin() {
+ // In format "Process[pid=? ..."
+ String s = mSimpleperfProcess.toString();
+ final String prefix = "Process[pid=";
+ if (s.startsWith(prefix)) {
+ int startIndex = prefix.length();
+ int endIndex = s.indexOf(',');
+ if (endIndex > startIndex) {
+ int pid = Integer.parseInt(s.substring(startIndex, endIndex).trim());
+ android.os.Process.sendSignal(pid, OsConstants.SIGTERM);
+ return;
+ }
+ }
+ mSimpleperfProcess.destroy();
+ }
+
+ private String readInputStream(InputStream in) {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ String result = reader.lines().collect(Collectors.joining("\n"));
+ try {
+ reader.close();
+ } catch (IOException e) {
+ }
+ return result;
+ }
+
+ private String findSimpleperf() {
+ // 1. Try /data/local/tmp/simpleperf. Probably it's newer than /system/bin/simpleperf.
+ String simpleperfPath = findSimpleperfInTempDir();
+ if (simpleperfPath != null) {
+ return simpleperfPath;
+ }
+ // 2. Try /system/bin/simpleperf, which is available on Android >= Q.
+ simpleperfPath = SIMPLEPERF_PATH_IN_IMAGE;
+ if (isExecutableFile(simpleperfPath)) {
+ return simpleperfPath;
+ }
+ throw new Error("can't find simpleperf on device. Please run api_profiler.py.");
+ }
+
+ private boolean isExecutableFile(@NonNull String path) {
+ File file = new File(path);
+ return file.canExecute();
+ }
+
+ @Nullable
+ private String findSimpleperfInTempDir() {
+ String path = "/data/local/tmp/simpleperf";
+ File file = new File(path);
+ if (!file.isFile()) {
+ return null;
+ }
+ // Copy it to app dir to execute it.
+ String toPath = mAppDataDir + "/simpleperf";
+ try {
+ Process process = new ProcessBuilder()
+ .command("cp", path, toPath).start();
+ process.waitFor();
+ } catch (Exception e) {
+ return null;
+ }
+ if (!isExecutableFile(toPath)) {
+ return null;
+ }
+ // For apps with target sdk >= 29, executing app data file isn't allowed.
+ // For android R, app context isn't allowed to use perf_event_open.
+ // So test executing downloaded simpleperf.
+ try {
+ Process process = new ProcessBuilder().command(toPath, "list", "sw").start();
+ process.waitFor();
+ String data = readInputStream(process.getInputStream());
+ if (!data.contains("cpu-clock")) {
+ return null;
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ return toPath;
+ }
+
+ private void checkIfPerfEnabled() {
+ String value;
+ Process process;
+ try {
+ process = new ProcessBuilder()
+ .command("/system/bin/getprop", "security.perf_harden").start();
+ } catch (IOException e) {
+ // Omit check if getprop doesn't exist.
+ return;
+ }
+ try {
+ process.waitFor();
+ } catch (InterruptedException e) {
+ }
+ value = readInputStream(process.getInputStream());
+ if (value.startsWith("1")) {
+ throw new Error("linux perf events aren't enabled on the device."
+ + " Please run api_profiler.py.");
+ }
+ }
+
+ private void createSimpleperfDataDir() {
+ File file = new File(mSimpleperfDataDir);
+ if (!file.isDirectory()) {
+ file.mkdir();
+ }
+ }
+
+ private void createSimpleperfProcess(String simpleperfPath, List<String> recordArgs) {
+ // 1. Prepare simpleperf arguments.
+ ArrayList<String> args = new ArrayList<>();
+ args.add(simpleperfPath);
+ args.add("record");
+ args.add("--log-to-android-buffer");
+ args.add("--log");
+ args.add("debug");
+ args.add("--stdio-controls-profiling");
+ args.add("--in-app");
+ args.add("--tracepoint-events");
+ args.add("/data/local/tmp/tracepoint_events");
+ args.addAll(recordArgs);
+
+ // 2. Create the simpleperf process.
+ ProcessBuilder pb = new ProcessBuilder(args).directory(new File(mSimpleperfDataDir));
+ try {
+ mSimpleperfProcess = pb.start();
+ } catch (IOException e) {
+ throw new Error("failed to create simpleperf process: " + e.getMessage());
+ }
+
+ // 3. Wait until simpleperf starts recording.
+ String startFlag = readReply();
+ if (!startFlag.equals("started")) {
+ throw new Error("failed to receive simpleperf start flag");
+ }
+ }
+
+ private void sendCmd(@NonNull String cmd) {
+ cmd += "\n";
+ try {
+ mSimpleperfProcess.getOutputStream().write(cmd.getBytes());
+ mSimpleperfProcess.getOutputStream().flush();
+ } catch (IOException e) {
+ throw new Error("failed to send cmd to simpleperf: " + e.getMessage());
+ }
+ if (!readReply().equals("ok")) {
+ throw new Error("failed to run cmd in simpleperf: " + cmd);
+ }
+ }
+
+ @NonNull
+ private String readReply() {
+ // Read one byte at a time to stop at line break or EOF. BufferedReader will try to read
+ // more than available and make us blocking, so don't use it.
+ String s = "";
+ while (true) {
+ int c = -1;
+ try {
+ c = mSimpleperfProcess.getInputStream().read();
+ } catch (IOException e) {
+ }
+ if (c == -1 || c == '\n') {
+ break;
+ }
+ s += (char) c;
+ }
+ return s;
+ }
+}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/simpleperf/RecordOptions.java b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/RecordOptions.java
new file mode 100644
index 0000000..095b55b
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/RecordOptions.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.simpleperf;
+
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * This class sets record options used by ProfileSession. The options are
+ * converted to a string list in toRecordArgs(), which is then passed to
+ * `simpleperf record` cmd. Run `simpleperf record -h` or
+ * `run_simpleperf_on_device.py record -h` for help messages.
+ * </p>
+ *
+ * <p>
+ * Example:
+ * RecordOptions options = new RecordOptions();
+ * options.setDuration(3).recordDwarfCallGraph().setOutputFilename("perf.data");
+ * ProfileSession session = new ProfileSession();
+ * session.startRecording(options);
+ * </p>
+ *
+ * NOTE: copied from
+ * https://cs.android.com/android/platform/superproject/+/master:system/extras/simpleperf/app_api/
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class RecordOptions {
+
+ /**
+ * Set output filename. Default is perf-<month>-<day>-<hour>-<minute>-<second>.data.
+ * The file will be generated under simpleperf_data/.
+ */
+ @NonNull
+ public RecordOptions setOutputFilename(@NonNull String filename) {
+ mOutputFilename = filename;
+ return this;
+ }
+
+ /**
+ * Set event to record. Default is cpu-cycles. See `simpleperf list` for all available events.
+ */
+ @NonNull
+ public RecordOptions setEvent(@NonNull String event) {
+ mEvent = event;
+ return this;
+ }
+
+ /**
+ * Set how many samples to generate each second running. Default is 4000.
+ */
+ @NonNull
+ public RecordOptions setSampleFrequency(int freq) {
+ mFreq = freq;
+ return this;
+ }
+
+ /**
+ * Set record duration. The record stops after `durationInSecond` seconds. By default,
+ * record stops only when stopRecording() is called.
+ */
+ @NonNull
+ public RecordOptions setDuration(double durationInSecond) {
+ mDurationInSeconds = durationInSecond;
+ return this;
+ }
+
+ /**
+ * Record some threads in the app process. By default, record all threads in the process.
+ */
+ @NonNull
+ public RecordOptions setSampleThreads(@NonNull List<Integer> threads) {
+ mThreads.addAll(threads);
+ return this;
+ }
+
+ /**
+ * Record dwarf based call graph. It is needed to get Java callstacks.
+ */
+ @NonNull
+ public RecordOptions recordDwarfCallGraph() {
+ mDwarfCallGraph = true;
+ mFpCallGraph = false;
+ return this;
+ }
+
+ /**
+ * Record frame pointer based call graph. It is suitable to get C++ callstacks on 64bit devices.
+ */
+ @NonNull
+ public RecordOptions recordFramePointerCallGraph() {
+ mFpCallGraph = true;
+ mDwarfCallGraph = false;
+ return this;
+ }
+
+ /**
+ * Trace context switch info to show where threads spend time off cpu.
+ */
+ @NonNull
+ public RecordOptions traceOffCpu() {
+ mTraceOffCpu = true;
+ return this;
+ }
+
+ /**
+ * Translate record options into arguments for `simpleperf record` cmd.
+ */
+ @NonNull
+ public List<String> toRecordArgs() {
+ ArrayList<String> args = new ArrayList<>();
+
+ String filename = mOutputFilename;
+ if (filename == null) {
+ filename = getDefaultOutputFilename();
+ }
+ args.add("-o");
+ args.add(filename);
+ args.add("-e");
+ args.add(mEvent);
+ args.add("-f");
+ args.add(String.valueOf(mFreq));
+ if (mDurationInSeconds != 0.0) {
+ args.add("--duration");
+ args.add(String.valueOf(mDurationInSeconds));
+ }
+ if (mThreads.isEmpty()) {
+ args.add("-p");
+ args.add(String.valueOf(Os.getpid()));
+ } else {
+ String s = "";
+ for (int i = 0; i < mThreads.size(); i++) {
+ if (i > 0) {
+ s += ",";
+ }
+ s += mThreads.get(i).toString();
+ }
+ args.add("-t");
+ args.add(s);
+ }
+ if (mDwarfCallGraph) {
+ args.add("-g");
+ } else if (mFpCallGraph) {
+ args.add("--call-graph");
+ args.add("fp");
+ }
+ if (mTraceOffCpu) {
+ args.add("--trace-offcpu");
+ }
+ return args;
+ }
+
+ private String getDefaultOutputFilename() {
+ LocalDateTime time = LocalDateTime.now();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("'perf'-MM-dd-HH-mm-ss'.data'");
+ return time.format(formatter);
+ }
+
+ @Nullable
+ private String mOutputFilename;
+
+ @NonNull
+ private String mEvent = "cpu-cycles";
+
+ private int mFreq = 4000;
+
+ private double mDurationInSeconds = 0.0;
+
+ @NonNull
+ private ArrayList<Integer> mThreads = new ArrayList<>();
+
+ private boolean mDwarfCallGraph = false;
+
+ private boolean mFpCallGraph = false;
+
+ private boolean mTraceOffCpu = false;
+}