CompilationMode.kt
/*
* Copyright 2021 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.macro
import android.os.Build
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Shell
import androidx.benchmark.macro.CompilationMode.SpeedProfile
import androidx.profileinstaller.ProfileInstallReceiver
import androidx.profileinstaller.ProfileInstaller
import org.junit.AssumptionViolatedException
/**
* Type of compilation to use for a Macrobenchmark.
*
* For example, [SpeedProfile] will run a configurable number of profiling iterations to generate
* a profile, and use that to compile the target app.
*/
public sealed class CompilationMode(
// for modes other than [None], is argument passed `cmd package compile`
private val compileArgument: String?
) {
internal fun compileArgument(): String {
if (compileArgument == null) {
throw UnsupportedOperationException("No compileArgument for mode $this")
}
return compileArgument
}
/**
* No pre-compilation - entire app will be allowed to Just-In-Time compile as it runs.
*/
public object None : CompilationMode(null) {
public override fun toString(): String = "None"
}
/**
* Partial pre-compilation, based on configurable number of profiling iterations.
*/
public class SpeedProfile(
public val warmupIterations: Int = 3
) : CompilationMode("speed-profile") {
public override fun toString(): String = "SpeedProfile(iterations=$warmupIterations)"
}
/**
* Partial pre-compilation based on bundled baseline profile.
*
* Note: this mode is only supported for APKs that have the profileinstaller library
* included, and have been built by AGP 7.0+ to package the baseline profile in the APK.
*/
public object BaselineProfile : CompilationMode("speed-profile") {
public override fun toString(): String = "BaselineProfile"
}
/**
* Full ahead-of-time compilation.
*/
public object Speed : CompilationMode("speed") {
public override fun toString(): String = "Speed"
}
/**
* No JIT / pre-compilation, all app code runs on the interpreter.
*
* Note: this mode will only be supported on rooted devices with jit disabled. For this reason,
* it's only available for internal benchmarking.
*
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public object Interpreted : CompilationMode(null) {
public override fun toString(): String = "Interpreted"
}
}
/**
* Compiles the application with the given mode.
*
* For more information: https://source.android.com/devices/tech/dalvik/jit-compiler
*/
internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
if (Build.VERSION.SDK_INT < 24) {
// All supported versions prior to 24 were full AOT
// TODO: clarify this with CompilationMode errors on these versions
return
}
// Clear profile between runs.
Log.d(TAG, "Clearing profiles for $packageName")
Shell.executeCommand("cmd package compile --reset $packageName")
if (this == CompilationMode.None || this == CompilationMode.Interpreted) {
return // nothing to do
} else if (this == CompilationMode.BaselineProfile) {
// For baseline profiles, if the ProfileInstaller library is included in the APK, then we
// triggering this broadcast will cause the baseline profile to get installed
// synchronously, instead of waiting for the
val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
// Use an explicit broadcast given the app was force-stopped.
val name = ProfileInstallReceiver::class.java.name
val result = Shell.executeCommand("am broadcast -a $action $packageName/$name")
.substringAfter("Broadcast completed: result=")
.trim()
.toIntOrNull()
when (result) {
null,
// 0 is returned by the platform by default, and also if no broadcast receiver
// receives the broadcast.
0 -> {
throw RuntimeException(
"The baseline profile install broadcast was not received. This most likely " +
"means that the profileinstaller library is not in the target APK. It " +
"must be in order to use CompilationMode.BaselineProfile."
)
}
ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
// success !
}
ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
throw RuntimeException(
"Unable to install baseline profiles. This most likely means that the latest " +
"version of the profileinstaller library is not being used. Please " +
"use the latest profileinstaller library version."
)
}
ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
throw RuntimeException("Baseline profiles aren't supported on this device version")
}
ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
throw RuntimeException("No baseline profile was found in the target apk.")
}
ProfileInstaller.RESULT_NOT_WRITABLE,
ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
ProfileInstaller.RESULT_IO_EXCEPTION,
ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
throw RuntimeException("Baseline Profile wasn't successfully installed")
}
else -> {
throw RuntimeException(
"unrecognized ProfileInstaller result code: $result"
)
}
}
// Kill Process
Log.d(TAG, "Killing process $packageName")
Shell.executeCommand("am force-stop $packageName")
// Compile
compilePackage(packageName)
Log.d(TAG, "$packageName is compiled.")
} else if (this is CompilationMode.SpeedProfile) {
repeat(this.warmupIterations) {
block()
}
// For speed profile compilation, ART team recommended to wait for 5 secs when app
// is in the foreground, dump the profile, wait for another 5 secs before
// speed-profile compilation.
Thread.sleep(5000)
val response = Shell.executeCommand("killall -s SIGUSR1 $packageName")
if (response.isNotBlank()) {
Log.d(TAG, "Received dump profile response $response")
throw RuntimeException("Failed to dump profile for $packageName ($response)")
}
compilePackage(packageName)
}
}
/**
* Returns true if the CompilationMode can be run with the device's current VM settings.
*
* Used by jetpack-internal benchmarks to skip CompilationModes that would self-suppress.
*
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public fun CompilationMode.isSupportedWithVmSettings(): Boolean {
val getProp = Shell.executeCommand("getprop dalvik.vm.extra-opts")
val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
// true if requires interpreted, false otherwise
val interpreted = this == CompilationMode.Interpreted
return vmRunningInterpretedOnly == interpreted
}
internal fun CompilationMode.assumeSupportedWithVmSettings() {
if (!isSupportedWithVmSettings()) {
throw AssumptionViolatedException(
when {
DeviceInfo.isRooted && this == CompilationMode.Interpreted ->
"""
To run benchmarks with CompilationMode $this,
you must disable jit on your device with the following command:
`adb shell setprop dalvik.vm.extra-opts -Xusejit:false; adb shell stop; adb shell start`
""".trimIndent()
DeviceInfo.isRooted && this != CompilationMode.Interpreted ->
"""
To run benchmarks with CompilationMode $this,
you must enable jit on your device with the following command:
`adb shell setprop dalvik.vm.extra-opts \"\"; adb shell stop; adb shell start`
""".trimIndent()
else ->
"You must toggle usejit on the VM to use CompilationMode $this, this requires" +
"rooting your device."
}
)
}
}
/**
* Compiles the application.
*/
internal fun CompilationMode.compilePackage(packageName: String) {
Log.d(TAG, "Compiling $packageName ($this)")
val response = Shell.executeCommand(
"cmd package compile -f -m ${compileArgument()} $packageName"
)
if (!response.contains("Success")) {
Log.d(TAG, "Received compile cmd response: $response")
throw RuntimeException("Failed to compile $packageName ($response)")
}
}