1 /* 2 * Copyright (C) 2016 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.performance.tests; 18 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.config.OptionClass; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.testtype.IDeviceTest; 26 import com.android.tradefed.testtype.IRemoteTest; 27 import com.android.tradefed.util.AaptParser; 28 import com.android.tradefed.util.RunUtil; 29 import com.android.tradefed.util.proto.TfMetricProtoUtil; 30 31 import java.io.File; 32 import java.util.ArrayList; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Map; 36 37 @OptionClass(alias = "app-install-perf") 38 // Test framework that measures the install time for all apk files located under a given directory. 39 // The test needs aapt to be in its path in order to determine the package name of the apk. The 40 // package name is needed to clean up after the test is done. 41 public class AppInstallTest implements IDeviceTest, IRemoteTest { 42 43 @Option( 44 name = "test-apk-dir", 45 description = "Directory that contains the test apks.", 46 mandatory = true 47 ) 48 private File mTestApkPath; 49 50 @Option(name = "test-label", description = "Unique test identifier label.") 51 private String mTestLabel = "AppInstallPerformance"; 52 53 @Option( 54 name = "test-start-delay", 55 description = "Delay in ms to wait for before starting the install test." 56 ) 57 private long mTestStartDelay = 60000; 58 59 // TODO: remove this once prod is updated. 60 @Option(name = "test-use-dex-metedata") 61 private boolean mUseDexMetadataMisspelled = false; 62 @Option(name = "test-dex-metedata-variant") 63 private String mDexMetadataVariantMisspelled = ""; 64 65 @Option( 66 name = "test-use-dex-metadata", 67 description = "If the test should install the dex metadata files." 68 ) 69 private boolean mUseDexMetadata = false; 70 71 @Option( 72 name = "test-delay-between-installs", 73 description = "Delay in ms to wait for before starting the install test." 74 ) 75 private long mTestDelayBetweenInstalls = 5000; 76 77 @Option( 78 name = "test-dex-metadata-variant", 79 description = 80 "The dex metadata variant that should be used." 81 + "When specified, the DM file name for foo.apk will be " 82 + "constructed as fooVARIANT.dm" 83 ) 84 private String mDexMetadataVariant = ""; 85 86 @Option(name = "test-uninstall-after", description = "If the apk should be uninstalled after.") 87 private boolean mUninstallAfter = true; 88 89 @Option( 90 name = "package-list", 91 description = 92 "If given, filters the apk files in the test dir based on the list of " 93 + "packages. It checks that the apk name is packageName-version.apk" 94 ) 95 private List<String> mPackages = new ArrayList<>(); 96 97 private ITestDevice mDevice; 98 99 /* 100 * {@inheritDoc} 101 */ 102 @Override setDevice(ITestDevice device)103 public void setDevice(ITestDevice device) { 104 mDevice = device; 105 } 106 107 /* 108 * {@inheritDoc} 109 */ 110 @Override getDevice()111 public ITestDevice getDevice() { 112 return mDevice; 113 } 114 115 /* 116 * {@inheritDoc} 117 */ 118 @Override run(ITestInvocationListener listener)119 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 120 // Check if we need to use the obsolete, misspelled flags. 121 if (!mUseDexMetadata) { 122 mUseDexMetadata = mUseDexMetadataMisspelled; 123 } 124 if (mDexMetadataVariant.isEmpty()) { 125 mDexMetadataVariant = mDexMetadataVariantMisspelled; 126 } 127 128 // Delay test start time to give the background processes to finish. 129 if (mTestStartDelay > 0) { 130 RunUtil.getDefault().sleep(mTestStartDelay); 131 } 132 133 assert mTestApkPath.isDirectory(); 134 135 // Find all apks in directory. 136 String[] files = mTestApkPath.list(); 137 Map<String, String> metrics = new HashMap<>(); 138 try { 139 for (String fileName : files) { 140 if (!fileName.endsWith(".apk")) { 141 CLog.d("Skipping non-apk %s", fileName); 142 continue; 143 } else if (!matchesPackagesForInstall(fileName)) { 144 CLog.d("Skipping apk %s", fileName); 145 continue; 146 } 147 File file = new File(mTestApkPath, fileName); 148 // Install app and measure time. 149 long installTime = installAndTime(file); 150 if (installTime > 0) { 151 metrics.put(fileName, Long.toString(installTime)); 152 } 153 RunUtil.getDefault().sleep(mTestDelayBetweenInstalls); 154 } 155 } finally { 156 reportMetrics(listener, mTestLabel, metrics); 157 } 158 } 159 160 /** 161 * Install file and time its install time. Cleans up after itself. 162 * 163 * @param packageFile apk file to install 164 * @return install time in msecs. 165 * @throws DeviceNotAvailableException 166 */ installAndTime(File packageFile)167 long installAndTime(File packageFile) throws DeviceNotAvailableException { 168 AaptParser parser = AaptParser.parse(packageFile); 169 if (parser == null) { 170 CLog.e("Failed to parse %s", packageFile); 171 return -1; 172 } 173 String packageName = parser.getPackageName(); 174 175 String remotePath = "/data/local/tmp/" + packageFile.getName(); 176 if (!mDevice.pushFile(packageFile, remotePath)) { 177 CLog.e("Failed to push %s", packageFile); 178 return -1; 179 } 180 181 String dmRemotePath = null; 182 if (mUseDexMetadata) { 183 File dexMetadataFile = getDexMetadataFile(packageFile); 184 dmRemotePath = "/data/local/tmp/" + dexMetadataFile.getName(); 185 if (!mDevice.pushFile(dexMetadataFile, dmRemotePath)) { 186 CLog.e("Failed to push %s", dexMetadataFile); 187 return -1; 188 } 189 } 190 191 long start = System.currentTimeMillis(); 192 193 // Create install session. 194 String output = mDevice.executeShellCommand("pm install-create -r -d -g"); 195 if (!checkSuccess(output, packageFile, "install-create")) { 196 return -1; 197 } 198 String session = sessionFromInstallCreateOutput(output); 199 200 // Write the files to the session 201 output = 202 mDevice.executeShellCommand( 203 String.format( 204 "pm install-write %s %s %s", session, "base.apk", remotePath)); 205 if (!checkSuccess(output, packageFile, "install-write base.apk")) { 206 return -1; 207 } 208 209 if (mUseDexMetadata) { 210 output = 211 mDevice.executeShellCommand( 212 String.format( 213 "pm install-write %s %s %s", session, "base.dm", dmRemotePath)); 214 if (!checkSuccess(output, packageFile, "install-write base.dm")) { 215 return -1; 216 } 217 } 218 219 // Commit the session. 220 output = mDevice.executeShellCommand(String.format("pm install-commit %s", session)); 221 222 long end = System.currentTimeMillis(); 223 if (!checkSuccess(output, packageFile, "install-commit")) { 224 return -1; 225 } 226 227 // Remove the temp files. 228 mDevice.executeShellCommand(String.format("rm \"%s\"", remotePath)); 229 if (mUseDexMetadata) { 230 mDevice.executeShellCommand(String.format("rm \"%s\"", dmRemotePath)); 231 } 232 233 // Uninstall the package if needed. 234 if (mUninstallAfter && packageName != null) { 235 CLog.d("Uninstalling: %s", packageName); 236 mDevice.uninstallPackage(packageName); 237 } 238 return end - start; 239 } 240 241 /** 242 * Report run metrics by creating an empty test run to stick them in 243 * 244 * @param listener the {@link ITestInvocationListener} of test results 245 * @param runName the test name 246 * @param metrics the {@link Map} that contains metrics for the given test 247 */ reportMetrics( ITestInvocationListener listener, String runName, Map<String, String> metrics)248 void reportMetrics( 249 ITestInvocationListener listener, String runName, Map<String, String> metrics) { 250 // Create an empty testRun to report the parsed runMetrics 251 CLog.d("About to report metrics: %s", metrics); 252 listener.testRunStarted(runName, 0); 253 listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(metrics)); 254 } 255 256 /** 257 * Extracts the session id from 'pm install-create' output. Usual output is: "Success: created 258 * install session [710542260]" 259 */ sessionFromInstallCreateOutput(String output)260 private String sessionFromInstallCreateOutput(String output) { 261 int start = output.indexOf("["); 262 int end = output.indexOf("]"); 263 return output.substring(start + 1, end); 264 } 265 266 /** Verifies that the output contains the "Success" mark. */ checkSuccess(String output, File packageFile, String stepForErrorLog)267 private boolean checkSuccess(String output, File packageFile, String stepForErrorLog) { 268 if (output == null || output.indexOf("Success") == -1) { 269 CLog.e( 270 "Failed to execute [%s] for package %s with error %s", 271 stepForErrorLog, packageFile, output); 272 return false; 273 } 274 return true; 275 } 276 getDexMetadataFile(File packageFile)277 private File getDexMetadataFile(File packageFile) { 278 return new File(packageFile.getAbsolutePath().replace(".apk", mDexMetadataVariant + ".dm")); 279 } 280 matchesPackagesForInstall(String fileName)281 private boolean matchesPackagesForInstall(String fileName) { 282 if (mPackages.isEmpty()) { 283 return true; 284 } 285 286 for (String pkg : mPackages) { 287 // "-" is the version delimiter and ensures we don't match for example 288 // com.google.android.apps.docs for com.google.android.apps.docs.slides. 289 if (fileName.contains(pkg + "-")) { 290 return true; 291 } 292 } 293 return false; 294 } 295 } 296