• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.rule;
17 
18 import static java.util.stream.Collectors.joining;
19 import static java.util.stream.Collectors.toSet;
20 
21 import android.os.SystemClock;
22 import android.util.Log;
23 import androidx.annotation.VisibleForTesting;
24 
25 import com.google.common.collect.ImmutableList;
26 
27 import org.junit.runner.Description;
28 
29 import java.util.Arrays;
30 import java.util.List;
31 import java.util.Set;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * A rule that stresses the device by running dex2oat in the background.
37  *
38  * <p>The rule takes a list of packages from command-line arguments and compiles them in a loop
39  * until the test framework informs it to stop via finished(), using a compilation filter supplied
40  * also via command-line args, or {@code speed} by default.
41  */
42 public class Dex2oatPressureRule extends TestWatcher {
43     public static final String LOG_TAG = Dex2oatPressureRule.class.getSimpleName();
44 
45     // Packages names for running dex2oat as a list.
46     public static final String PACKAGES_OPTION = "dex2oat-stressor-pkgs";
47 
48     // Option for filter to compile the packages with, and some associated variables.
49     public static final String COMPILATION_FILTER_OPTION = "dex2oat-stressor-compilation-filter";
50     public static final String SPEED_FILTER = "speed";
51     public static final ImmutableList<String> SUPPORTED_FILTERS_LIST =
52             ImmutableList.of(SPEED_FILTER, "quicken", "verify");
53 
54     // A switch for turning the stressor off for quick a/b comparisons.
55     public static final String ENABLE_OPTION = "dex2oat-stressor-enable";
56     private boolean mEnabled = false;
57 
58     @VisibleForTesting public static final String DEX2OAT_RUNNING_CHECK_COMMAND = "pgrep dex2oat";
59     public static final String LIST_PACKAGES_COMMAND = "pm list packages";
60     private static final String COMPILE_COMMAND_TEMPLATE = "cmd package compile -f -m %s %s";
61     private static final String COMPILE_COMMAND_SUCCESS_RESPONSE = "Success";
62 
63     private static final long DEX2OAT_POLLING_INTERVAL = TimeUnit.MILLISECONDS.toMillis(50);
64     private static final long DEX2OAT_POLLING_TIMEOUT = TimeUnit.SECONDS.toMillis(5);
65 
66     private Dex2oatRunnable mDex2oatTask;
67     private Thread mDex2oatThread;
68 
69     @Override
starting(Description description)70     protected void starting(Description description) {
71         mEnabled = Boolean.valueOf(getArguments().getString(ENABLE_OPTION, String.valueOf(false)));
72         if (!mEnabled) {
73             return;
74         }
75 
76         if (!getArguments().containsKey(PACKAGES_OPTION)) {
77             throw new IllegalArgumentException(
78                     String.format(
79                             "Please supply a comma-separated list of packages to compile via the "
80                                     + "%s option. The rule will have no effect and results "
81                                     + "should be discarded.",
82                             PACKAGES_OPTION));
83         }
84         List<String> packagesToCompile =
85                 Arrays.asList(getArguments().getString(PACKAGES_OPTION).split(","));
86         Set<String> installedPackages = getInstalledPackages();
87         if (!packagesToCompile.stream().allMatch(pkg -> installedPackages.contains(pkg))) {
88             throw new IllegalArgumentException(
89                     String.format(
90                             "The following supplied packages are not installed on the device and "
91                                     + "can't be compiled: %s. Results should be discarded.",
92                             packagesToCompile
93                                     .stream()
94                                     .filter(pkg -> !installedPackages.contains(pkg))
95                                     .collect(joining(", "))));
96         }
97 
98         String compilationFilter =
99                 getArguments().getString(COMPILATION_FILTER_OPTION, SPEED_FILTER);
100         if (!SUPPORTED_FILTERS_LIST.contains(compilationFilter)) {
101             throw new IllegalArgumentException(
102                     String.format(
103                             "Invalid compilation filter %s. Please supply a compilation filter "
104                                     + "from the following: %s. Results should be discarded.",
105                             compilationFilter, String.join(", ", SUPPORTED_FILTERS_LIST)));
106         }
107 
108         mDex2oatTask = new Dex2oatRunnable(packagesToCompile, compilationFilter);
109         mDex2oatThread = new Thread(mDex2oatTask);
110         mDex2oatThread.start();
111 
112         // Wait until dex2oat is running.
113         long pollingStartTime = System.currentTimeMillis();
114         while (System.currentTimeMillis() - pollingStartTime <= DEX2OAT_POLLING_TIMEOUT) {
115             if (dex2oatIsRunning()) {
116                 return;
117             }
118             SystemClock.sleep(DEX2OAT_POLLING_INTERVAL);
119         }
120         // If we reach here, dex2oat still isn't running. At this point we should cancel our
121         // attempts and throw to prevent inaccurate results.
122         stopDex2oatAndWaitForFinish();
123         throw new IllegalStateException(
124                 String.format(
125                         "dex2oat still isn't running after %d ms. Results should be discarded.",
126                         DEX2OAT_POLLING_TIMEOUT));
127     }
128 
129     @Override
finished(Description description)130     protected void finished(Description description) {
131         if (!mEnabled) {
132             return;
133         }
134         // If the thread is null, dex2oat had never been triggered. No further actions are needed.
135         if (mDex2oatThread == null) {
136             return;
137         }
138 
139         stopDex2oatAndWaitForFinish();
140     }
141 
stopDex2oatAndWaitForFinish()142     private void stopDex2oatAndWaitForFinish() {
143         stopDex2oat();
144         try {
145             mDex2oatThread.join();
146         } catch (InterruptedException e) {
147             throw new RuntimeException(e);
148         }
149     }
150 
151     /** Get all installed packages on the device. Leaving visible for stubbing. */
152     @VisibleForTesting
getInstalledPackages()153     protected Set<String> getInstalledPackages() {
154         String response = executeShellCommand(LIST_PACKAGES_COMMAND);
155         return Arrays.asList(response.split("\n"))
156                 .stream()
157                 .map(line -> line.replace("package:", "").trim())
158                 .collect(toSet());
159     }
160 
161     /** Stop the dex2oat thread. Leaving visible for stubbing. */
162     @VisibleForTesting
stopDex2oat()163     protected void stopDex2oat() {
164         mDex2oatTask.pleaseStop();
165     }
166 
167     /** Run the actual compilation command. Enclosed in a separate method for testing. */
168     @VisibleForTesting
runCompileCommand(String pkg, String filter)169     protected void runCompileCommand(String pkg, String filter) {
170         String response =
171                 executeShellCommand(String.format(COMPILE_COMMAND_TEMPLATE, filter, pkg)).trim();
172         if (!response.equalsIgnoreCase(COMPILE_COMMAND_SUCCESS_RESPONSE)) {
173             // Log but do not throw on the failure, so that other packages can continue.
174             Log.w(
175                     LOG_TAG,
176                     String.format(
177                             "Compilation for package %s and filter %s failed with response %s.",
178                             pkg, filter, response));
179         }
180     }
181 
182     /** Check if dex2oat is running. Enclosed in a separate method for testing. */
183     @VisibleForTesting
dex2oatIsRunning()184     protected boolean dex2oatIsRunning() {
185         String dex2oatPid = executeShellCommand(DEX2OAT_RUNNING_CHECK_COMMAND).trim();
186         if (!dex2oatPid.isEmpty()) {
187             Log.i(
188                     LOG_TAG,
189                     String.format("dex2oat is now running and has a process ID %s.", dex2oatPid));
190             return true;
191         }
192         return false;
193     }
194 
195     private class Dex2oatRunnable implements Runnable {
196         private final ImmutableList<String> mPackagesToCompile;
197         private final String mCompilationFilter;
198         private AtomicBoolean mShouldContinue = new AtomicBoolean(true);
199 
Dex2oatRunnable(List<String> packagesToCompile, String compilationFilter)200         public Dex2oatRunnable(List<String> packagesToCompile, String compilationFilter) {
201             mPackagesToCompile = ImmutableList.copyOf(packagesToCompile);
202             mCompilationFilter = compilationFilter;
203         }
204 
205         @Override
run()206         public void run() {
207             while (mShouldContinue.get()) {
208                 for (String pkg : mPackagesToCompile) {
209                     runCompileCommand(pkg, mCompilationFilter);
210                     if (!mShouldContinue.get()) {
211                         break;
212                     }
213                 }
214             }
215         }
216 
pleaseStop()217         public void pleaseStop() {
218             mShouldContinue.set(false);
219         }
220     }
221 }
222