1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark.macro
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.content.Intent
22 import android.os.Build
23 import android.util.Log
24 import androidx.annotation.RequiresApi
25 import androidx.benchmark.Arguments
26 import androidx.benchmark.DeviceInfo
27 import androidx.benchmark.InstrumentationResults
28 import androidx.benchmark.Outputs
29 import androidx.benchmark.Profiler
30 import androidx.benchmark.Shell
31 import androidx.benchmark.macro.MacrobenchmarkScope.Companion.Api24ContextHelper.createDeviceProtectedStorageContextCompat
32 import androidx.benchmark.macro.perfetto.forceTrace
33 import androidx.test.platform.app.InstrumentationRegistry
34 import androidx.test.uiautomator.UiDevice
35 import androidx.tracing.trace
36 import java.io.File
37 
38 /**
39  * Provides access to common operations in app automation, such as killing the app, or navigating
40  * home.
41  */
42 public class MacrobenchmarkScope(
43     /** ApplicationId / Package name of the app being tested. */
44     val packageName: String,
45     /**
46      * Controls whether launches will automatically set [Intent.FLAG_ACTIVITY_CLEAR_TASK].
47      *
48      * Default to true, so Activity launches go through full creation lifecycle stages, instead of
49      * just resume.
50      */
51     private val launchWithClearTask: Boolean
52 ) {
53 
54     internal val instrumentation = InstrumentationRegistry.getInstrumentation()
55 
56     internal val context = instrumentation.context
57 
58     /** The per-iteration file label used as a prefix when storing Macrobenchmark results. */
59     internal lateinit var fileLabel: String
60 
61     /**
62      * Controls if the process will be launched with method tracing turned on.
63      *
64      * Default to false, because we only want to turn on method tracing when explicitly enabled via
65      * `Arguments.methodTracingOptions`.
66      */
67     private var isMethodTracingActive: Boolean = false
68 
69     /** This is `true` iff method tracing is currently active for this benchmarking session. */
70     private var isMethodTracingSessionActive: Boolean = false
71 
72     /** Additional flags that can be used to determine the type of process kill. */
73     internal data class KillMode(
74         /**
75          * When `true`, we are not strict about the process staying dead after it was initially
76          * killed. This is useful in the context of generating a Baseline profile for System apps
77          * which end up restarting right-away after a process kill happens.
78          */
79         val isKillSoftly: Boolean = false,
80         /**
81          * When used, the app will be forced to flush its ART profiles to disk before being killed.
82          * This allows them to be later collected e.g. by a `BaselineProfile` capture, or immediate
83          * compilation by [CompilationMode.Partial] with warmupIterations.
84          */
85         val flushArtProfiles: Boolean = false,
86         /**
87          * After killing the process, clear any potential runtime image.
88          *
89          * Starting in API 34 (and below with mainline), `verify` complied apps will attempt to
90          * store initialized classes to disk directly. To consistently capture worst case `verify`
91          * performance, this means macrobenchmark must recompile the target app with `verify`.
92          *
93          * @See DeviceInfo.supportsRuntimeImages
94          */
95         val clearArtRuntimeImage: Boolean = false,
96     ) {
97         companion object {
98             /** The default kill mode. Kills the process, strictly. */
99             internal val None = KillMode()
100         }
101     }
102 
103     internal inline fun <T> withKillMode(
104         current: KillMode,
105         override: KillMode,
106         block: MacrobenchmarkScope.() -> T
107     ): T {
108         check(killMode == current) { "Expected KFM = $current, was $killMode" }
109         killMode = override
110         try {
111             return block(this)
112         } finally {
113             check(killMode == override) { "Expected KFM at end to be = $override, was $killMode" }
114             killMode = current
115         }
116     }
117 
118     internal var killMode: KillMode = KillMode.None
119         private set(value) {
120             hasFlushedArtProfiles = false
121             field = value
122         }
123 
124     /**
125      * When `true`, the app has successfully flushed art profiles at least once.
126      *
127      * This will only be set by [killProcessAndFlushArtProfiles] when called directly, or
128      * [killProcess] when [KillMode.flushArtProfiles] is used.
129      */
130     internal var hasFlushedArtProfiles: Boolean = false
131         private set
132 
133     /**
134      * When `true`, the app has attempted to flush the runtime image during [killProcess].
135      *
136      * This will only be set by [killProcess] when [KillMode.clearArtRuntimeImage] is used.
137      */
138     internal var hasClearedRuntimeImage: Boolean = false
139         private set
140 
141     /** `true` if the app is a system app. */
142     internal val isSystemApp: Boolean = getInstalledPackageInfo(packageName).isSystemApp()
143 
144     /**
145      * Current Macrobenchmark measurement iteration, or null if measurement is not yet enabled.
146      *
147      * Non-measurement iterations can occur due to warmup a [CompilationMode], or prior to the first
148      * iteration for [StartupMode.WARM] or [StartupMode.HOT], to create the Process or Activity
149      * ahead of time.
150      */
151     @get:Suppress("AutoBoxing") // low frequency, non-perf-relevant part of test
152     var iteration: Int? = null
153         internal set
154 
155     /**
156      * The list of method traces accumulated during a benchmarking session. The [Pair] has the label
157      * and the absolute path to the trace. These should be reported at the end of a Macro
158      * benchmarking session, if method tracing was on.
159      */
160     private val methodTraces: MutableList<Pair<String, String>> = mutableListOf()
161 
162     /**
163      * Get the [UiDevice] instance, to use in reading target app UI state, or interacting with the
164      * UI via touches, scrolls, or other inputs.
165      *
166      * Convenience for `UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())`
167      */
168     val device: UiDevice = UiDevice.getInstance(instrumentation)
169 
170     /**
171      * Start an activity, by default the launcher activity of the package, and wait until its launch
172      * completes.
173      *
174      * This call supports primitive extras on the intent, but will ignore any
175      * [android.os.Parcelable] extras, as the start is performed by converting the Intent to a URI,
176      * and starting via the `am start` shell command. Note that from api 33 the launch intent needs
177      * to have category `android.intent.category.LAUNCHER`.
178      *
179      * @param block Allows customization of the intent used to launch the activity.
180      * @throws IllegalStateException if unable to acquire intent for package.
181      */
182     @JvmOverloads
183     public fun startActivityAndWait(block: (Intent) -> Unit = {}) {
184         val intent =
185             context.packageManager.getLaunchIntentForPackage(packageName)
186                 ?: context.packageManager.getLeanbackLaunchIntentForPackage(packageName)
187                 ?: throw IllegalStateException("Unable to acquire intent for package $packageName")
188 
189         block(intent)
190         startActivityAndWait(intent)
191     }
192 
193     /**
194      * Start an activity with the provided intent, and wait until its launch completes.
195      *
196      * This call supports primitive extras on the intent, but will ignore any
197      * [android.os.Parcelable] extras, as the start is performed by converting the Intent to a URI,
198      * and starting via the `am start` shell command. Note that from api 33 the launch intent needs
199      * to have category `android.intent.category.LAUNCHER`.
200      *
201      * @param intent Specifies which app/Activity should be launched.
202      */
203     public fun startActivityAndWait(intent: Intent): Unit =
204         forceTrace("startActivityAndWait") {
205             // Must launch with new task, as we're not launching from an existing task
206             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
207             if (launchWithClearTask) {
208                 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
209             }
210 
211             // Note: intent.toUri(0) produces a String that can't be parsed by `am start-activity`.
212             // intent.toUri(Intent.URI_ANDROID_APP_SCHEME) also works though.
213             startActivityImpl(intent.toUri(Intent.URI_INTENT_SCHEME))
214         }
215 
216     private fun startActivityImpl(uri: String) {
217         if (
218             isMethodTracingActive &&
219                 !isMethodTracingSessionActive &&
220                 !Shell.isPackageAlive(packageName)
221         ) {
222             isMethodTracingSessionActive = true
223             // Use the canonical trace path for the given package name.
224             val tracePath = methodTraceRecordPath(packageName)
225             val profileArgs = "--start-profiler \"$tracePath\" --streaming"
226             amStartAndWait(uri, profileArgs)
227         } else {
228             amStartAndWait(uri)
229         }
230     }
231 
232     @SuppressLint("BanThreadSleep") // Cannot always detect activity launches.
233     private fun amStartAndWait(uri: String, profilingArgs: String? = null) {
234         val ignoredUniqueNames =
235             if (!launchWithClearTask) {
236                 emptyList()
237             } else {
238                 // ignore existing names, as we expect a new window
239                 getFrameStats().map { it.uniqueName }
240             }
241 
242         val preLaunchTimestampNs = System.nanoTime()
243 
244         val additionalArgs = profilingArgs ?: ""
245         val cmd = "am start $additionalArgs -W \"$uri\""
246         Log.d(TAG, "Starting activity with command: $cmd")
247 
248         // executeShellScript used to access stderr, and avoid need to escape special chars like `;`
249         val result = Shell.executeScriptCaptureStdoutStderr(cmd)
250 
251         if (result.stderr.contains("java.lang.SecurityException")) {
252             throw SecurityException(result.stderr)
253         }
254         if (result.stderr.isNotEmpty()) {
255             throw IllegalStateException(result.stderr)
256         }
257 
258         val outputLines = result.stdout.split("\n").map { it.trim() }
259 
260         // Check for errors
261         outputLines.forEach {
262             if (it.startsWith("Error:")) {
263                 throw IllegalStateException(it)
264             }
265         }
266 
267         Log.d(TAG, "Result: ${result.stdout}")
268 
269         if (outputLines.any { it.startsWith("Warning: Activity not started") }) {
270             // Intent was sent to running activity, which may not produce a new frame.
271             // Since we can't be sure, simply sleep and hope launch has completed.
272             Log.d(TAG, "Unable to safely detect Activity launch, waiting 2s")
273             Thread.sleep(2000)
274             return
275         }
276 
277         if (!Shell.isPackageAlive(packageName)) {
278             throw IllegalStateException(
279                 "Target package $packageName is not running," +
280                     " check logcat to verify the activity launched correctly"
281             )
282         }
283 
284         // `am start -W` doesn't reliably wait for process to complete and renderthread to produce
285         // a new frame (b/226179160), so we use `dumpsys gfxinfo <package> framestats` to determine
286         // when the next frame is produced.
287         var lastFrameStats: List<FrameStatsResult> = emptyList()
288         repeat(100) {
289             lastFrameStats = getFrameStats()
290             if (
291                 lastFrameStats.any {
292                     it.uniqueName !in ignoredUniqueNames &&
293                         it.lastFrameNs != null &&
294                         it.lastFrameNs > preLaunchTimestampNs
295                 }
296             ) {
297                 return // success, launch observed!
298             }
299 
300             trace("wait for $packageName to draw") {
301                 // Note - sleep must not be long enough to miss activity initial draw in 120 frame
302                 // internal ring buffer of `dumpsys gfxinfo <pkg> framestats`.
303                 Thread.sleep(100)
304             }
305         }
306         throw IllegalStateException(
307             "Unable to confirm activity launch completion $lastFrameStats" +
308                 " Please report a bug with the output of" +
309                 " `adb shell dumpsys gfxinfo $packageName framestats`"
310         )
311     }
312 
313     /**
314      * Uses `dumpsys gfxinfo <pkg> framestats` to detect the initial timestamp of the most recently
315      * completed (fully rendered) activity launch frame.
316      */
317     internal fun getFrameStats(): List<FrameStatsResult> {
318         // iterate through each subprocess, since UI may not be in primary process
319         return Shell.getRunningProcessesForPackage(packageName).flatMap { processName ->
320             val frameStatsOutput =
321                 trace("dumpsys gfxinfo framestats") {
322                     // we use framestats here because it gives us not just frame counts, but actual
323                     // timestamps for new activity starts. Frame counts would mostly work, but would
324                     // have false positives if some window of the app is still animating/rendering.
325                     Shell.executeScriptCaptureStdout("dumpsys gfxinfo $processName framestats")
326                 }
327             FrameStatsResult.parse(frameStatsOutput)
328         }
329     }
330 
331     /**
332      * Perform a home button click.
333      *
334      * Useful for resetting the test to a base condition in cases where the app isn't killed in each
335      * iteration.
336      */
337     @JvmOverloads
338     @SuppressLint("BanThreadSleep") // Defaults to no delays at all.
339     public fun pressHome(delayDurationMs: Long = 0) {
340         device.pressHome()
341 
342         // This delay is unnecessary, since UiAutomator's pressHome already waits for device idle.
343         // This sleep remains just for API stability.
344         Thread.sleep(delayDurationMs)
345     }
346 
347     /**
348      * Force-stop the process being measured.
349      *
350      * @param useKillAll should be set to `true` for System apps or pre-installed apps.
351      */
352     @Deprecated(
353         "Use the parameter-less killProcess() API instead",
354         replaceWith = ReplaceWith("killProcess()")
355     )
356     @Suppress("UNUSED_PARAMETER")
357     fun killProcess(useKillAll: Boolean = false) {
358         killProcess()
359     }
360 
361     /** Force-stop the process being measured. */
362     fun killProcess() {
363         // Method traces are only flushed is a method tracing session is active.
364         flushMethodTraces()
365 
366         if (killMode.flushArtProfiles && Build.VERSION.SDK_INT >= 24) {
367             // Flushing ART profiles will also kill the process at the end.
368             killProcessAndFlushArtProfiles()
369         } else {
370             killProcessImpl()
371             if (killMode.clearArtRuntimeImage && Build.VERSION.SDK_INT >= 24) {
372                 if (DeviceInfo.verifyClearsRuntimeImage) {
373                     // clear the runtime image
374                     CompilationMode.cmdPackageCompile(packageName, "verify")
375                 } else if (Shell.isSessionRooted()) {
376                     CompilationMode.cmdPackageCompileReset(packageName)
377                 } else {
378                     // TODO - follow up!
379                     // b/368404173
380                     InstrumentationResults.scheduleIdeWarningOnNextReport(
381                         "Unable to clear Runtime Image, subsequent launches/iterations may" +
382                             " exhibit faster startup than production due to accelerated class" +
383                             " loading."
384                     )
385                 }
386             }
387         }
388     }
389 
390     /**
391      * Deletes the shader cache for an application.
392      *
393      * Used by `measureRepeated(startupMode = StartupMode.COLD)` to remove compiled shaders for each
394      * measurement, to ensure their cost is captured each time.
395      *
396      * Requires `profileinstaller` 1.3.0-alpha02 to be used by the target, or a rooted device.
397      *
398      * @throws IllegalStateException if the device is not rooted, and the target app cannot be
399      *   signalled to drop its shader cache.
400      */
401     fun dropShaderCache() {
402         if (Arguments.dropShadersEnable) {
403             Log.d(TAG, "Dropping shader cache for $packageName")
404             val dropError = ProfileInstallBroadcast.dropShaderCache(packageName)
405             if (dropError != null && !DeviceInfo.isEmulator) {
406                 if (!dropShaderCacheRoot()) {
407                     if (Arguments.dropShadersThrowOnFailure) {
408                         throw IllegalStateException(dropError)
409                     } else {
410                         Log.d(TAG, dropError)
411                     }
412                 }
413             }
414         } else {
415             Log.d(TAG, "Skipping drop shader cache for $packageName")
416         }
417     }
418 
419     /**
420      * Returns true if rooted, and delete operation succeeded without error.
421      *
422      * Note that if no files are present in the shader dir, true will still be returned.
423      */
424     internal fun dropShaderCacheRoot(): Boolean {
425         if (Shell.isSessionRooted()) {
426             // fall back to root approach
427             val path = getShaderCachePath(packageName)
428 
429             // Use -f to allow missing files, since app may not have generated shaders.
430             Shell.executeScriptSilent("find $path -type f | xargs rm -f")
431             return true
432         }
433         return false
434     }
435 
436     /**
437      * Drop caches via setprop added in API 31
438      *
439      * Feature for dropping caches without root added in 31: https://r.android.com/1584525 Passing 3
440      * will cause caches to be dropped, and prop will go back to 0 when it's done
441      */
442     @RequiresApi(31)
443     @SuppressLint("BanThreadSleep") // Need to poll to drop kernel page caches
444     private fun dropKernelPageCacheSetProp() {
445         val result = Shell.executeScriptCaptureStdoutStderr("setprop perf.drop_caches 3")
446         check(result.stdout.isEmpty() && result.stderr.isEmpty()) {
447             "Failed to trigger drop cache via setprop: $result"
448         }
449         // Polling duration is very conservative, on Pixel 4L finishes in ~150ms
450         repeat(50) {
451             Thread.sleep(50)
452             when (val getPropResult = Shell.getprop("perf.drop_caches")) {
453                 "0" -> return // completed!
454                 "3" -> {} // not completed, continue
455                 else ->
456                     throw IllegalStateException(
457                         "Unable to drop caches: Failed to read drop cache via getprop: $getPropResult"
458                     )
459             }
460         }
461         throw IllegalStateException(
462             "Unable to drop caches: Did not observe perf.drop_caches reset automatically"
463         )
464     }
465 
466     @RequiresApi(24)
467     internal fun killProcessAndFlushArtProfiles(allowFlushWithBroadcast: Boolean = true) {
468         Log.d(TAG, "Flushing ART profiles for $packageName")
469         // For speed profile compilation, ART team recommended to wait for 5 secs when app
470         // is in the foreground, dump the profile in each process waiting an additional second each
471         // before speed-profile compilation.
472         @Suppress("BanThreadSleep") Thread.sleep(5000)
473         val saveResult =
474             if (allowFlushWithBroadcast) {
475                 ProfileInstallBroadcast.saveProfilesForAllProcesses(packageName)
476             } else {
477                 // test codepath only, force failed save result (as if broadcast receiver not
478                 // present)
479                 val processCount = Shell.getRunningPidsAndProcessesForPackage(packageName).size
480                 ProfileInstallBroadcast.SaveProfileResult(
481                     processCount = processCount,
482                     error = if (processCount == 0) null else "skipped"
483                 )
484             }
485         if (saveResult.processCount > 0) {
486             if (saveResult.error != null) {
487                 if (Shell.isSessionRooted()) {
488                     Log.d(
489                         TAG,
490                         "Unable to saveProfile with profileinstaller ($saveResult), trying kill"
491                     )
492                     val response =
493                         Shell.executeScriptCaptureStdoutStderr("killall -s SIGUSR1 $packageName")
494                     check(response.isBlank()) {
495                         "Failed to dump profile for $packageName ($response),\n" +
496                             " and failed to save profile with broadcast: ${saveResult.error}"
497                     }
498                     @SuppressLint("BanThreadSleep") Thread.sleep(Arguments.saveProfileWaitMillis)
499                 } else {
500                     // unable to flush profiles, throw
501                     throw RuntimeException(saveResult.error)
502                 }
503             }
504             // we only mark flush successful and kill the target if running processes found
505             Log.d(TAG, "Flushed profiles in ${saveResult.processCount} processes")
506             hasFlushedArtProfiles = true
507             killProcessImpl()
508         }
509     }
510 
511     /** Force-stop the process being measured. */
512     private fun killProcessImpl() {
513         val onFailure: (String) -> Unit = { errorMessage ->
514             if (killMode.isKillSoftly) {
515                 Log.w(TAG, "Unable to kill process ($packageName): $errorMessage")
516             } else {
517                 throw IllegalStateException(errorMessage)
518             }
519         }
520         Shell.killProcessesAndWait(packageName, onFailure = onFailure) {
521             val isRooted = Shell.isSessionRooted()
522             Log.d(TAG, "Killing process $packageName")
523             if (isRooted && isSystemApp) {
524                 Shell.executeScriptSilent("killall $packageName")
525             } else {
526                 // We want to use `am force-stop` for apps that are not system apps
527                 // to make sure app components are not automatically restarted by system_server.
528                 Shell.executeScriptSilent("am force-stop $packageName")
529             }
530             // System Apps need an additional Thread.sleep() to ensure that the process is killed.
531             @Suppress("BanThreadSleep") Thread.sleep(Arguments.killProcessDelayMillis)
532         }
533     }
534 
535     /**
536      * Drop Kernel's in-memory cache of disk pages.
537      *
538      * Enables measuring disk-based startup cost, without simply accessing cache of disk data held
539      * in memory, such as during [cold startup](androidx.benchmark.macro.StartupMode.COLD).
540      *
541      * @Throws IllegalStateException if dropping the cache fails on a API 31+ or rooted device,
542      *   where it is expected to work.
543      */
544     public fun dropKernelPageCache() {
545         if (Build.VERSION.SDK_INT >= 31) {
546             dropKernelPageCacheSetProp()
547         } else {
548             val result =
549                 Shell.executeScriptCaptureStdoutStderr(
550                     "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure"
551                 )
552             // Older user builds don't allow drop caches, should investigate workaround
553             if (result.stdout.trim() != "Success") {
554                 if (DeviceInfo.isRooted && !Shell.isSessionRooted()) {
555                     throw IllegalStateException("Failed to drop caches - run `adb root`")
556                 }
557                 Log.w(TAG, "Failed to drop kernel page cache, result: '$result'")
558             }
559         }
560     }
561 
562     /**
563      * Cancels the job responsible for running background `dexopt`.
564      *
565      * Background `dexopt` is a CPU intensive operation that can interfere with benchmarks. By
566      * cancelling this job, we ensure that this operation will not interfere with the benchmark, and
567      * we get stable numbers.
568      */
569     @RequiresApi(33)
570     internal fun cancelBackgroundDexopt() {
571         val result =
572             if (Build.VERSION.SDK_INT >= 34) {
573                 Shell.executeScriptCaptureStdout("pm bg-dexopt-job --cancel")
574             } else {
575                 // This command is deprecated starting Android U, and is just an alias for the
576                 // command above. More info in the link below.
577                 // https://cs.android.com/android/platform/superproject/main/+/main:art/libartservice/service/java/com/android/server/art/ArtShellCommand.java;l=123;drc=93f35d39de15c555b0ddea16121b0ee3f0aa9f91
578                 Shell.executeScriptCaptureStdout("pm cancel-bg-dexopt-job")
579             }
580         // We expect one of the following messages in stdout.
581         val expected = listOf("Success", "Background dexopt job cancelled")
582         if (expected.none { it == result.trim() }) {
583             throw IllegalStateException("Failed to cancel background dexopt job, result: '$result'")
584         }
585     }
586 
587     /** Starts method tracing for the given [packageName]. */
588     internal fun startMethodTracing() {
589         require(!isMethodTracingActive && !isMethodTracingSessionActive) {
590             "Method tracing should not already be active."
591         }
592         isMethodTracingActive = true
593         // If the process is running, start a profiling session by connecting to the process.
594         // Otherwise, given isMethodTracingActive = true, startActivityAndWait(...) will ensure
595         // that a subsequent process launch happens with tracing turned on.
596         if (Shell.isPackageAlive(packageName)) {
597             isMethodTracingSessionActive = true
598             // Use the canonical trace path for the given package name.
599             val tracePath = methodTraceRecordPath(packageName)
600             // Clock Type is only available starting Android V
601             // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ProfilerInfo.java;l=115;drc=c58be09d9273485c54d6a16defc42d9f26182b73
602             val clockType =
603                 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
604                     "--clock-type wall"
605                 } else {
606                     ""
607                 }
608             val arguments = "--streaming $clockType $packageName \"$tracePath\""
609             Shell.executeScriptSilent("am profile start $arguments")
610         }
611     }
612 
613     /**
614      * Stops a method tracing session for the provided [packageName]. This returns a list of traces
615      * accumulated in an active Method tracing session.
616      */
617     internal fun stopMethodTracing(): List<Profiler.ResultFile> {
618         require(isMethodTracingActive) {
619             "startMethodTracing() must be called prior to a call to stopMethodTracing()."
620         }
621         // Only flushes method traces when a trace session is active.
622         flushMethodTraces()
623         isMethodTracingActive = false
624         val results = methodTraces.map { Profiler.ResultFile.ofMethodTrace(it.first, it.second) }
625         methodTraces.clear()
626         return results
627     }
628 
629     /**
630      * Stops the current method tracing session and copies the output to the
631      * `additionalTestOutputDir` if a session was active.
632      */
633     private fun flushMethodTraces() {
634         if (isMethodTracingSessionActive) {
635             isMethodTracingSessionActive = false
636             val tracePath = methodTraceRecordPath(packageName)
637 
638             // We have to poll here as `am profile stop` is async, but it's hard to calibrate these
639             // numbers, as different devices take drastically different amounts of time.
640             // E.g. pixel 8 takes 100ms for it's full flush, while mokey takes 1700ms to start,
641             // then a few hundred ms to complete.
642             //
643             // Ideally, we'd use the native approach that Studio profilers use (on P+):
644             // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:transport/native/utils/activity_manager.cc;l=111;drc=a4c97db784418341c9f1be60b98ba22301b5ced8
645             Shell.waitForFileFlush(
646                 tracePath,
647                 maxInitialFlushWaitIterations = 50, // up to 2.5 sec of waiting on flush to start
648                 maxStableFlushWaitIterations = 50, // up to 2.5 sec of waiting on flush to complete
649                 stableIterations = 8, // 400ms of stability after flush starts
650                 pollDurationMs = 50L
651             ) {
652                 Shell.executeScriptSilent("am profile stop $packageName")
653             }
654             // unique label so source is clear, dateToFileName so each run of test is unique on host
655             val outputFileName = "$fileLabel-methodTracing-${Outputs.dateToFileName()}.trace"
656             val stagingFile =
657                 File.createTempFile("methodTrace", null, Outputs.dirUsableByAppAndShell)
658             // Staging location before we write it again using Outputs.writeFile(...)
659             // NOTE: staging copy may be unnecessary if we just use a single `cp`
660             Shell.cp(from = tracePath, to = stagingFile.absolutePath)
661 
662             // Report file
663             val outputPath =
664                 Outputs.writeFile(outputFileName) {
665                     Log.d(TAG, "Writing method traces to ${it.absolutePath}")
666                     stagingFile.copyTo(it, overwrite = true)
667 
668                     // Cleanup
669                     stagingFile.delete()
670                     Shell.rm(tracePath)
671                 }
672             val traceLabel = "MethodTrace iteration ${iteration ?: 0}"
673             // Keep track of the label and the corresponding output paths.
674             methodTraces += traceLabel to outputPath
675         }
676     }
677 
678     internal companion object {
679         fun getShaderCachePath(packageName: String): String {
680             val context = InstrumentationRegistry.getInstrumentation().context
681 
682             // Shader paths sourced from ActivityThread.java
683             val shaderDirectory =
684                 if (Build.VERSION.SDK_INT >= 34) {
685                     // U switched to cache dir, so it's not deleted on each app update
686                     context.createDeviceProtectedStorageContextCompat().cacheDir
687                 } else if (Build.VERSION.SDK_INT >= 24) {
688                     // shaders started using device protected storage context once it was added in N
689                     context.createDeviceProtectedStorageContextCompat().codeCacheDir
690                 } else {
691                     // getCodeCacheDir was added in L, but not used by platform for shaders until M
692                     // as M is minApi of this library, that's all we support here
693                     context.codeCacheDir
694                 }
695             return shaderDirectory.absolutePath.replace(context.packageName, packageName)
696         }
697 
698         /** Path for method trace during record, before fully flushed/stopped, move to outputs */
699         fun methodTraceRecordPath(packageName: String): String {
700             return "/data/local/tmp/$packageName-method.trace"
701         }
702 
703         @RequiresApi(Build.VERSION_CODES.N)
704         internal object Api24ContextHelper {
705             fun Context.createDeviceProtectedStorageContextCompat(): Context =
706                 createDeviceProtectedStorageContext()
707         }
708     }
709 }
710