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