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