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