应用基准化分析—Benchmark

一、简介

谷歌在 I/O 2019 发布了 Benchmark 库的第一个 alpha 版。Jetpack Benchmark 是一个运行在 Android 设备上的标准 JUnit 插桩测试 (instrumentation tests),它使用 Benchmark 库提供的一套规则进行测量和报告。

Jetpack Benchmark 可以在 Android Studio 中快速对 Kotlin 或 Java 代码进行基准化分析。该库会处理预热,衡量代码性能,并将基准化分析结果输出到 Android Studio 控制台,完成功能的测试与代码性能分析。

 

二、使用示例

1.添加依赖到模块的 build.gradle 文件中:

project_root/module_dir/build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'junit:junit:4.12'
    androidTestImplementation "androidx.benchmark:benchmark-junit4:$benchmark_version"

    ...
}

2.在 androidTest 目录下的清单文件中,停用调试功能。如下所示:

project_root/module_dir/src/androidTest/AndroidManifest.xml

    <!--
      Important: disable debugging for accurate performance results
      In a com.android.library project, this flag must be disabled from this
      manifest, as it is not possible to override this flag from Gradle.
    -->
    <application
        android:debuggable="false"
        tools:ignore="HardcodedDebugMode"
        tools:replace="android:debuggable" />

3.在 androidTest 目录下的测试类中,添加 BenchmarkRule 实现:

  • Kotlin实现
@get:Rule
val benchmarkRule = BenchmarkRule();

@Test
fun myBenchmark() {
     ...
     benchmarkRule.measureRepeated {
         doSomeWork()
     }
     ...
}
  • Java实现
@Rule
public BenchmarkRule benchmarkRule = new BenchmarkRule();

@Test
public void myBenchmark() {
     ...
     BenchmarkState state = benchmarkRule.getState();
     while (state.keepRunning()) {
         doSomeWork();
     }
     ...
}

4.时钟稳定性

(1) 添加插件到工程的 build.gradle 文件中:

project_root/build.gradle

buildscript {
    ...
    dependencies {
        ...
        classpath "androidx.benchmark:benchmark-gradle-plugin:$benchmark_version"
    }
}

(2) 对于要进行分析的模块,在 build.gradle 文件中应用该插件:

project_root/module_dir/build.gradle

apply plugin: com.android.app
apply plugin: androidx.benchmark
...
  • 最小化干扰

启动一个全屏的、不透明的 Activity,以确保其在前台运行,避免其它 BenchmarkRunner 的干扰。当然,也可通过 ActivityTestRule 或者 ActivityScenario 启动其它的 Activity。当在所有 @Test 方法执行之前,没有其它 BenchmarkRunner 运行时,该 Activity 会被重新启动。

  • CPU锁频

需要 root 权限,确保CPU频率不会过高导致设备过热,也不会过低导致不能充分利用 CPU。

5.抑制错误

正常检测的基本条件:

  • 设备为真机

  • 设备电量充足

  • 设备已被root

  • 设备CPU已被锁频

  • Debuggable 已设为 false

如果上述任一项检查失败,Benchmark将抛出错误,并中止分析过程和结果的输出。如果想继续执行检测过程,可以将要抑制的错误类型以逗号分隔列表的形式,传递给插桩参数 androidx.benchmark.suppressErrors,但会在测试名称前附加上 DEBUGGABLE_

android {
    defaultConfig {
       // Set this argument to capture profiling information, instead of measuring performance.
        // Can be one of:
        //   * None
        //   * Sampled
        //   * Method
        // See full descriptions of available options at: d.android.com/benchmark#profiling
        testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'none'

        // Enable measuring on an emulator, or devices with low battery
        testInstrumentationRunnerArgument 'androidx.benchmark.suppressErrors', 'EMULATOR,LOW_BATTERY,DEBUGGABLE'
    }
}

6.运行输出

对 Android Studio 中çè¾åºè¿è¡åºåååæ

  • AS方式

