blob: 6b155e233e7da39c96407181ae26b94631056016 [file] [log] [blame]
/*
* Copyright 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
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.os.BatteryManager
import android.os.Build
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
/**
* Lazy-initialized test-suite global state for warnings around measurement inaccuracy.
*/
internal object WarningState {
private const val TAG = "Benchmark"
val WARNING_PREFIX: String
private var warningString: String? = null
/**
* Battery percentage required to avoid low battery warning.
*
* This number is supposed to be a conservative cutoff for when low-battery-triggered power
* savings modes (such as disabling cores) may be enabled. It's possible that
* [BatteryManager.EXTRA_BATTERY_LOW] is a better source of truth for this, but we want to be
* conservative in case the device loses power slowly while benchmarks run.
*/
private const val MINIMUM_BATTERY_PERCENT = 25
fun acquireWarningStringForLogging(): String? {
val ret = warningString
warningString = null
return ret
}
val isEmulator = Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.MODEL.contains("google_sdk") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK built for x86") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") ||
"google_sdk" == Build.PRODUCT
private val isDeviceRooted =
arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
).any { File(it).exists() }
init {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val appInfo = context.applicationInfo
var warningPrefix = ""
var warningString = ""
if (appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
warningPrefix += "DEBUGGABLE_"
warningString += """
|WARNING: Debuggable Benchmark
| Benchmark is running with debuggable=true, which drastically reduces
| runtime performance in order to support debugging features. Run
| benchmarks with debuggable=false. Debuggable affects execution speed
| in ways that mean benchmark improvements might not carry over to a
| real user's experience (or even regress release performance).
""".trimMarginWrapNewlines()
}
if (isEmulator) {
warningPrefix += "EMULATOR_"
warningString += """
|WARNING: Running on Emulator
| Benchmark is running on an emulator, which is not representative of
| real user devices. Use a physical device to benchmark. Emulator
| benchmark improvements might not carry over to a real user's
| experience (or even regress real device performance).
""".trimMarginWrapNewlines()
}
if (Build.FINGERPRINT.contains(":eng/")) {
warningPrefix += "ENG-BUILD_"
warningString += """
|WARNING: Running on Eng Build
| Benchmark is running on device flashed with a '-eng' build. Eng builds
| of the platform drastically reduce performance to enable testing
| changes quickly. For this reason they should not be used for
| benchmarking. Use a '-user' or '-userdebug' system image.
""".trimMarginWrapNewlines()
}
val arguments = InstrumentationRegistry.getArguments()
if (arguments["coverage"] == "true") {
warningPrefix += "CODE-COVERAGE_"
warningString += """
|WARNING: Code coverage enabled
| Benchmark is running with code coverage enabled, which typically alters the dex
| in a way that can affect performance. Ensure that code coverage is disabled by
| setting testCoverageEnabled to false in the buildType your benchmarks run in.
""".trimMarginWrapNewlines()
}
if (isDeviceRooted && !CpuInfo.locked) {
warningPrefix += "UNLOCKED_"
warningString += """
|WARNING: Unlocked CPU clocks
| Benchmark appears to be running on a rooted device with unlocked CPU
| clocks. Unlocked CPU clocks can lead to inconsistent results due to
| dynamic frequency scaling, and thermal throttling. On a rooted device,
| lock your device clocks to a stable frequency with `./gradlew lockClocks`
""".trimMarginWrapNewlines()
}
if (!CpuInfo.locked &&
AndroidBenchmarkRunner.isSustainedPerformanceModeSupported() &&
!AndroidBenchmarkRunner.sustainedPerformanceModeInUse
) {
warningPrefix += "UNSUSTAINED-RUNNER-MISSING_"
warningString += """
|WARNING: Cannot use SustainedPerformanceMode without AndroidBenchmarkRunner
| Benchmark running on device that supports Window.setSustainedPerformanceMode,
| but not using the AndroidBenchmarkRunner. This runner is required to limit
| CPU clock max frequency, to prevent thermal throttling. To fix this, add the
| following to your benchmark module-level build.gradle:
| android.defaultConfig.testInstrumentationRunner
| = "androidx.benchmark.AndroidBenchmarkRunner"
""".trimMarginWrapNewlines()
} else if (!AndroidBenchmarkRunner.runnerInUse) {
warningPrefix += "RUNNER-MISSING_"
warningString += """
|WARNING: Not using AndroidBenchmarkRunner
| AndroidBenchmarkRunner should be used to isolate benchmarks from interference
| from other visible apps. To fix this, add the following to your module-level
| build.gradle:
| android.defaultConfig.testInstrumentationRunner
| = "androidx.benchmark.AndroidBenchmarkRunner"
""".trimMarginWrapNewlines()
}
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryPercent = context.registerReceiver(null, filter)?.run {
val level = getIntExtra(BatteryManager.EXTRA_LEVEL, 100)
val scale = getIntExtra(BatteryManager.EXTRA_SCALE, 100)
level * 100 / scale
} ?: 100
if (batteryPercent < MINIMUM_BATTERY_PERCENT) {
warningPrefix += "LOW-BATTERY_"
warningString += """
|WARNING: Device has low battery ($batteryPercent%)
| When battery is low, devices will often reduce performance (e.g. disabling big
| cores) to save remaining battery. This occurs even when they are plugged in.
| Wait for your battery to charge to at least $MINIMUM_BATTERY_PERCENT%.
| Currently at $batteryPercent%.
""".trimMarginWrapNewlines()
}
WARNING_PREFIX = warningPrefix
if (!warningString.isEmpty()) {
this.warningString = warningString
warningString.split("\n").map { Log.w(TAG, it) }
}
}
/**
* Same as trimMargins, but add newlines on either side.
*/
private fun String.trimMarginWrapNewlines(): String {
return "\n" + trimMargin() + " \n"
}
}