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