project_root/module_dir/src/androidTest,然后按 Control+Shift+F10 快捷键。或者 project_root/module_dir/src/androidTest/java,鼠标右键, 选择相应的操作。

  • Gradle方式
./gradlew benchmark:connectedCheck

7.生成报告

  • 保存到设备

androidx.benchmark 插件默认会启用 JSON 输出。默认情况下,JSON 报告会写入设备上磁盘的测试 APK 的外部共享“下载内容”文件夹中,该文件夹通常位于以下位置:

/storage/emulated/0/Download/app_id-benchmarkData.json

  • 拷贝到主机

Android Gradle Plugin 3.6 及更高版本,需要将以下属性添加到项目的 gradle.properties 文件中:

android.enableAdditionalTestOutput=true

如果 API 级别是 29 或更高级别,使用 Android Gradle Plugin 3.5 或更低版本,需要在 androidTest 目录下的清单文件中,添加如下属性:

<manifest ... >
    <!-- This attribute is "false" by default on apps targeting Android 10. -->
    <application
        ...
        android:requestLegacyExternalStorage="true">
            ...
    </application>
</manifest>
    

 

三、使用建议

基准化分析对应用中频繁执行的 CPU 工作最有用。例如,RecyclerView 滚动、数据转换/处理以及反复使用的代码。由于基准是循环运行的,所以那些不经常运行的代码,或调用时以不同方式执行的代码,都不太适合进行基准化分析。

1. 缓存

尽量避免只衡量缓存。例如,自定义 View 的布局基准可能只能衡量布局缓存的性能,建议在每个循环中传递不同的布局参数。

2.不经常运行的代码

Android 运行时 (ART) 不太可能对只会在应用启动期间运行一次的代码进行 JIT 编译。因此在循环运行此代码时,建议在应用中对其进行跟踪或性能剖析。

 

四、原理简析

1.代码结构

2.设计思路

(1) 如何对CPU进行锁频?

通过benchmark-gradle-plugin中的lockClocks.sh脚本:

CPU_TARGET_FREQ_PERCENT=50
GPU_TARGET_FREQ_PERCENT=50

if [ "`command -v getprop`" == "" ]; then
    if [ -n "`command -v adb`" ]; then
        echo ""
        echo "Pushing $0 and running it on device..."
        dest=/data/local/tmp/`basename $0`
        adb push $0 ${dest}
        adb shell ${dest}
        adb shell rm ${dest}
        exit
    else
        echo "Could not find adb. Options are:"
        echo "  1. Ensure adb is on your \$PATH"
        echo "  2. Use './gradlew lockClocks'"
        echo "  3. Manually adb push this script to your device, and run it there"
        exit -1
    fi
fi

# require root
if [[ `id` != "uid=0"* ]]; then
    echo "Not running as root, cannot lock clocks, aborting"
    exit -1
fi

DEVICE=`getprop ro.product.device`
MODEL=`getprop ro.product.model`

