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