1 /* 2 * Copyright (C) 2018 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 package android.platform.test.microbenchmark; 17 18 import static android.content.Context.BATTERY_SERVICE; 19 import static android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY; 20 import static android.os.BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER; 21 22 import android.os.BatteryManager; 23 import android.os.Bundle; 24 import android.os.SystemClock; 25 import android.platform.test.composer.Iterate; 26 import android.platform.test.rule.DynamicRuleChain; 27 import android.platform.test.rule.TracePointRule; 28 import android.util.Log; 29 import androidx.annotation.VisibleForTesting; 30 import androidx.test.InstrumentationRegistry; 31 32 import java.lang.annotation.ElementType; 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.lang.annotation.Target; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 42 import org.junit.internal.AssumptionViolatedException; 43 import org.junit.internal.runners.model.EachTestNotifier; 44 import org.junit.internal.runners.model.ReflectiveCallable; 45 import org.junit.internal.runners.statements.RunAfters; 46 import org.junit.rules.TestRule; 47 import org.junit.runner.Description; 48 import org.junit.runner.notification.RunNotifier; 49 import org.junit.runners.BlockJUnit4ClassRunner; 50 import org.junit.runners.model.FrameworkMethod; 51 import org.junit.runners.model.InitializationError; 52 import org.junit.runners.model.Statement; 53 import org.junit.rules.RunRules; 54 55 /** 56 * The {@code Microbenchmark} runner allows you to run individual JUnit {@code @Test} methods 57 * repeatedly like a benchmark test in order to get more repeatable, reliable measurements, or to 58 * ensure it passes when run many times. This runner supports a number of customized features that 59 * enable better benchmarking and better integration with surrounding tools. 60 * 61 * <p>One iteration represents a single pass of a test method. The number of iterations and how 62 * those iterations get renamed are configurable with options listed at the top of the source code. 63 * Some custom annotations also exist to enable this runner to replicate JUnit's functionality with 64 * modified behaviors related to running repeated methods. These are documented separately below 65 * with the corresponding annotations, {@link @NoMetricBefore}, {@link @NoMetricAfter}, and 66 * {@link @TightMethodRule}. 67 * 68 * <p>Finally, this runner supports some power-specific testing features used to denoise (also 69 * documented below), and can be configured to terminate early if the battery drops too low or if 70 * any test fails. 71 */ 72 public class Microbenchmark extends BlockJUnit4ClassRunner { 73 74 private static final String LOG_TAG = Microbenchmark.class.getSimpleName(); 75 76 private static final Statement EMPTY_STATEMENT = 77 new Statement() { 78 @Override 79 public void evaluate() throws Throwable {} 80 }; 81 82 // Use these options to inject rules at runtime via the command line. For details, please see 83 // documentation for DynamicRuleChain. 84 @VisibleForTesting static final String DYNAMIC_OUTER_CLASS_RULES_OPTION = "outer-class-rules"; 85 @VisibleForTesting static final String DYNAMIC_INNER_CLASS_RULES_OPTION = "inner-class-rules"; 86 @VisibleForTesting static final String DYNAMIC_OUTER_TEST_RULES_OPTION = "outer-test-rules"; 87 @VisibleForTesting static final String DYNAMIC_INNER_TEST_RULES_OPTION = "inner-test-rules"; 88 89 // Renames repeated test methods as <description><separator><iteration> (if set to true). 90 public static final String RENAME_ITERATION_OPTION = "rename-iterations"; 91 @VisibleForTesting static final String ITERATION_SEP_OPTION = "iteration-separator"; 92 @VisibleForTesting static final String ITERATION_SEP_DEFAULT = "$"; 93 94 // Stop running tests after any failure is encountered (if set to true). 95 private static final String TERMINATE_ON_TEST_FAIL_OPTION = "terminate-on-test-fail"; 96 97 // Don't start new iterations if the battery falls below this value (if set). 98 @VisibleForTesting static final String MIN_BATTERY_LEVEL_OPTION = "min-battery"; 99 // Don't start new iterations if the battery already fell more than this value (if set). 100 @VisibleForTesting static final String MAX_BATTERY_DRAIN_OPTION = "max-battery-drain"; 101 // Options for aligning with the battery charge (coulomb) counter for power tests. We want to 102 // start microbenchmarks just after the coulomb counter has decremented to account for the 103 // counter being quantized. The counter most accurately reflects the true value just after it 104 // decrements. 105 private static final String ALIGN_WITH_CHARGE_COUNTER_OPTION = "align-with-charge-counter"; 106 private static final String COUNTER_DECREMENT_TIMEOUT_OPTION = "counter-decrement-timeout_ms"; 107 108 private final String mIterationSep; 109 private final Bundle mArguments; 110 private final boolean mRenameIterations; 111 private final int mMinBatteryLevel; 112 private final int mMaxBatteryDrain; 113 private final int mCounterDecrementTimeoutMs; 114 private final boolean mAlignWithChargeCounter; 115 private final boolean mTerminateOnTestFailure; 116 private final Map<Description, Integer> mIterations = new HashMap<>(); 117 private int mStartBatteryLevel; 118 119 private final BatteryManager mBatteryManager; 120 121 /** 122 * Called reflectively on classes annotated with {@code @RunWith(Microbenchmark.class)}. 123 */ Microbenchmark(Class<?> klass)124 public Microbenchmark(Class<?> klass) throws InitializationError { 125 this(klass, InstrumentationRegistry.getArguments()); 126 } 127 128 /** Do not call. Called explicitly from tests to provide an arguments. */ 129 @VisibleForTesting Microbenchmark(Class<?> klass, Bundle arguments)130 Microbenchmark(Class<?> klass, Bundle arguments) throws InitializationError { 131 super(klass); 132 mArguments = arguments; 133 // Parse out additional options. 134 mRenameIterations = Boolean.parseBoolean(arguments.getString(RENAME_ITERATION_OPTION)); 135 mIterationSep = 136 arguments.containsKey(ITERATION_SEP_OPTION) 137 ? arguments.getString(ITERATION_SEP_OPTION) 138 : ITERATION_SEP_DEFAULT; 139 mMinBatteryLevel = Integer.parseInt(arguments.getString(MIN_BATTERY_LEVEL_OPTION, "-1")); 140 mMaxBatteryDrain = Integer.parseInt(arguments.getString(MAX_BATTERY_DRAIN_OPTION, "100")); 141 mCounterDecrementTimeoutMs = 142 Integer.parseInt(arguments.getString(COUNTER_DECREMENT_TIMEOUT_OPTION, "30000")); 143 mAlignWithChargeCounter = 144 Boolean.parseBoolean( 145 arguments.getString(ALIGN_WITH_CHARGE_COUNTER_OPTION, "false")); 146 147 mTerminateOnTestFailure = 148 Boolean.parseBoolean( 149 arguments.getString(TERMINATE_ON_TEST_FAIL_OPTION, "false")); 150 151 // Get the battery manager for later use. 152 mBatteryManager = 153 (BatteryManager) 154 InstrumentationRegistry.getContext().getSystemService(BATTERY_SERVICE); 155 } 156 157 @Override run(final RunNotifier notifier)158 public void run(final RunNotifier notifier) { 159 if (mAlignWithChargeCounter) { 160 // Try to wait until the coulomb counter has just decremented to start the test. 161 int startChargeCounter = getBatteryChargeCounter(); 162 long startTimestamp = SystemClock.uptimeMillis(); 163 while (startChargeCounter == getBatteryChargeCounter()) { 164 if (SystemClock.uptimeMillis() - startTimestamp > mCounterDecrementTimeoutMs) { 165 Log.d( 166 LOG_TAG, 167 "Timed out waiting for the counter to change. Continuing anyway."); 168 break; 169 } else { 170 Log.d( 171 LOG_TAG, 172 String.format( 173 "Charge counter still reads: %d. Waiting.", 174 startChargeCounter)); 175 SystemClock.sleep(getCounterPollingInterval()); 176 } 177 } 178 } 179 Log.d(LOG_TAG, String.format("The charge counter reads: %d.", getBatteryChargeCounter())); 180 181 mStartBatteryLevel = getBatteryLevel(); 182 183 super.run(notifier); 184 } 185 186 /** 187 * Returns a {@link Statement} that invokes {@code method} on {@code test}, surrounded by any 188 * explicit or command-line-supplied {@link TightMethodRule}s. This allows for tighter {@link 189 * TestRule}s that live inside {@link Before} and {@link After} statements. 190 */ 191 @Override methodInvoker(FrameworkMethod method, Object test)192 protected Statement methodInvoker(FrameworkMethod method, Object test) { 193 // Iterate on the test method multiple times for more data. If unset, defaults to 1. 194 Iterate<Statement> methodIterator = new Iterate<Statement>(); 195 methodIterator.setOptionName("method-iterations"); 196 final List<Statement> testMethodStatement = 197 methodIterator.apply( 198 mArguments, 199 Arrays.asList(new Statement[] {super.methodInvoker(method, test)})); 200 Statement start = 201 new Statement() { 202 @Override 203 public void evaluate() throws Throwable { 204 for (Statement method : testMethodStatement) { 205 method.evaluate(); 206 } 207 } 208 }; 209 // Wrap the multiple-iteration test method with trace points. 210 start = getTracePointRule().apply(start, describeChild(method)); 211 // Invoke special @TightMethodRules that wrap @Test methods. 212 List<TestRule> tightMethodRules = 213 getTestClass().getAnnotatedFieldValues(test, TightMethodRule.class, TestRule.class); 214 for (TestRule tightMethodRule : tightMethodRules) { 215 start = tightMethodRule.apply(start, describeChild(method)); 216 } 217 return start; 218 } 219 220 @VisibleForTesting getTracePointRule()221 protected TracePointRule getTracePointRule() { 222 return new TracePointRule(); 223 } 224 225 /** 226 * Returns a list of repeated {@link FrameworkMethod}s to execute. 227 */ 228 @Override getChildren()229 protected List<FrameworkMethod> getChildren() { 230 return new Iterate<FrameworkMethod>().apply(mArguments, super.getChildren()); 231 } 232 233 /** 234 * An annotation for the corresponding tight rules above. These rules are ordered differently 235 * from standard JUnit {@link Rule}s because they live between {@link Before} and {@link After} 236 * methods, instead of wrapping those methods. 237 * 238 * <p>In particular, these serve as a proxy for tight metric collection in microbenchmark-style 239 * tests, where collection is isolated to just the method under test. This is important for when 240 * {@link Before} and {@link After} methods will obscure signal reliability. 241 * 242 * <p>Currently these are only registered from inside a test class as follows, but should soon 243 * be extended for command-line support. 244 * 245 * ``` 246 * @RunWith(Microbenchmark.class) 247 * public class TestClass { 248 * @TightMethodRule 249 * public ExampleRule exampleRule = new ExampleRule(); 250 * 251 * @Test ... 252 * } 253 * ``` 254 */ 255 @Retention(RetentionPolicy.RUNTIME) 256 @Target({ElementType.FIELD, ElementType.METHOD}) 257 public @interface TightMethodRule {} 258 259 /** 260 * A temporary annotation that acts like the {@code @Before} but is excluded from metric 261 * collection. 262 * 263 * <p>This should be removed as soon as possible. Do not use this unless explicitly instructed 264 * to do so. You'll regret it! 265 * 266 * <p>Note that all {@code TestOption}s must be instantiated as {@code @ClassRule}s to work 267 * inside these annotations. 268 */ 269 @Retention(RetentionPolicy.RUNTIME) 270 @Target({ElementType.FIELD, ElementType.METHOD}) 271 public @interface NoMetricBefore {} 272 273 /** A temporary annotation, same as the above, but for replacing {@code @After} methods. */ 274 @Retention(RetentionPolicy.RUNTIME) 275 @Target({ElementType.FIELD, ElementType.METHOD}) 276 public @interface NoMetricAfter {} 277 278 /** 279 * Rename the child class name to add iterations if the renaming iteration option is enabled. 280 * 281 * <p>Renaming the class here is chosen over renaming the method name because 282 * 283 * <ul> 284 * <li>Conceptually, the runner is running a class multiple times, as opposed to a method. 285 * <li>When instrumenting a suite in command line, by default the instrumentation command 286 * outputs the class name only. Renaming the class helps with interpretation in this case. 287 */ 288 @Override describeChild(FrameworkMethod method)289 protected Description describeChild(FrameworkMethod method) { 290 Description original = super.describeChild(method); 291 if (!mRenameIterations) { 292 return original; 293 } 294 return Description.createTestDescription( 295 String.join(mIterationSep, original.getClassName(), 296 String.valueOf(mIterations.get(original))), original.getMethodName()); 297 } 298 299 /** Re-implement the private rules wrapper from {@link BlockJUnit4ClassRunner} in JUnit 4.12. */ withRules(FrameworkMethod method, Object target, Statement statement)300 private Statement withRules(FrameworkMethod method, Object target, Statement statement) { 301 Statement result = statement; 302 List<TestRule> testRules = new ArrayList<>(); 303 // Inner dynamic rules should be included first because RunRules applies rules inside-out. 304 testRules.add(new DynamicRuleChain(DYNAMIC_INNER_TEST_RULES_OPTION, mArguments)); 305 testRules.addAll(getTestRules(target)); 306 testRules.add(new DynamicRuleChain(DYNAMIC_OUTER_TEST_RULES_OPTION, mArguments)); 307 // Apply legacy MethodRules, if they don't overlap with TestRules. 308 for (org.junit.rules.MethodRule each : rules(target)) { 309 if (!testRules.contains(each)) { 310 result = each.apply(result, method, target); 311 } 312 } 313 // Apply modern, method-level TestRules in outer statements. 314 result = new RunRules(result, testRules, describeChild(method)); 315 return result; 316 } 317 318 /** Add {@link DynamicRuleChain} to existing class rules. */ 319 @Override classRules()320 protected List<TestRule> classRules() { 321 List<TestRule> classRules = new ArrayList<>(); 322 // Inner dynamic class rules should be included first because RunRules applies rules inside 323 // -out. 324 classRules.add(new DynamicRuleChain(DYNAMIC_INNER_CLASS_RULES_OPTION, mArguments)); 325 classRules.addAll(super.classRules()); 326 classRules.add(new DynamicRuleChain(DYNAMIC_OUTER_CLASS_RULES_OPTION, mArguments)); 327 return classRules; 328 } 329 330 /** 331 * Combine the {@code #runChild}, {@code #methodBlock}, and final {@code #runLeaf} methods to 332 * implement the specific {@code Microbenchmark} test behavior. In particular, (1) keep track of 333 * the number of iterations for a particular method description, and (2) run {@code 334 * NoMetricBefore} and {@code NoMetricAfter} methods outside of the {@code RunListener} test 335 * wrapping methods. 336 */ 337 @Override runChild(final FrameworkMethod method, RunNotifier notifier)338 protected void runChild(final FrameworkMethod method, RunNotifier notifier) { 339 if (isBatteryLevelBelowMin()) { 340 throw new TerminateEarlyException("the battery level is below the threshold."); 341 } else if (isBatteryDrainAboveMax()) { 342 throw new TerminateEarlyException("the battery drain is above the threshold."); 343 } 344 345 // Update the number of iterations this method has been run. 346 if (mRenameIterations) { 347 Description original = super.describeChild(method); 348 mIterations.computeIfPresent(original, (k, v) -> v + 1); 349 mIterations.computeIfAbsent(original, k -> 1); 350 } 351 352 Description description = describeChild(method); 353 if (isIgnored(method)) { 354 notifier.fireTestIgnored(description); 355 } else { 356 EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description); 357 358 Object test; 359 try { 360 // Fail fast if the test is not successfully created. 361 test = 362 new ReflectiveCallable() { 363 @Override 364 protected Object runReflectiveCall() throws Throwable { 365 return createTest(); 366 } 367 }.run(); 368 369 // Run {@code NoMetricBefore} methods first. Fail fast if they fail. 370 for (FrameworkMethod noMetricBefore : 371 getTestClass().getAnnotatedMethods(NoMetricBefore.class)) { 372 noMetricBefore.invokeExplosively(test); 373 } 374 } catch (Throwable e) { 375 eachNotifier.fireTestStarted(); 376 eachNotifier.addFailure(e); 377 eachNotifier.fireTestFinished(); 378 if(mTerminateOnTestFailure) { 379 throw new TerminateEarlyException("test failed."); 380 } 381 return; 382 } 383 384 Statement statement = methodInvoker(method, test); 385 statement = possiblyExpectingExceptions(method, test, statement); 386 statement = withPotentialTimeout(method, test, statement); 387 statement = withBefores(method, test, statement); 388 statement = withAfters(method, test, statement); 389 statement = withRules(method, test, statement); 390 391 boolean testFailed = false; 392 // Fire test events from inside to exclude "no metric" methods. 393 eachNotifier.fireTestStarted(); 394 try { 395 statement.evaluate(); 396 } catch (AssumptionViolatedException e) { 397 eachNotifier.addFailedAssumption(e); 398 testFailed = true; 399 } catch (Throwable e) { 400 eachNotifier.addFailure(e); 401 testFailed = true; 402 } finally { 403 eachNotifier.fireTestFinished(); 404 } 405 406 try { 407 // Run {@code NoMetricAfter} methods last, reporting all errors. 408 List<FrameworkMethod> afters = 409 getTestClass().getAnnotatedMethods(NoMetricAfter.class); 410 if (!afters.isEmpty()) { 411 new RunAfters(EMPTY_STATEMENT, afters, test).evaluate(); 412 } 413 } catch (AssumptionViolatedException e) { 414 eachNotifier.addFailedAssumption(e); 415 testFailed = true; 416 } catch (Throwable e) { 417 eachNotifier.addFailure(e); 418 testFailed = true; 419 } 420 421 if(mTerminateOnTestFailure && testFailed) { 422 throw new TerminateEarlyException("test failed."); 423 } 424 } 425 } 426 427 /* Checks if the battery level is below the specified level where the test should terminate. */ isBatteryLevelBelowMin()428 private boolean isBatteryLevelBelowMin() { 429 return getBatteryLevel() < mMinBatteryLevel; 430 } 431 432 /* Checks if the battery level has drained enough to where the test should terminate. */ isBatteryDrainAboveMax()433 private boolean isBatteryDrainAboveMax() { 434 return mStartBatteryLevel - getBatteryLevel() > mMaxBatteryDrain; 435 } 436 437 /* Gets the current battery level (as a percentage). */ 438 @VisibleForTesting getBatteryLevel()439 public int getBatteryLevel() { 440 return mBatteryManager.getIntProperty(BATTERY_PROPERTY_CAPACITY); 441 } 442 443 /* Gets the current battery charge counter (coulomb counter). */ 444 @VisibleForTesting getBatteryChargeCounter()445 public int getBatteryChargeCounter() { 446 return mBatteryManager.getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER); 447 } 448 449 /* Gets the polling interval to check for changes in the battery charge counter. */ 450 @VisibleForTesting getCounterPollingInterval()451 public long getCounterPollingInterval() { 452 return 100; 453 } 454 455 /** 456 * A {@code RuntimeException} class for terminating test runs early for some specified reason. 457 */ 458 @VisibleForTesting 459 static class TerminateEarlyException extends RuntimeException { TerminateEarlyException(String message)460 public TerminateEarlyException(String message) { 461 super(String.format("Terminating early because %s", message)); 462 } 463 } 464 465 } 466