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 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-incremental-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 AppIncrementalInstallTest implements IDeviceTest, IRemoteTest { 42 43 @Option( 44 name = "test-apk-dir", 45 description = "Directory that contains the test apks.", 46 mandatory = true) 47 private File mTestApkPath; 48 49 @Option(name = "test-label", description = "Unique test identifier label.") 50 private String mTestLabel = "AppIncrementalInstallPerformance"; 51 52 @Option( 53 name = "test-start-delay", 54 description = "Delay in ms to wait for before starting the install test.") 55 private long mTestStartDelay = 60000; 56 57 @Option( 58 name = "test-delay-between-installs", 59 description = "Delay in ms to wait for before starting the install test.") 60 private long mTestDelayBetweenInstalls = 5000; 61 62 @Option(name = "test-uninstall-after", description = "If the apk should be uninstalled after.") 63 private boolean mUninstallAfter = true; 64 65 @Option( 66 name = "package-list", 67 description = 68 "If given, filters the apk files in the test dir based on the list of " 69 + "packages. It checks that the apk name is packageName-version.apk") 70 private List<String> mPackages = new ArrayList<>(); 71 72 @Option( 73 name = "test-apk-remote-dir", 74 description = "Directory on the device to push artefacts.") 75 private String mTestApkRemoteDir = "/data/local/tmp/"; 76 77 private ITestDevice mDevice; 78 79 /* 80 * {@inheritDoc} 81 */ 82 @Override setDevice(ITestDevice device)83 public void setDevice(ITestDevice device) { 84 mDevice = device; 85 } 86 87 /* 88 * {@inheritDoc} 89 */ 90 @Override getDevice()91 public ITestDevice getDevice() { 92 return mDevice; 93 } 94 95 /* 96 * {@inheritDoc} 97 */ 98 @Override run(ITestInvocationListener listener)99 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 100 // Delay test start time to give the background processes to finish. 101 if (mTestStartDelay > 0) { 102 RunUtil.getDefault().sleep(mTestStartDelay); 103 } 104 105 assert mTestApkPath.isDirectory(); 106 107 // Find all apks in directory. 108 String[] files = mTestApkPath.list(); 109 Map<String, String> metrics = new HashMap<>(); 110 try { 111 for (String fileName : files) { 112 if (!fileName.endsWith(".apk")) { 113 CLog.d("Skipping non-apk %s", fileName); 114 continue; 115 } else if (!matchesPackagesForInstall(fileName)) { 116 CLog.d("Skipping apk %s", fileName); 117 continue; 118 } 119 File file = new File(mTestApkPath, fileName); 120 // Install app and measure time. 121 long installTime = installAndTime(file); 122 if (installTime > 0) { 123 metrics.put(fileName, Long.toString(installTime)); 124 } 125 RunUtil.getDefault().sleep(mTestDelayBetweenInstalls); 126 } 127 } finally { 128 reportMetrics(listener, mTestLabel, metrics); 129 } 130 } 131 132 /** 133 * Install file and time its install time. Cleans up after itself. 134 * 135 * @param packageFile apk file to install 136 * @return install time in msecs. 137 * @throws DeviceNotAvailableException 138 */ installAndTime(File packageFile)139 long installAndTime(File packageFile) throws DeviceNotAvailableException { 140 AaptParser parser = AaptParser.parse(packageFile); 141 if (parser == null) { 142 CLog.e("Failed to parse %s", packageFile); 143 return -1; 144 } 145 146 String packageName = parser.getPackageName(); 147 if (packageName == null || packageName.length() == 0) { 148 CLog.e("Failed to obtain package name %s", packageFile); 149 return -1; 150 } 151 152 String remotePath = mTestApkRemoteDir + packageFile.getName(); 153 if (!mDevice.pushFile(packageFile, remotePath)) { 154 CLog.e("Failed to push %s", packageFile); 155 return -1; 156 } 157 158 File signatureFile = getSignatureFile(packageFile); 159 String idsigRemotePath = mTestApkRemoteDir + signatureFile.getName(); 160 if (!mDevice.pushFile(signatureFile, idsigRemotePath)) { 161 CLog.e("Failed to push %s", signatureFile); 162 return -1; 163 } 164 165 long start = System.currentTimeMillis(); 166 167 // Install using incremental. 168 String output = 169 mDevice.executeShellCommand("pm install-incremental -r -d -g " + remotePath); 170 if (!checkSuccess(output, packageFile, "install-incremental")) { 171 return -1; 172 } 173 174 long end = System.currentTimeMillis(); 175 if (!checkSuccess(output, packageFile, "install-commit")) { 176 return -1; 177 } 178 179 // Remove the temp files. 180 mDevice.executeShellCommand(String.format("rm \"%s\"", remotePath)); 181 mDevice.executeShellCommand(String.format("rm \"%s\"", idsigRemotePath)); 182 183 // Uninstall the package if needed. 184 if (mUninstallAfter) { 185 CLog.d("Uninstalling: %s", packageName); 186 mDevice.uninstallPackage(packageName); 187 } 188 return end - start; 189 } 190 191 /** 192 * Report run metrics by creating an empty test run to stick them in 193 * 194 * @param listener the {@link ITestInvocationListener} of test results 195 * @param runName the test name 196 * @param metrics the {@link Map} that contains metrics for the given test 197 */ reportMetrics( ITestInvocationListener listener, String runName, Map<String, String> metrics)198 void reportMetrics( 199 ITestInvocationListener listener, String runName, Map<String, String> metrics) { 200 // Create an empty testRun to report the parsed runMetrics 201 CLog.d("About to report metrics: %s", metrics); 202 listener.testRunStarted(runName, 0); 203 listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(metrics)); 204 } 205 206 /** Verifies that the output contains the "Success" mark. */ checkSuccess(String output, File packageFile, String stepForErrorLog)207 private boolean checkSuccess(String output, File packageFile, String stepForErrorLog) { 208 if (output == null || output.indexOf("Success") == -1) { 209 CLog.e( 210 "Failed to execute [%s] for package %s with error %s", 211 stepForErrorLog, packageFile, output); 212 return false; 213 } 214 return true; 215 } 216 getSignatureFile(File packageFile)217 private File getSignatureFile(File packageFile) { 218 return new File(packageFile.getAbsolutePath().concat(".idsig")); 219 } 220 matchesPackagesForInstall(String fileName)221 private boolean matchesPackagesForInstall(String fileName) { 222 if (mPackages.isEmpty()) { 223 return true; 224 } 225 226 for (String pkg : mPackages) { 227 // "-" is the version delimiter and ensures we don't match for example 228 // com.google.android.apps.docs for com.google.android.apps.docs.slides. 229 if (fileName.contains(pkg + "-")) { 230 return true; 231 } 232 } 233 return false; 234 } 235 } 236