• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 libcore.heapmetrics;
18 
19 import com.android.ahat.heapdump.AhatSnapshot;
20 import com.android.ahat.heapdump.Diff;
21 import com.android.ahat.heapdump.HprofFormatException;
22 import com.android.ahat.heapdump.Parser;
23 import com.android.ahat.proguard.ProguardMap;
24 import com.android.tradefed.device.DeviceNotAvailableException;
25 import com.android.tradefed.device.ITestDevice;
26 import com.android.tradefed.result.FileInputStreamSource;
27 import com.android.tradefed.result.LogDataType;
28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
29 import com.android.tradefed.util.FileUtil;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.text.SimpleDateFormat;
34 import java.util.Date;
35 
36 /**
37  * Helper class that runs the metric instrumentations on a test device.
38  */
39 class MetricsRunner {
40 
41     private final ITestDevice testDevice;
42     private final String deviceParentDirectory;
43     private final TestLogData logs;
44     private final String timestampedLabel;
45 
46     /**
47      * Creates a helper using the given {@link ITestDevice}, uploading heap dumps to the given
48      * {@link TestLogData}.
49      */
create(ITestDevice testDevice, TestLogData logs)50     static MetricsRunner create(ITestDevice testDevice, TestLogData logs)
51             throws DeviceNotAvailableException {
52         String deviceParentDirectory =
53                 testDevice.executeShellCommand("echo -n ${EXTERNAL_STORAGE}");
54         return new MetricsRunner(testDevice, deviceParentDirectory, logs);
55     }
56 
MetricsRunner( ITestDevice testDevice, String deviceParentDirectory, TestLogData logs)57     private MetricsRunner(
58             ITestDevice testDevice, String deviceParentDirectory, TestLogData logs) {
59         this.testDevice = testDevice;
60         this.deviceParentDirectory = deviceParentDirectory;
61         this.logs = logs;
62         this.timestampedLabel = "LibcoreHeapMetricsTest-" + getCurrentTimeIso8601();
63     }
64 
65     /**
66      * Contains the results of running the instrumentation.
67      */
68     static class Result {
69 
70         private final AhatSnapshot afterDump;
71         private final int beforeTotalPssKb;
72         private final int afterTotalPssKb;
73 
Result( AhatSnapshot beforeDump, AhatSnapshot afterDump, int beforeTotalPssKb, int afterTotalPssKb)74         private Result(
75                 AhatSnapshot beforeDump, AhatSnapshot afterDump,
76                 int beforeTotalPssKb, int afterTotalPssKb) {
77             Diff.snapshots(afterDump, beforeDump);
78             this.beforeTotalPssKb = beforeTotalPssKb;
79             this.afterTotalPssKb = afterTotalPssKb;
80             this.afterDump = afterDump;
81         }
82 
83         /**
84          * Returns the parsed form of the heap dump captured when the instrumentation starts.
85          */
getBeforeDump()86         AhatSnapshot getBeforeDump() {
87             return afterDump.getBaseline();
88         }
89 
90         /**
91          * Returns the parsed form of the heap dump captured after the instrumentation action has
92          * been executed. The first heap dump will be set as the baseline for this second one.
93          */
getAfterDump()94         AhatSnapshot getAfterDump() {
95             return afterDump;
96         }
97 
98         /**
99          * Returns the PSS measured when the instrumentation starts, in kB.
100          */
getBeforeTotalPssKb()101         int getBeforeTotalPssKb() {
102             return beforeTotalPssKb;
103         }
104 
105         /**
106          * Returns the PSS measured after the instrumentation action has been executed, in kB.
107          */
getAfterTotalPssKb()108         int getAfterTotalPssKb() {
109             return afterTotalPssKb;
110         }
111     }
112 
113     /**
114      * Runs all the instrumentation and fetches the metrics.
115      *
116      * @param action The name of the action to run, to be sent as an argument to the instrumentation
117      * @return The combined results of the instrumentations.
118      */
runAllInstrumentations(String action)119     Result runAllInstrumentations(String action)
120             throws DeviceNotAvailableException, IOException, HprofFormatException {
121         String relativeDirectoryName = String.format("%s-%s", timestampedLabel, action);
122         String deviceDirectoryName =
123                 String.format("%s/%s", deviceParentDirectory, relativeDirectoryName);
124         testDevice.executeShellCommand(String.format("mkdir %s", deviceDirectoryName));
125         try {
126             runInstrumentation(
127                     action, relativeDirectoryName, deviceDirectoryName,
128                     "libcore.heapdumper/.HeapDumpInstrumentation");
129             runInstrumentation(
130                     action, relativeDirectoryName, deviceDirectoryName,
131                     "libcore.heapdumper/.PssInstrumentation");
132             AhatSnapshot beforeDump = fetchHeapDump(deviceDirectoryName, "before.hprof", action);
133             AhatSnapshot afterDump = fetchHeapDump(deviceDirectoryName, "after.hprof", action);
134             int beforeTotalPssKb = fetchTotalPssKb(deviceDirectoryName, "before.pss.txt");
135             int afterTotalPssKb = fetchTotalPssKb(deviceDirectoryName, "after.pss.txt");
136             return new Result(beforeDump, afterDump, beforeTotalPssKb, afterTotalPssKb);
137         } finally {
138             testDevice.executeShellCommand(String.format("rm -r %s", deviceDirectoryName));
139         }
140     }
141 
142     /**
143      * Runs a given instrumentation.
144      *
145      * <p>After the instrumentation has been run, checks for any reported errors and throws a
146      * {@link ApplicationException} if any are found.
147      *
148      * @param action The name of the action to run, to be sent as an argument to the instrumentation
149      * @param relativeDirectoryName The relative directory name for files on the device, to be sent
150      *     as an argument to the instrumentation
151      * @param deviceDirectoryName The absolute directory name for files on the device
152      * @param apk The name of the APK, in the form {@code test_package/runner_class}
153      */
runInstrumentation( String action, String relativeDirectoryName, String deviceDirectoryName, String apk)154     private void runInstrumentation(
155             String action, String relativeDirectoryName, String deviceDirectoryName, String apk)
156             throws DeviceNotAvailableException, IOException {
157         String command = String.format(
158                 "am instrument -w -e dumpdir %s -e action %s  %s",
159                 relativeDirectoryName, action, apk);
160         testDevice.executeShellCommand(command);
161         checkForErrorFile(deviceDirectoryName);
162     }
163 
164     /**
165      * Looks for a file called {@code error} in the named device directory, and throws an
166      * {@link ApplicationException} using the first line of that file as the message if found.
167      */
checkForErrorFile(String deviceDirectoryName)168     private void checkForErrorFile(String deviceDirectoryName)
169             throws DeviceNotAvailableException, IOException {
170         String[] deviceDirectoryContents =
171                 testDevice.executeShellCommand("ls " + deviceDirectoryName).split("\\s");
172         for (String deviceFileName : deviceDirectoryContents) {
173             if (deviceFileName.equals("error")) {
174                 throw new ApplicationException(readErrorFile(deviceDirectoryName));
175             }
176         }
177     }
178 
179     /**
180      * Returns the first line read from a file called {@code error} on the device in the named
181      * directory.
182      *
183      * <p>The file is pulled into a temporary location on the host, and deleted after reading.
184      */
readErrorFile(String deviceDirectoryName)185     private String readErrorFile(String deviceDirectoryName)
186             throws IOException, DeviceNotAvailableException {
187         File file = testDevice.pullFile(String.format("%s/error", deviceDirectoryName));
188         if (file == null) {
189             throw new RuntimeException(
190                     "Failed to pull error log from directory " + deviceDirectoryName);
191         }
192         try {
193             return FileUtil.readStringFromFile(file);
194         } finally {
195             file.delete();
196         }
197     }
198 
199     /**
200      * Returns an {@link AhatSnapshot} parsed from an {@code hprof} file on the device at the
201      * given directory and relative filename.
202      *
203      * <p>The file is pulled into a temporary location on the host, and deleted after reading.
204      * It is also logged via {@link TestLogData} under a name formed from the action and the
205      * relative filename (e.g. {@code noop-before.hprof}).
206      */
fetchHeapDump( String deviceDirectoryName, String relativeDumpFilename, String action)207     private AhatSnapshot fetchHeapDump(
208             String deviceDirectoryName, String relativeDumpFilename, String action)
209             throws DeviceNotAvailableException, IOException, HprofFormatException {
210         String deviceFileName = String
211                 .format("%s/%s", deviceDirectoryName, relativeDumpFilename);
212         File file = testDevice.pullFile(deviceFileName);
213         if (file == null) {
214             throw new RuntimeException("Failed to pull dump: " + deviceFileName);
215         }
216         try {
217             logHeapDump(file, String.format("%s-%s", action, relativeDumpFilename));
218             return Parser.parseHeapDump(file, new ProguardMap());
219         } finally {
220             file.delete();
221         }
222     }
223 
224     /**
225      * Returns the total PSS in kB read from a stringified integer in a file on the device at the
226      * given directory and relative filename.
227      */
fetchTotalPssKb( String deviceDirectoryName, String relativeFilename)228     private int fetchTotalPssKb(
229             String deviceDirectoryName, String relativeFilename)
230             throws DeviceNotAvailableException, IOException, HprofFormatException {
231         String shellCommand = String.format("cat %s/%s", deviceDirectoryName, relativeFilename);
232         String totalPssKbStr = testDevice.executeShellCommand(shellCommand);
233         return Integer.parseInt(totalPssKbStr);
234     }
235 
236     /**
237      * Logs the heap dump from the given file via {@link TestLogData} with the given log
238      * filename.
239      */
logHeapDump(File file, String logFilename)240     private void logHeapDump(File file, String logFilename) {
241         try (FileInputStreamSource dataStream = new FileInputStreamSource(file)) {
242             logs.addTestLog(logFilename, LogDataType.HPROF, dataStream);
243         }
244     }
245 
getCurrentTimeIso8601()246     private static String getCurrentTimeIso8601() {
247         SimpleDateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
248         Date now = new Date();
249         return iso8601Format.format(now);
250     }
251 
252     /**
253      * An exception indicating that the activity on the device encountered an error which it
254      * passed
255      * back to the host.
256      */
257     private static class ApplicationException extends RuntimeException {
258 
259         private static final long serialVersionUID = 0;
260 
ApplicationException(String applicationError)261         ApplicationException(String applicationError) {
262             super("Error encountered running application on device: " + applicationError);
263         }
264     }
265 }
266