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
17package libcore.heapmetrics;
18
19import com.android.ahat.heapdump.AhatSnapshot;
20import com.android.ahat.heapdump.Diff;
21import com.android.ahat.heapdump.HprofFormatException;
22import com.android.ahat.heapdump.Parser;
23import com.android.ahat.proguard.ProguardMap;
24import com.android.tradefed.device.DeviceNotAvailableException;
25import com.android.tradefed.device.ITestDevice;
26import com.android.tradefed.result.FileInputStreamSource;
27import com.android.tradefed.result.LogDataType;
28import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
29import com.android.tradefed.util.FileUtil;
30
31import java.io.File;
32import java.io.IOException;
33import java.text.SimpleDateFormat;
34import java.util.Date;
35
36/**
37 * Helper class that runs the metric instrumentations on a test device.
38 */
39class 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     */
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
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
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         */
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         */
94        AhatSnapshot getAfterDump() {
95            return afterDump;
96        }
97
98        /**
99         * Returns the PSS measured when the instrumentation starts, in kB.
100         */
101        int getBeforeTotalPssKb() {
102            return beforeTotalPssKb;
103        }
104
105        /**
106         * Returns the PSS measured after the instrumentation action has been executed, in kB.
107         */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
261        ApplicationException(String applicationError) {
262            super("Error encountered running application on device: " + applicationError);
263        }
264    }
265}
266