• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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