# Find CPU max frequency, and lock big cores to an available frequency
# that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
function_lock_cpu() {
    CPU_BASE=/sys/devices/system/cpu
    GOV=cpufreq/scaling_governor

    # Find max CPU freq, and associated list of available freqs
    cpuMaxFreq=0
    cpuAvailFreqCmpr=0
    cpuAvailFreq=0
    enableIndices=''
    disableIndices=''
    cpu=0
    while [ -f ${CPU_BASE}/cpu${cpu}/online ]; do
        # enable core, so we can find its frequencies
        echo 1 > ${CPU_BASE}/cpu${cpu}/online

        # set userspace governor on all CPUs to ensure freq scaling is disabled
        echo userspace > ${CPU_BASE}/cpu${cpu}/${GOV}

        maxFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/cpuinfo_max_freq`
        availFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/scaling_available_frequencies`
        availFreqCmpr=${availFreq// /-}

        if [ ${maxFreq} -gt ${cpuMaxFreq} ]; then
            # new highest max freq, look for cpus with same max freq and same avail freq list
            cpuMaxFreq=${maxFreq}
            cpuAvailFreq=${availFreq}
            cpuAvailFreqCmpr=${availFreqCmpr}

            if [ -z ${disableIndices} ]; then
                disableIndices="$enableIndices"
            else
                disableIndices="$disableIndices $enableIndices"
            fi
            enableIndices=${cpu}
        elif [ ${maxFreq} == ${cpuMaxFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then
            enableIndices="$enableIndices $cpu"
        else
            disableIndices="$disableIndices $cpu"
        fi

        cpu=$(($cpu + 1))
    done

    # Chose a frequency to lock to that's >= $CPU_TARGET_FREQ_PERCENT% of max
    # (below, 100M = 1K for KHz->MHz * 100 for %)
    TARGET_FREQ_MHZ=$(( (${cpuMaxFreq} * ${CPU_TARGET_FREQ_PERCENT}) / 100000 ))
    chosenFreq=0
    for freq in ${cpuAvailFreq}; do
        freqMhz=$(( ${freq} / 1000 ))
        if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ]; then
            chosenFreq=${freq}
            break
        fi
    done

    # enable 'big' CPUs
    for cpu in ${enableIndices}; do
        freq=${CPU_BASE}/cpu$cpu/cpufreq

        echo 1 > ${CPU_BASE}/cpu${cpu}/online
        echo ${chosenFreq} > ${freq}/scaling_max_freq
        echo ${chosenFreq} > ${freq}/scaling_min_freq
        echo ${chosenFreq} > ${freq}/scaling_setspeed

        # validate setting the freq worked
        obsCur=`cat ${freq}/scaling_cur_freq`
        obsMin=`cat ${freq}/scaling_min_freq`
        obsMax=`cat ${freq}/scaling_max_freq`
        if [ obsCur -ne ${chosenFreq} ] || [ obsMin -ne ${chosenFreq} ] || [ obsMax -ne ${chosenFreq} ]; then
            echo "Failed to set CPU$cpu to $chosenFreq Hz! Aborting..."
            echo "scaling_cur_freq = $obsCur"
            echo "scaling_min_freq = $obsMin"
            echo "scaling_max_freq = $obsMax"
            exit -1
        fi
    done

    # disable other CPUs (Note: important to enable big cores first!)
    for cpu in ${disableIndices}; do
      echo 0 > ${CPU_BASE}/cpu${cpu}/online
    done

    echo "=================================="
    echo "Locked CPUs ${enableIndices// /,} to $chosenFreq / $maxFreq KHz"
    echo "Disabled CPUs ${disableIndices// /,}"
    echo "=================================="
}

# If we have a Qualcomm GPU, find its max frequency, and lock to
# an available frequency that's >= GPU_TARGET_FREQ_PERCENT% of max.
function_lock_gpu_kgsl() {
    if [ ! -d /sys/class/kgsl/kgsl-3d0/ ]; then
        # not kgsl, abort
        echo "Currently don't support locking GPU clocks of $MODEL ($DEVICE)"
        return -1
    fi
    if [ ${DEVICE} == "walleye" ] || [ ${DEVICE} == "taimen" ]; then
        # Workaround crash
        echo "Unable to lock GPU clocks of $MODEL ($DEVICE)"
        return -1
    fi

    GPU_BASE=/sys/class/kgsl/kgsl-3d0

    gpuMaxFreq=0
    gpuAvailFreq=`cat $GPU_BASE/devfreq/available_frequencies`
    for freq in ${gpuAvailFreq}; do
        if [ ${freq} -gt ${gpuMaxFreq} ]; then
            gpuMaxFreq=${freq}
        fi
    done

    # (below, 100M = 1M for MHz * 100 for %)
    TARGET_FREQ_MHZ=$(( (${gpuMaxFreq} * ${GPU_TARGET_FREQ_PERCENT}) / 100000000 ))

    chosenFreq=${gpuMaxFreq}
    index=0
    chosenIndex=0
    for freq in ${gpuAvailFreq}; do
        freqMhz=$(( ${freq} / 1000000 ))
        if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ] && [ ${chosenFreq} -ge ${freq} ]; then
            # note avail freq are generally in reverse order, so we don't break out of this loop
            chosenFreq=${freq}
            chosenIndex=${index}
        fi
        index=$(($index + 1))
    done
    lastIndex=$(($index - 1))

    firstFreq=`function_cut_first_from_space_seperated_list $gpuAvailFreq`

    if [ ${gpuMaxFreq} != ${firstFreq} ]; then
        # pwrlevel is index of desired freq among available frequencies, from highest to lowest.
        # If gpuAvailFreq appears to be in-order, reverse the index
        chosenIndex=$(($lastIndex - $chosenIndex))
    fi

    echo 0 > ${GPU_BASE}/bus_split
    echo 1 > ${GPU_BASE}/force_clk_on
    echo 10000 > ${GPU_BASE}/idle_timer

    echo performance > ${GPU_BASE}/devfreq/governor

    # NOTE: we store in min/max twice, because we don't know if we're increasing
    # or decreasing, and it's invalid to try and set min > max, or max < min
    echo ${chosenFreq} > ${GPU_BASE}/devfreq/min_freq
    echo ${chosenFreq} > ${GPU_BASE}/devfreq/max_freq
    echo ${chosenFreq} > ${GPU_BASE}/devfreq/min_freq
    echo ${chosenFreq} > ${GPU_BASE}/devfreq/max_freq
    echo ${chosenIndex} > ${GPU_BASE}/min_pwrlevel
    echo ${chosenIndex} > ${GPU_BASE}/max_pwrlevel
    echo ${chosenIndex} > ${GPU_BASE}/min_pwrlevel
    echo ${chosenIndex} > ${GPU_BASE}/max_pwrlevel

    obsCur=`cat ${GPU_BASE}/devfreq/cur_freq`
    obsMin=`cat ${GPU_BASE}/devfreq/min_freq`
    obsMax=`cat ${GPU_BASE}/devfreq/max_freq`
    if [ obsCur -ne ${chosenFreq} ] || [ obsMin -ne ${chosenFreq} ] || [ obsMax -ne ${chosenFreq} ]; then
        echo "Failed to set GPU to $chosenFreq Hz! Aborting..."
        echo "cur_freq = $obsCur"
        echo "min_freq = $obsMin"
        echo "max_freq = $obsMax"
        echo "index = $chosenIndex"
        exit -1
    fi
    echo "Locked GPU to $chosenFreq / $gpuMaxFreq Hz"
}

# cut is not available on some devices (Nexus 5 running LRX22C).
function_cut_first_from_space_seperated_list() {
    list=$1

    for freq in $list; do
        echo $freq
        break
    done
}

# kill processes that manage thermals / scaling
stop thermal-engine
stop perfd
stop vendor.thermal-engine
stop vendor.perfd
setprop vendor.powerhal.init 0
setprop ctl.interface_restart android.hardware.power@1.0::IPower/default

function_lock_cpu

function_lock_gpu_kgsl

# Memory bus - hardcoded per-device for now
if [ ${DEVICE} == "marlin" ] || [ ${DEVICE} == "sailfish" ]; then
    echo 13763 > /sys/class/devfreq/soc:qcom,gpubw/max_freq
else
    echo "Unable to lock memory bus of $MODEL ($DEVICE)."
fi

echo "$DEVICE clocks have been locked - to reset, reboot the device"

(2) 如何衡量代码性能的?

跟踪源码可知,BenchmarkState 类的 keepRunningInternal() 方法是检测的入口,检测过程分为 NOT_STARTED、WARMUP、RUNNING、FINISHED 四种状态,具体请仔细阅读代码中的注释。

  1. NOT_STARTED:先记录检测总用时的开始时间点。如果配置了 dryRunMode 或者 startupMode 参数,调用 beginBenchmark() 方法开始检测,否则调用 beginWarmup() 方法做下 gc,记录一些时间、次数信息,将状态切换为 WARMUP。

  2. WARMUP:先计算距最近一次检测的时间间隔 lastDuration,刷新最新的检测时间点。如果通过 WarmupManager 类的 onNextIteration(...) 方法判断Warmup过程已完成,则调用 beginBenchmark() 方法开始检测,记录一些时间、次数信息,将状态切换为 RUNNING。

  3. RUNNING:如果达到了 WarmupManager 类计算出的迭代次数,则调用 startNextTestRun() 方法,先保存减去 BenchmarkRule.runWithTimingDisabled(...) 的值 pausedDurationNs 后的检测结果,再判断是否执行了需要的循环次数(dryRunMode:1 、startupMode:10、默认:50),满足条件则调用 endBenchmark() 方法,将状态切换为 FINISHED。

  4. FINISHED:在完成检测后,若执行到了此状态,将抛出异常。

 

class BenchmarkState {
    // 省略部分代码...

    @PublishedApi
    internal fun keepRunningInternal(): Boolean {
        when (state) {
            NOT_STARTED -> {
                if (Errors.UNSUPPRESSED_WARNING_MESSAGE != null) {
                    throw AssertionError(Errors.UNSUPPRESSED_WARNING_MESSAGE)
                }
                if (!firstBenchmark && Arguments.startupMode) {
                    throw AssertionError(
                        "Error - multiple benchmarks in startup mode. Only one " +
                                "benchmark may be run per 'am instrument' call, to ensure result " +
                                "isolation."
                    )
                }
                firstBenchmark = false

                if (totalRunTimeStartNs == 0L) {
                    // This is the beginning of the benchmark, we remember it.
                    totalRunTimeStartNs = System.nanoTime()
                }
                if (performThrottleChecks &&
                    !CpuInfo.locked &&
                    !IsolationActivity.sustainedPerformanceModeInUse &&
                    !Errors.isEmulator
                ) {
                    ThrottleDetector.computeThrottleBaseline()
                }

                ThreadPriority.bumpCurrentThreadPriority()

                if (Arguments.dryRunMode || Arguments.startupMode) {
                    beginBenchmark()
                } else {
                    beginWarmup()
                }
                return true
            }
            WARMUP -> {
                warmupIteration++
                // Only check nanoTime on every iteration in WARMUP since we
                // don't yet have a target iteration count.
                val time = System.nanoTime()
                val lastDuration = time - startTimeNs
                startTimeNs = time
                throwIfPaused() // check each loop during warmup
                if (warmupManager.onNextIteration(lastDuration)) {
                    endTraceSection() // paired with start in beginWarmup()
                    beginBenchmark()
                }
                return true
            }
            RUNNING -> {
                iterationsRemaining--
                if (iterationsRemaining <= 0) {
                    throwIfPaused() // only check at end of loop to save cycles
                    return startNextTestRun()
                }
                return true
            }
            FINISHED -> throw IllegalStateException("The benchmark has finished.")
            else -> throw IllegalStateException("The benchmark is in unknown state.")
        }
    }

    private fun beginWarmup() {
        beginTraceSection("Warmup")
        // Run GC to avoid memory pressure from previous run from affecting this one.
        // Note, we don't use System.gc() because it doesn't always have consistent behavior
        Runtime.getRuntime().gc()

        startTimeNs = System.nanoTime()
        warmupIteration = 0
        state = WARMUP
    }

    private fun beginBenchmark() {
        if (ENABLE_PROFILING && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // TODO: support data dir for old platforms
            val f = File(
                InstrumentationRegistry.getInstrumentation().context.dataDir,
                "benchprof"
            )
            Log.d(TAG, "Tracing to: " + f.absolutePath)
            Debug.startMethodTracingSampling(f.absolutePath, 16 * 1024 * 1024, 100)
        }
        maxIterations = OVERRIDE_ITERATIONS ?: computeIterationsFromWarmup()
        pausedDurationNs = 0
        iterationsRemaining = maxIterations
        repeatCount = 0
        thermalThrottleSleepSeconds = 0
        state = RUNNING
        beginTraceSection("Benchmark")
        startTimeNs = System.nanoTime()
    }

    private fun startNextTestRun(): Boolean {
        val currentTime = System.nanoTime()

        results.add((currentTime - startTimeNs - pausedDurationNs) / maxIterations)
        repeatCount++

        if (repeatCount >= REPEAT_COUNT) {
            if (performThrottleChecks &&
                throttleRemainingRetries > 0 &&
                sleepIfThermalThrottled(THROTTLE_BACKOFF_S)
            ) {
                // We've slept due to thermal throttle - retry benchmark!
                throttleRemainingRetries -= 1
                results.clear()
                repeatCount = 0
            } else {
                // Benchmark finished!
                endBenchmark()
                return false
            }
        }
        pausedDurationNs = 0
        iterationsRemaining = maxIterations
        startTimeNs = System.nanoTime()
        return true
    }

    private fun endBenchmark() {
        endTraceSection() // paired with start in beginBenchmark()
        if (ENABLE_PROFILING) {
            Debug.stopMethodTracing()
        }
        ThreadPriority.resetBumpedThread()
        warmupManager.logInfo()
        results.chunked(10).forEachIndexed { i, list ->
            Log.d(
                TAG, "Results[%2d:%2d]: %s".format(
                    i * 10,
                    (i + 1) * 10,
                    list.joinToString()
                )
            )
        }

        internalStats = Stats(results)
        state = FINISHED
        totalRunTimeNs = System.nanoTime() - totalRunTimeStartNs
    }

    companion object {
        internal const val TAG = "Benchmark"
        private const val STUDIO_OUTPUT_KEY_PREFIX = "android.studio.display."
        private const val STUDIO_OUTPUT_KEY_ID = "benchmark"

        private const val ENABLE_PROFILING = false

        private const val NOT_STARTED = 0 // The benchmark has not started yet.
        private const val WARMUP = 1 // The benchmark is warming up.
        private const val RUNNING = 2 // The benchmark is running.
        private const val FINISHED = 3 // The benchmark has stopped.

        // Values determined empirically.
        @VisibleForTesting
        internal val REPEAT_COUNT = when {
            Arguments.dryRunMode -> 1
            Arguments.startupMode -> 10
            else -> 50
        }
        private val OVERRIDE_ITERATIONS = when {
            Arguments.dryRunMode || Arguments.startupMode -> 1
            else -> null
        }
        private val TARGET_TEST_DURATION_NS = TimeUnit.MICROSECONDS.toNanos(500)
        private const val MAX_TEST_ITERATIONS = 1000000
        private const val MIN_TEST_ITERATIONS = 1

        private const val THROTTLE_MAX_RETRIES = 3
        private const val THROTTLE_BACKOFF_S = 90L

        private var firstBenchmark = true

       // 省略部分代码...
   }

   // 省略部分代码...
}

(3) 在检测过程中,如何忽略指定代码的性能损耗?

可阅读 BenchmarkRule 类的 runWithTimingDisabled(...) 方法的注释:

class BenchmarkRule : TestRule {
    // 省略部分代码...

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    val scope = Scope()

    /**
     * Handle used for controlling timing during [measureRepeated].
     */
    inner class Scope internal constructor() {
        /**
         * Disable timing for a block of code.
         *
         * Used for disabling timing for work that isn't part of the benchmark:
         * - When constructing per-loop randomized inputs for operations with caching,
         * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout)
         * - Disabling timing during per-loop verification
         *
         * ```
         * @Test
         * fun bitmapProcessing() = benchmarkRule.measureRepeated {
         *     val input: Bitmap = runWithTimingDisabled { constructTestBitmap() }
         *     processBitmap(input)
         * }
         * ```
         */
        inline fun <T> runWithTimingDisabled(block: () -> T): T {
            getOuterState().pauseTiming()
            val ret = block()
            getOuterState().resumeTiming()
            return ret
        }

        /**
         * Allows the inline function [runWithTimingDisabled] to be called outside of this scope.
         */
        @PublishedApi
        internal fun getOuterState(): BenchmarkState {
            return getState()
        }
    }

    // 省略部分代码...
}
class BenchmarkState {
    // 省略部分代码...

    /**
     * Stops the benchmark timer.
     *
     * This method can be called only when the timer is running.
     *
     * ```
     * @Test
     * public void bitmapProcessing() {
     *     final BenchmarkState state = mBenchmarkRule.getState();
     *     while (state.keepRunning()) {
     *         state.pauseTiming();
     *         // disable timing while constructing test input
     *         Bitmap input = constructTestBitmap();
     *         state.resumeTiming();
     *
     *         processBitmap(input);
     *     }
     * }
     * ```
     *
     * @throws [IllegalStateException] if the benchmark is already paused.
     *
     * @see resumeTiming
     */
    fun pauseTiming() {
        check(!paused) { "Unable to pause the benchmark. The benchmark has already paused." }
        pausedTimeNs = System.nanoTime()
        paused = true
    }

    /**
     * Resumes the benchmark timer.
     *
     * This method can be called only when the timer is stopped.
     *
     * ```
     * @Test
     * public void bitmapProcessing() {
     *     final BenchmarkState state = mBenchmarkRule.getState();
     *     while (state.keepRunning()) {
     *         state.pauseTiming();
     *         // disable timing while constructing test input
     *         Bitmap input = constructTestBitmap();
     *         state.resumeTiming();
     *
     *         processBitmap(input);
     *     }
     * }
     *
     * @throws [IllegalStateException] if the benchmark is already running.
     *
     * ```
     *
     * @see pauseTiming
     */
    fun resumeTiming() {
        check(paused) { "Unable to resume the benchmark. The benchmark is already running." }

        pausedDurationNs += System.nanoTime() - pausedTimeNs
        pausedTimeNs = 0
        paused = false
    }

   // 省略部分代码...
}

(4) 如何维持屏幕常亮,以确保结果的可靠性?

在执行 IsolationActivity类 的 onCreate(...) 方法时,注册 ActivityLifecycleCallbacks 并触发执行 onActivityCreated(...) 方法,以强制唤醒设备、保持屏幕常亮,从而确保结果的可靠性:

class IsolationActivity : android.app.Activity() {
    private var destroyed = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.isolation_activity)

        // disable launch animation
        overridePendingTransition(0, 0)

        if (firstInit) {
            if (!CpuInfo.locked && isSustainedPerformanceModeSupported()) {
                sustainedPerformanceModeInUse = true
            }

            application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
            // trigger the one missed lifecycle event, from registering the callbacks late
            activityLifecycleCallbacks.onActivityCreated(this, savedInstanceState)

            // 省略部分代码...
            firstInit = false
        }

        // 省略部分代码...
    }

    // 省略部分代码...
}

private val activityLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
        // 省略部分代码...

        // Forcibly wake the device, and keep the screen on to prevent benchmark failures.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            val keyguardManager =
                activity.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
            keyguardManager.requestDismissKeyguard(activity, null)
            activity.setShowWhenLocked(true)
            activity.setTurnScreenOn(true)
            activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        } else {
            @Suppress("DEPRECATION")
            activity.window.addFlags(
                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                        or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                        or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                        or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
            )
        }
    }

    // 省略部分代码...
}

 

参考:

https://developer.android.google.cn/studio/profile/benchmark

https://developer.android.google.cn/jetpack/androidx/releases/benchmark

https://zhuanlan.zhihu.com/p/158616297

https://medium.com/@iamanbansal/exploring-jetpack-benchmark-de107165bf43

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值