1 /* 2 * Copyright (C) 2017 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 17 package com.android.server.pm; 18 19 import android.app.AlarmManager; 20 import android.content.Context; 21 import android.os.Environment; 22 import android.os.SystemProperties; 23 import android.os.storage.StorageManager; 24 import android.support.test.InstrumentationRegistry; 25 import android.util.Log; 26 27 import org.junit.After; 28 import org.junit.AfterClass; 29 import org.junit.Assert; 30 import org.junit.Before; 31 import org.junit.BeforeClass; 32 import org.junit.Test; 33 import org.junit.runner.RunWith; 34 import org.junit.runners.JUnit4; 35 36 import java.io.File; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.InputStreamReader; 40 import java.util.concurrent.TimeUnit; 41 42 /** 43 * Integration tests for {@link BackgroundDexOptService}. 44 * 45 * Tests various scenarios around BackgroundDexOptService. 46 * 1. Under normal conditions, check that dexopt upgrades test app to 47 * $(getprop pm.dexopt.bg-dexopt). 48 * 2. Under low storage conditions and package is unused, check 49 * that dexopt downgrades test app to $(getprop pm.dexopt.inactive). 50 * 3. Under low storage conditions and package is recently used, check 51 * that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt). 52 * 53 * Each test case runs "cmd package bg-dexopt-job com.android.frameworks.bgdexopttest". 54 * 55 * The setup for these tests make sure this package has been configured to have been recently used 56 * plus installed far enough in the past. If a test case requires that this package has not been 57 * recently used, it sets the time forward more than 58 * `getprop pm.dexopt.downgrade_after_inactive_days` days. 59 * 60 * For tests that require low storage, the phone is filled up. 61 * 62 * Run with "atest BackgroundDexOptServiceIntegrationTests". 63 */ 64 @RunWith(JUnit4.class) 65 public final class BackgroundDexOptServiceIntegrationTests { 66 67 private static final String TAG = BackgroundDexOptServiceIntegrationTests.class.getSimpleName(); 68 69 // Name of package to test on. 70 private static final String PACKAGE_NAME = "com.android.frameworks.bgdexopttest"; 71 // Name of file used to fill up storage. 72 private static final String BIG_FILE = "bigfile"; 73 private static final String BG_DEXOPT_COMPILER_FILTER = SystemProperties.get( 74 "pm.dexopt.bg-dexopt"); 75 private static final String DOWNGRADE_COMPILER_FILTER = SystemProperties.get( 76 "pm.dexopt.inactive"); 77 private static final long DOWNGRADE_AFTER_DAYS = SystemProperties.getLong( 78 "pm.dexopt.downgrade_after_inactive_days", 0); 79 // Needs to be between 1.0 and 2.0. 80 private static final double LOW_STORAGE_MULTIPLIER = 1.5; 81 82 // The file used to fill up storage. 83 private File mBigFile; 84 85 // Remember start time. 86 @BeforeClass setUpAll()87 public static void setUpAll() { 88 if (!SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false)) { 89 throw new RuntimeException( 90 "bg-dexopt is not disabled (set pm.dexopt.disable_bg_dexopt to true)"); 91 } 92 if (DOWNGRADE_AFTER_DAYS < 1) { 93 throw new RuntimeException( 94 "pm.dexopt.downgrade_after_inactive_days must be at least 1"); 95 } 96 if ("quicken".equals(BG_DEXOPT_COMPILER_FILTER)) { 97 throw new RuntimeException("pm.dexopt.bg-dexopt should not be \"quicken\""); 98 } 99 if ("quicken".equals(DOWNGRADE_COMPILER_FILTER)) { 100 throw new RuntimeException("pm.dexopt.inactive should not be \"quicken\""); 101 } 102 } 103 104 getContext()105 private static Context getContext() { 106 return InstrumentationRegistry.getTargetContext(); 107 } 108 109 @Before setUp()110 public void setUp() throws IOException { 111 File dataDir = getContext().getDataDir(); 112 mBigFile = new File(dataDir, BIG_FILE); 113 } 114 115 @After tearDown()116 public void tearDown() { 117 if (mBigFile.exists()) { 118 boolean result = mBigFile.delete(); 119 if (!result) { 120 throw new RuntimeException("Couldn't delete big file"); 121 } 122 } 123 } 124 125 // Return the content of the InputStream as a String. inputStreamToString(InputStream is)126 private static String inputStreamToString(InputStream is) throws IOException { 127 char[] buffer = new char[1024]; 128 StringBuilder builder = new StringBuilder(); 129 try (InputStreamReader reader = new InputStreamReader(is)) { 130 for (; ; ) { 131 int count = reader.read(buffer, 0, buffer.length); 132 if (count < 0) { 133 break; 134 } 135 builder.append(buffer, 0, count); 136 } 137 } 138 return builder.toString(); 139 } 140 141 // Run the command and return the stdout. runShellCommand(String cmd)142 private static String runShellCommand(String cmd) throws IOException { 143 Log.i(TAG, String.format("running command: '%s'", cmd)); 144 long startTime = System.nanoTime(); 145 Process p = Runtime.getRuntime().exec(cmd); 146 int res; 147 try { 148 res = p.waitFor(); 149 } catch (InterruptedException e) { 150 throw new RuntimeException(e); 151 } 152 String stdout = inputStreamToString(p.getInputStream()); 153 String stderr = inputStreamToString(p.getErrorStream()); 154 long elapsedTime = System.nanoTime() - startTime; 155 Log.i(TAG, String.format("ran command: '%s' in %d ms with return code %d", cmd, 156 TimeUnit.NANOSECONDS.toMillis(elapsedTime), res)); 157 Log.i(TAG, "stdout"); 158 Log.i(TAG, stdout); 159 Log.i(TAG, "stderr"); 160 Log.i(TAG, stderr); 161 if (res != 0) { 162 throw new RuntimeException(String.format("failed command: '%s'", cmd)); 163 } 164 return stdout; 165 } 166 167 // Run the command and return the stdout split by lines. runShellCommandSplitLines(String cmd)168 private static String[] runShellCommandSplitLines(String cmd) throws IOException { 169 return runShellCommand(cmd).split("\n"); 170 } 171 172 // Return the compiler filter of a package. getCompilerFilter(String pkg)173 private static String getCompilerFilter(String pkg) throws IOException { 174 String cmd = String.format("dumpsys package %s", pkg); 175 String[] lines = runShellCommandSplitLines(cmd); 176 final String substr = "[status="; 177 for (String line : lines) { 178 int startIndex = line.indexOf(substr); 179 if (startIndex < 0) { 180 continue; 181 } 182 startIndex += substr.length(); 183 int endIndex = line.indexOf(']', startIndex); 184 return line.substring(startIndex, endIndex); 185 } 186 throw new RuntimeException("Couldn't find compiler filter in dumpsys package"); 187 } 188 189 // Return the number of bytes available in the data partition. getDataDirUsableSpace()190 private static long getDataDirUsableSpace() { 191 return Environment.getDataDirectory().getUsableSpace(); 192 } 193 194 // Fill up the storage until there are bytesRemaining number of bytes available in the data 195 // partition. Writes to the current package's data directory. fillUpStorage(long bytesRemaining)196 private void fillUpStorage(long bytesRemaining) throws IOException { 197 Log.i(TAG, String.format("Filling up storage with %d bytes remaining", bytesRemaining)); 198 logSpaceRemaining(); 199 long numBytesToAdd = getDataDirUsableSpace() - bytesRemaining; 200 String cmd = String.format("fallocate -l %d %s", numBytesToAdd, mBigFile.getAbsolutePath()); 201 runShellCommand(cmd); 202 logSpaceRemaining(); 203 } 204 205 // Fill up storage so that device is in low storage condition. fillUpToLowStorage()206 private void fillUpToLowStorage() throws IOException { 207 fillUpStorage((long) (getStorageLowBytes() * LOW_STORAGE_MULTIPLIER)); 208 } 209 210 // TODO(aeubanks): figure out how to get scheduled bg-dexopt to run runBackgroundDexOpt()211 private static void runBackgroundDexOpt() throws IOException { 212 runShellCommand("cmd package bg-dexopt-job " + PACKAGE_NAME); 213 } 214 215 // Set the time ahead of the last use time of the test app in days. setTimeFutureDays(long futureDays)216 private static void setTimeFutureDays(long futureDays) { 217 setTimeFutureMillis(TimeUnit.DAYS.toMillis(futureDays)); 218 } 219 220 // Set the time ahead of the last use time of the test app in milliseconds. setTimeFutureMillis(long futureMillis)221 private static void setTimeFutureMillis(long futureMillis) { 222 long currentTime = System.currentTimeMillis(); 223 setTime(currentTime + futureMillis); 224 } 225 setTime(long time)226 private static void setTime(long time) { 227 AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 228 am.setTime(time); 229 } 230 231 // Return the number of free bytes when the data partition is considered low on storage. getStorageLowBytes()232 private static long getStorageLowBytes() { 233 StorageManager storageManager = (StorageManager) getContext().getSystemService( 234 Context.STORAGE_SERVICE); 235 return storageManager.getStorageLowBytes(Environment.getDataDirectory()); 236 } 237 238 // Log the amount of space remaining in the data directory. logSpaceRemaining()239 private static void logSpaceRemaining() throws IOException { 240 runShellCommand("df -h /data"); 241 } 242 243 // Compile the given package with the given compiler filter. compilePackageWithFilter(String pkg, String filter)244 private static void compilePackageWithFilter(String pkg, String filter) throws IOException { 245 runShellCommand(String.format("cmd package compile -f -m %s %s", filter, pkg)); 246 } 247 248 // Test that background dexopt under normal conditions succeeds. 249 @Test testBackgroundDexOpt()250 public void testBackgroundDexOpt() throws IOException { 251 // Set filter to quicken. 252 compilePackageWithFilter(PACKAGE_NAME, "verify"); 253 Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME)); 254 255 runBackgroundDexOpt(); 256 257 // Verify that bg-dexopt is successful. 258 Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 259 } 260 261 // Test that background dexopt under low storage conditions upgrades used packages. 262 @Test testBackgroundDexOptDowngradeSkipRecentlyUsedPackage()263 public void testBackgroundDexOptDowngradeSkipRecentlyUsedPackage() throws IOException { 264 // Should be less than DOWNGRADE_AFTER_DAYS. 265 long deltaDays = DOWNGRADE_AFTER_DAYS - 1; 266 try { 267 // Set time to future. 268 setTimeFutureDays(deltaDays); 269 270 // Set filter to quicken. 271 compilePackageWithFilter(PACKAGE_NAME, "quicken"); 272 Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME)); 273 274 // Fill up storage to trigger low storage threshold. 275 fillUpToLowStorage(); 276 277 runBackgroundDexOpt(); 278 279 // Verify that downgrade did not happen. 280 Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 281 } finally { 282 // Reset time. 283 setTimeFutureDays(-deltaDays); 284 } 285 } 286 287 // Test that background dexopt under low storage conditions downgrades unused packages. 288 @Test testBackgroundDexOptDowngradeSuccessful()289 public void testBackgroundDexOptDowngradeSuccessful() throws IOException { 290 // Should be more than DOWNGRADE_AFTER_DAYS. 291 long deltaDays = DOWNGRADE_AFTER_DAYS + 1; 292 try { 293 // Set time to future. 294 setTimeFutureDays(deltaDays); 295 296 // Set filter to quicken. 297 compilePackageWithFilter(PACKAGE_NAME, "quicken"); 298 Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME)); 299 300 // Fill up storage to trigger low storage threshold. 301 fillUpToLowStorage(); 302 303 runBackgroundDexOpt(); 304 305 // Verify that downgrade is successful. 306 Assert.assertEquals(DOWNGRADE_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 307 } finally { 308 // Reset time. 309 setTimeFutureDays(-deltaDays); 310 } 311 } 312 313 } 314