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