• 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.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