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