MacrobenchmarkScope.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.content.Intent
import android.util.Log
import androidx.benchmark.macro.perfetto.executeShellScript
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import java.util.concurrent.TimeUnit

/**
 * Provides access to common operations in app automation, such as killing the app,
 * or navigating home.
 */
public class MacrobenchmarkScope(
    private val packageName: String,
    /**
     * Controls whether launches will automatically set [Intent.FLAG_ACTIVITY_CLEAR_TASK].
     *
     * Default to true, so Activity launches go through full creation lifecycle stages, instead of
     * just resume.
     */
    private val launchWithClearTask: Boolean
) {
    private val instrumentation = InstrumentationRegistry.getInstrumentation()
    private val context = instrumentation.context
    private val device = UiDevice.getInstance(instrumentation)

    /**
     * Start an activity, by default the default launch of the package, and wait until
     * its launch completes.
     *
     * @throws IllegalStateException if unable to acquire intent for package.
     *
     * @param block Allows customization of the intent used to launch the activity.
     */
    public fun startActivityAndWait(
        block: (Intent) -> Unit = {}
    ) {
        val intent = context.packageManager.getLaunchIntentForPackage(packageName)
            ?: throw IllegalStateException("Unable to acquire intent for package $packageName")

        block(intent)
        startActivityAndWait(intent)
    }

    /**
     * Start an activity with the provided intent, and wait until its launch completes.
     *
     * @param intent Specifies which app/Activity should be launched.
     */
    public fun startActivityAndWait(intent: Intent) {
        // Must launch with new task, as we're not launching from an existing task
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        if (launchWithClearTask) {
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
        }
        try {
            context.startActivity(intent)
        } catch (securityException: SecurityException) {
            // Android 11 sets exported=false by default, which means we can't launch, but this
            // can also happen if "android:exported=false" is used directly.
            throw SecurityException(
                "Unable to launch Activity due to Security Exception. To launch an " +
                    "activity from a benchmark, you may need to set android:exported=true " +
                    "for the Activity in your application's manifest",
                securityException
            )
        }

        waitOnPackageLaunch()
    }

    /**
     * Wait on the current package to complete an Activity launch.
     *
     * Note that [timeoutInSeconds] is for full Activity launch, and UiAutomator detection of
     * Activity content. This is not just Activity launch time as would be reported by
     * [StartupTimingMetric] - it must include additional fixed time.
     *
     * As an example, when this timeout was 5 seconds, a 2 second activity launch would
     * frequently hit the timeout. This timeout should be conservatively large to encapsulate
     * any slow app / hardware combo.
     */
    internal fun waitOnPackageLaunch(timeoutInSeconds: Long = 30) {
        val timeoutInMilliseconds = TimeUnit.SECONDS.toMillis(timeoutInSeconds)

        // Note: if this wait starts during an activity launch, it will wait until the launch
        // completes. This is why it's safe to simply check package - even if launching from one
        // activity to another within the package, the launch has to fully complete.

        // Note though, that this wait does not wait for within-activity launch behavior to
        // complete. that must be done separately.
        val packageIsDisplayed = device.wait(
            Until.hasObject(By.pkg(packageName).depth(0)),
            timeoutInMilliseconds
        )
        if (!packageIsDisplayed) {
            throw IllegalStateException(
                "Unable to detect Activity of package $packageName after " +
                    "$timeoutInSeconds second timeout. Did it fail to launch?"
            )
        }
    }

    /**
     * Perform a home button click.
     *
     * Useful for resetting the test to a base condition in cases where the app isn't killed in
     * each iteration.
     */
    public fun pressHome(delayDurationMs: Long = 300) {
        device.pressHome()
        Thread.sleep(delayDurationMs)
    }

    /**
     * Force-stop the process being measured.
     */
    public fun killProcess() {
        Log.d(TAG, "Killing process $packageName")
        device.executeShellCommand("am force-stop $packageName")
    }

    /**
     * Drop Kernel's in-memory cache of disk pages.
     *
     * Enables measuring disk-based startup cost, without simply accessing cache of disk data
     * held in memory, such as during [cold startup](androidx.benchmark.macro.StartupMode.COLD).
     */
    public fun dropKernelPageCache() {
        val result = device.executeShellScript(
            "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure"
        ).trim()
        // User builds don't allow drop caches yet.
        if (result != "Success") {
            Log.w(TAG, "Failed to drop kernel page cache, result: '$result'")
        }
    }
}