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