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.content.pm.ApplicationInfo
20 import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE
21 import android.content.pm.ApplicationInfo.FLAG_SYSTEM
22 import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
23 import android.content.pm.PackageManager
24 import android.os.Build
25 import androidx.annotation.RestrictTo
26 import androidx.benchmark.Arguments
27 import androidx.benchmark.ConfigurationError
28 import androidx.benchmark.DeviceInfo
29 import androidx.benchmark.ExperimentalBenchmarkConfigApi
30 import androidx.benchmark.ExperimentalConfig
31 import androidx.benchmark.InstrumentationResults
32 import androidx.benchmark.Profiler
33 import androidx.benchmark.ResultWriter
34 import androidx.benchmark.Shell
35 import androidx.benchmark.checkAndGetSuppressionState
36 import androidx.benchmark.conditionalError
37 import androidx.benchmark.createInsightSummaries
38 import androidx.benchmark.inMemoryTrace
39 import androidx.benchmark.json.BenchmarkData
40 import androidx.benchmark.macro.MacrobenchmarkScope.KillMode
41 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig
42 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
43 import androidx.benchmark.traceprocessor.TraceProcessor
44 import androidx.test.platform.app.InstrumentationRegistry
45 import org.junit.Assume.assumeFalse
46 
47 /** Get package ApplicationInfo, throw if not found. */
48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
49 @Suppress("DEPRECATION")
50 fun getInstalledPackageInfo(packageName: String): ApplicationInfo {
51     val pm = InstrumentationRegistry.getInstrumentation().context.packageManager
52     try {
53         return pm.getApplicationInfo(packageName, 0)
54     } catch (notFoundException: PackageManager.NameNotFoundException) {
55         throw AssertionError(
56             "Unable to find target package $packageName, is it installed?",
57             notFoundException
58         )
59     }
60 }
61 
62 /** @return `true` if the [ApplicationInfo] instance is referring to a system app. */
63 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
isSystemAppnull64 fun ApplicationInfo.isSystemApp(): Boolean {
65     return flags and (FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP) > 0
66 }
67 
checkErrorsnull68 internal fun checkErrors(packageName: String): ConfigurationError.SuppressionState? {
69     Arguments.throwIfError()
70 
71     val applicationInfo = getInstalledPackageInfo(packageName)
72 
73     val instrumentation = InstrumentationRegistry.getInstrumentation()
74     val errors =
75         DeviceInfo.errors +
76             // TODO: Merge this debuggable check / definition with Errors.kt in benchmark-common
77             listOfNotNull(
78                     conditionalError(
79                         hasError = applicationInfo.flags.and(FLAG_DEBUGGABLE) != 0,
80                         id = "DEBUGGABLE",
81                         summary = "Benchmark Target is Debuggable",
82                         message =
83                             """
84                     Target package $packageName is running with debuggable=true in its manifest,
85                     which drastically reduces runtime performance in order to support debugging
86                     features. Run benchmarks with debuggable=false. Debuggable affects execution
87                     speed in ways that mean benchmark improvements might not carry over to a
88                     real user's experience (or even regress release performance).
89                 """
90                                 .trimIndent()
91                     ),
92                     conditionalError(
93                         // Profileable is currently only needed on API 29+30, since app trace tag no
94                         // longer
95                         // requires profileable on API 31, and macrobench doesn't currently offer
96                         // other
97                         // means of profiling (like simpleperf) that need the flag.
98                         hasError =
99                             DeviceInfo.profileableEnforced &&
100                                 Build.VERSION.SDK_INT in 29..30 &&
101                                 applicationInfo.isNotProfileableByShell(),
102                         id = "NOT-PROFILEABLE",
103                         summary = "Benchmark Target is NOT profileable",
104                         message =
105                             """
106                     Target package $packageName is running without <profileable shell=true>.
107                     Profileable is required on Android 10 & 11 to enable macrobenchmark to capture
108                     detailed trace information from the target process, such as System tracing
109                     sections defined in the app, or libraries.
110 
111                     To make the target profileable, add the following in your target app's
112                     main AndroidManifest.xml, within the application tag:
113 
114                     <!--suppress AndroidElementNotAllowed -->
115                     <profileable android:shell="true"/>
116                 """
117                                 .trimIndent()
118                     ),
119                     conditionalError(
120                         hasError =
121                             instrumentation.targetContext.packageName !=
122                                 instrumentation.context.packageName,
123                         id = "NOT-SELF-INSTRUMENTING",
124                         summary = "Benchmark manifest is instrumenting separate process",
125                         message =
126                             """
127                     Macrobenchmark instrumentation target in manifest
128                     ${instrumentation.targetContext.packageName} does not match macrobenchmark
129                     package ${instrumentation.context.packageName}. While macrobenchmarks 'target' a
130                     separate app they measure, they can not declare it as their instrumentation
131                     targetPackage in their manifest. Doing so would cause the macrobenchmark test
132                     app to be loaded into the target application process, which would prevent
133                     macrobenchmark from killing, compiling, or launching the target process.
134 
135                     Ensure your macrobenchmark test apk's manifest matches the manifest package, and
136                     instrumentation target package, also called 'self-instrumenting':
137 
138                     <manifest
139                         package="com.mymacrobenchpackage" ...>
140                         <instrumentation
141                             android:name="androidx.benchmark.junit4.AndroidBenchmarkRunner"
142                             android:targetPackage="mymacrobenchpackage"/>
143 
144                     In gradle library modules, this is the default behavior. In gradle test modules,
145                     specify the experimental self-instrumenting property:
146                     android {
147                         targetProjectPath = ":app"
148                         // Enable the benchmark to run separately from the app process
149                         experimentalProperties["android.experimental.self-instrumenting"] = true
150                     }
151                 """
152                                 .trimIndent()
153                     ),
154                     conditionalError(
155                         hasError = DeviceInfo.misconfiguredForTracing,
156                         id = "DEVICE-TRACING-MISCONFIGURED",
157                         summary = "This ${DeviceInfo.typeLabel}'s OS is misconfigured for tracing",
158                         message =
159                             """
160                     This ${DeviceInfo.typeLabel}'s OS image has not correctly mounted the tracing
161                     file system, which prevents macrobenchmarking, and Perfetto/atrace trace capture
162                     in general. You can try a different device, or experiment with an emulator
163                     (though that will not give timing measurements representative of real device
164                     experience).
165                     This error may not be suppressed.
166                 """
167                                 .trimIndent()
168                     ),
169                     conditionalError(
170                         hasError = Arguments.macrobenchMethodTracingEnabled(),
171                         id = "METHOD-TRACING-ENABLED",
172                         summary = "Method tracing is enabled during a Macrobenchmark",
173                         message =
174                             """
175                     The Macrobenchmark run for $packageName has method tracing enabled.
176                     This causes the VM to run more slowly than usual, so the metrics from the
177                     trace files should only be considered in relative terms
178                     (e.g. was run #1 faster than run #2). Also, these metrics cannot be compared
179                     with benchmark runs that don't have method tracing enabled.
180                 """
181                                 .trimIndent()
182                     ),
183                 )
184                 .sortedBy { it.id }
185 
186     // These error ids are really warnings. In that, we don't need developers to have to
187     // explicitly suppress them using test instrumentation arguments.
188     // TODO: Introduce a better way to surface warnings.
189     val alwaysSuppressed = setOf("METHOD-TRACING-ENABLED")
190     val neverSuppressed = setOf("DEVICE-TRACING-MISCONFIGURED")
191 
192     return errors.checkAndGetSuppressionState(
193         Arguments.suppressedErrors + alwaysSuppressed - neverSuppressed
194     )
195 }
196 
197 /**
198  * macrobenchmark test entrypoint, which doesn't depend on JUnit.
199  *
200  * This function is a building block for public testing APIs
201  */
202 @ExperimentalBenchmarkConfigApi
macrobenchmarknull203 private fun macrobenchmark(
204     uniqueName: String,
205     className: String,
206     testName: String,
207     packageName: String,
208     metrics: List<Metric>,
209     compilationMode: CompilationMode,
210     iterations: Int,
211     launchWithClearTask: Boolean,
212     startupModeMetricHint: StartupMode?,
213     experimentalConfig: ExperimentalConfig?,
214     perfettoSdkConfig: PerfettoSdkConfig?,
215     setupBlock: MacrobenchmarkScope.() -> Unit,
216     measureBlock: MacrobenchmarkScope.() -> Unit
217 ): BenchmarkData.TestResult {
218     require(iterations > 0) { "Require iterations > 0 (iterations = $iterations)" }
219     require(metrics.isNotEmpty()) {
220         "Empty list of metrics passed to metrics param, must pass at least one Metric"
221     }
222 
223     // When running on emulator and argument `skipOnEmulator` is passed, the test is skipped.
224     if (Arguments.skipBenchmarksOnEmulator) {
225         assumeFalse(
226             "Skipping test because it's running on emulator and `skipOnEmulator` is enabled",
227             DeviceInfo.isEmulator
228         )
229     }
230 
231     val suppressionState = checkErrors(packageName)
232     var warningMessage = suppressionState?.warningMessage ?: ""
233     // skip benchmark if not supported by vm settings
234     compilationMode.assumeSupportedWithVmSettings()
235 
236     val startTime = System.nanoTime()
237     // Ensure method tracing is explicitly enabled and that we are not running in dry run mode.
238     val requestMethodTracing = Arguments.macrobenchMethodTracingEnabled()
239     val scope = MacrobenchmarkScope(packageName, launchWithClearTask = launchWithClearTask)
240 
241     // Ensure the device is awake
242     scope.device.wakeUp()
243 
244     // Stop Background Dexopt during a Macrobenchmark to improve stability.
245     if (Build.VERSION.SDK_INT >= 33) {
246         scope.cancelBackgroundDexopt()
247     }
248 
249     // Always kill the process at beginning of test
250     scope.killProcess()
251 
252     inMemoryTrace("compile $packageName") {
253         compilationMode.resetAndCompile(scope) {
254             setupBlock(scope)
255             measureBlock(scope)
256         }
257     }
258 
259     // package name for macrobench process, so it's captured as well
260     val macrobenchPackageName = InstrumentationRegistry.getInstrumentation().context.packageName
261     val iterationResults = mutableListOf<IterationResult>()
262 
263     TraceProcessor.runServer {
264         scope.withKillMode(
265             current = KillMode.None,
266             override =
267                 KillMode(clearArtRuntimeImage = compilationMode.requiresClearArtRuntimeImage())
268         ) {
269             // Measurement Phase
270             iterationResults +=
271                 runPhase(
272                     uniqueName = uniqueName,
273                     packageName = packageName,
274                     macrobenchmarkPackageName = macrobenchPackageName,
275                     iterations = if (Arguments.dryRunMode) 1 else iterations,
276                     startupMode = startupModeMetricHint,
277                     scope = scope,
278                     profiler = null, // Don't profile when measuring
279                     metrics = metrics,
280                     experimentalConfig = experimentalConfig,
281                     perfettoSdkConfig = perfettoSdkConfig,
282                     setupBlock = setupBlock,
283                     measureBlock = measureBlock
284                 )
285             // Profiling Phase
286             if (requestMethodTracing) {
287                 iterationResults +=
288                     runPhase(
289                         uniqueName = uniqueName,
290                         packageName = packageName,
291                         macrobenchmarkPackageName = macrobenchPackageName,
292                         // We should open up an API to control the number of iterations here.
293                         // Run profiling for 1 additional iteration.
294                         iterations = 1,
295                         startupMode = startupModeMetricHint,
296                         scope = scope,
297                         profiler = MethodTracingProfiler(scope),
298                         metrics = emptyList(), // Nothing to measure
299                         experimentalConfig = experimentalConfig,
300                         perfettoSdkConfig = perfettoSdkConfig,
301                         setupBlock = setupBlock,
302                         measureBlock = measureBlock
303                     )
304             }
305         }
306     }
307 
308     // Merge measurements
309     val measurements = iterationResults.map { it.measurements }.mergeMultiIterResults()
310     require(measurements.isNotEmpty()) {
311         """
312             Unable to read any metrics during benchmark (metric list: $metrics).
313             Check that you're performing the operations to be measured. For example, if
314             using StartupTimingMetric, are you starting an activity for the specified package
315             in the measure block?
316         """
317             .trimIndent()
318     }
319 
320     val iterationTracePaths = iterationResults.map { it.tracePath }
321     val profilerResults = iterationResults.flatMap { it.profilerResultFiles }
322     InstrumentationResults.instrumentationReport {
323         reportSummaryToIde(
324             warningMessage = warningMessage,
325             testName = uniqueName,
326             measurements = measurements,
327             insightSummaries = iterationResults.flatMap { it.insights }.createInsightSummaries(),
328             iterationTracePaths = iterationTracePaths,
329             profilerResults = profilerResults,
330             useTreeDisplayFormat = experimentalConfig?.startupInsightsConfig?.isEnabled == true
331         )
332 
333         warningMessage = "" // warning only printed once
334         measurements.singleMetrics.forEach {
335             it.putInBundle(bundle, suppressionState?.prefix ?: "")
336         }
337         measurements.sampledMetrics.forEach {
338             it.putPercentilesInBundle(bundle, suppressionState?.prefix ?: "")
339         }
340     }
341 
342     val warmupIterations =
343         when (compilationMode) {
344             is CompilationMode.Partial -> compilationMode.warmupIterations
345             else -> 0
346         }
347 
348     val mergedProfilerOutputs =
349         (iterationTracePaths.mapIndexed { index, it ->
350                 Profiler.ResultFile.ofPerfettoTrace(
351                     label = "Trace Iteration $index",
352                     absolutePath = it
353                 )
354             } + profilerResults)
355             .map { BenchmarkData.TestResult.ProfilerOutput(it) }
356 
357     val testResult =
358         BenchmarkData.TestResult(
359             className = className,
360             name = testName,
361             totalRunTimeNs = System.nanoTime() - startTime,
362             metrics = measurements.singleMetrics + measurements.sampledMetrics,
363             repeatIterations = iterations,
364             thermalThrottleSleepSeconds = 0,
365             warmupIterations = warmupIterations,
366             profilerOutputs = mergedProfilerOutputs
367         )
368     ResultWriter.appendTestResult(testResult)
369     return testResult
370 }
371 
372 /** Run a macrobenchmark with the specified StartupMode */
373 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
374 @ExperimentalBenchmarkConfigApi
macrobenchmarkWithStartupModenull375 fun macrobenchmarkWithStartupMode(
376     uniqueName: String,
377     className: String,
378     testName: String,
379     packageName: String,
380     metrics: List<Metric>,
381     compilationMode: CompilationMode,
382     iterations: Int,
383     experimentalConfig: ExperimentalConfig?,
384     startupMode: StartupMode?,
385     setupBlock: MacrobenchmarkScope.() -> Unit,
386     measureBlock: MacrobenchmarkScope.() -> Unit
387 ): BenchmarkData.TestResult {
388     val perfettoSdkConfig =
389         if (Arguments.perfettoSdkTracingEnable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
390             PerfettoSdkConfig(
391                 packageName,
392                 when (startupMode) {
393                     null -> InitialProcessState.Unknown
394                     StartupMode.COLD -> InitialProcessState.NotAlive
395                     StartupMode.HOT,
396                     StartupMode.WARM -> InitialProcessState.Alive
397                 }
398             )
399         } else null
400     return macrobenchmark(
401         uniqueName = uniqueName,
402         className = className,
403         testName = testName,
404         packageName = packageName,
405         metrics = metrics,
406         compilationMode = compilationMode,
407         iterations = iterations,
408         startupModeMetricHint = startupMode,
409         experimentalConfig = experimentalConfig,
410         perfettoSdkConfig = perfettoSdkConfig,
411         setupBlock = {
412             if (startupMode == StartupMode.COLD) {
413                 // Run setup before killing process
414                 setupBlock(this)
415 
416                 // Shader caches are stored in the code cache directory. Make sure that
417                 // they are cleared every iteration. Must be done before kill, since on user builds
418                 // this broadcasts to the target app
419                 dropShaderCache()
420 
421                 // Kill - code below must not wake process!
422                 killProcess()
423 
424                 // Ensure app's pages are not cached in memory for a true _cold_ start.
425                 dropKernelPageCache()
426 
427                 // validate process is not running just before returning
428                 check(!Shell.isPackageAlive(packageName)) {
429                     "Package $packageName must not be running prior to cold start!"
430                 }
431             } else {
432                 if (iteration == 0 && startupMode != null) {
433                     try {
434                         iteration = null // override to null for warmup
435 
436                         // warmup process by running the measure block once unmeasured
437                         setupBlock(this)
438                         measureBlock()
439                     } finally {
440                         iteration = 0 // resume counting
441                     }
442                 }
443                 setupBlock(this)
444             }
445         },
446         // Don't reuse activities by default in COLD / WARM
447         launchWithClearTask = startupMode == StartupMode.COLD || startupMode == StartupMode.WARM,
448         measureBlock = measureBlock
449     )
450 }
451