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