• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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