1 /* 2 * Copyright (C) 2016 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.device.DeviceNotAvailableException; 21 import com.android.tradefed.device.ITestDevice; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.result.ByteArrayInputStreamSource; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.result.LogDataType; 26 import com.android.tradefed.testtype.IDeviceTest; 27 import com.android.tradefed.testtype.IRemoteTest; 28 import com.android.tradefed.util.ProcessInfo; 29 import com.android.tradefed.util.RunUtil; 30 import com.android.tradefed.util.StreamUtil; 31 import com.android.tradefed.util.proto.TfMetricProtoUtil; 32 33 import org.junit.Assert; 34 35 import java.util.HashMap; 36 import java.util.Map; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 /** 41 * Test to gather post launch memory details after launching app 42 * that include app memory usage and system memory usage 43 */ 44 public class HermeticMemoryTest implements IDeviceTest, IRemoteTest { 45 46 private static final String AM_START = "am start -n %s"; 47 private static final String PROC_MEMINFO = "cat /proc/meminfo"; 48 private static final String MEM_AVAILABLE = "cat /proc/meminfo| grep MemAvailable:"; 49 private static final String CACHED_PROCESSES = "dumpsys meminfo|awk '/Total PSS by category:" 50 + "/{found=0} {if(found) print} /: Cached/{found=1}'|tr -d ' '"; 51 private static final Pattern PID_PATTERN = Pattern.compile("^.*pid(?<processid>[0-9]*).*$"); 52 private static final String DUMPSYS_PROCESS = "dumpsys meminfo %s |grep 'TOTAL'"; 53 private static final String DUMPSYS_MEMINFO = "dumpsys meminfo -a "; 54 private static final String MAPS_INFO = "cat /proc/%d/maps"; 55 private static final String SMAPS_INFO = "cat /proc/%d/smaps"; 56 private static final String STATUS_INFO = "cat /proc/%d/status"; 57 private static final String NATIVE_HEAP = "Native"; 58 private static final String DALVIK_HEAP = "Dalvik"; 59 private static final String HEAP = "Heap"; 60 private static final String MEMTOTAL = "MemTotal"; 61 private static final String MEMFREE = "MemFree"; 62 private static final String CACHED = "Cached"; 63 private static final int NO_PROCESS_ID = -1; 64 private static final String DROP_CACHE = "echo 3 > /proc/sys/vm/drop_caches"; 65 private static final String SEPARATOR ="\\s+"; 66 private static final String LINE_SEPARATOR = "\\n"; 67 68 69 @Option(name = "post-app-launch-delay", 70 description = "The delay, between the app launch and the meminfo dump", 71 isTimeVal = true) 72 private long mPostAppLaunchDelay = 60; 73 74 @Option(name = "component-name", 75 description = "package/activity name to launch the activity") 76 private String mComponentName = new String(); 77 78 @Option(name = "total-memory-kb", 79 description = "Built in total memory of the device") 80 private long mTotalMemory = 0; 81 82 @Option(name = "reporting-key", description = "Reporting key is the unique identifier" 83 + "used to report data in the dashboard.") 84 private String mRuKey = ""; 85 86 private ITestDevice mTestDevice = null; 87 private ITestInvocationListener mlistener = null; 88 private Map<String, String> mMetrics = new HashMap<String, String> (); 89 90 @Override run(ITestInvocationListener listener)91 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 92 mlistener = listener; 93 94 calculateFreeMem(); 95 96 String preMemInfo = mTestDevice.executeShellCommand(PROC_MEMINFO); 97 98 if (!preMemInfo.isEmpty()) { 99 100 uploadLogFile(preMemInfo, "BeforeLaunchProcMemInfo"); 101 } else { 102 CLog.e("Not able to collect the /proc/meminfo before launching app"); 103 } 104 105 Assert.assertTrue("Device built in memory in kb is mandatory.Use --total-memory-kb value" 106 + "command line parameter", 107 mTotalMemory != 0); 108 RunUtil.getDefault().sleep(5000); 109 mTestDevice.executeShellCommand(DROP_CACHE); 110 RunUtil.getDefault().sleep(5000); 111 Assert.assertTrue("Not a valid component name to start the activity", 112 (mComponentName.split("/").length == 2)); 113 mTestDevice.executeShellCommand(String.format(AM_START, mComponentName)); 114 115 RunUtil.getDefault().sleep(mPostAppLaunchDelay); 116 String postMemInfo = mTestDevice.executeShellCommand(PROC_MEMINFO); 117 int processId = getProcessId(); 118 String dumpsysMemInfo = mTestDevice.executeShellCommand( 119 String.format("%s %d", DUMPSYS_MEMINFO, processId)); 120 String mapsInfo = mTestDevice.executeShellCommand( 121 String.format(MAPS_INFO, processId)); 122 String sMapsInfo = mTestDevice.executeShellCommand( 123 String.format(SMAPS_INFO, processId)); 124 String statusInfo = mTestDevice.executeShellCommand( 125 String.format(STATUS_INFO, processId)); 126 127 if (!postMemInfo.isEmpty()) { 128 uploadLogFile(postMemInfo, "AfterLaunchProcMemInfo"); 129 parseProcInfo(postMemInfo); 130 } else { 131 CLog.e("Not able to collect the proc/meminfo after launching app"); 132 } 133 134 if (NO_PROCESS_ID == processId) { 135 CLog.e("Process Id not found for the activity launched"); 136 } else { 137 if (!dumpsysMemInfo.isEmpty()) { 138 uploadLogFile(dumpsysMemInfo, "DumpsysMemInfo"); 139 parseDumpsysInfo(dumpsysMemInfo); 140 } else { 141 CLog.e("Not able to collect the Dumpsys meminfo after launching app"); 142 } 143 if (!mapsInfo.isEmpty()) { 144 uploadLogFile(mapsInfo, "mapsInfo"); 145 } else { 146 CLog.e("Not able to collect maps info after launching app"); 147 } 148 if (!sMapsInfo.isEmpty()) { 149 uploadLogFile(sMapsInfo, "smapsInfo"); 150 } else { 151 CLog.e("Not able to collect smaps info after launching app"); 152 } 153 if (!statusInfo.isEmpty()) { 154 uploadLogFile(statusInfo, "statusInfo"); 155 } else { 156 CLog.e("Not able to collect status info after launching app"); 157 } 158 } 159 160 reportMetrics(listener, mRuKey, mMetrics); 161 162 } 163 164 /** 165 * Method to get the process id of the target package/activity name 166 * 167 * @return processId of the activity launched 168 * @throws DeviceNotAvailableException 169 */ getProcessId()170 private int getProcessId() throws DeviceNotAvailableException { 171 String pkgActivitySplit[] = mComponentName.split("/"); 172 if (pkgActivitySplit[0] != null) { 173 ProcessInfo processData = mTestDevice.getProcessByName(pkgActivitySplit[0]); 174 if (null != processData) { 175 return processData.getPid(); 176 } 177 } 178 return NO_PROCESS_ID; 179 } 180 181 /** 182 * Method to write the data to test logs. 183 * @param data 184 * @param fileName 185 */ uploadLogFile(String data, String fileName)186 private void uploadLogFile(String data, String fileName) { 187 ByteArrayInputStreamSource inputStreamSrc = null; 188 try { 189 inputStreamSrc = new ByteArrayInputStreamSource(data.getBytes()); 190 mlistener.testLog(fileName, LogDataType.TEXT, inputStreamSrc); 191 } finally { 192 StreamUtil.cancel(inputStreamSrc); 193 } 194 } 195 196 /** 197 * Method to parse dalvik and heap info for launched app 198 */ parseDumpsysInfo(String dumpInfo)199 private void parseDumpsysInfo(String dumpInfo) { 200 String line[] = dumpInfo.split(LINE_SEPARATOR); 201 for (int lineCount = 0; lineCount < line.length; lineCount++) { 202 String dataSplit[] = line[lineCount].trim().split(SEPARATOR); 203 if ((dataSplit[0].equalsIgnoreCase(NATIVE_HEAP) && dataSplit[1] 204 .equalsIgnoreCase(HEAP)) || 205 (dataSplit[0].equalsIgnoreCase(DALVIK_HEAP) && dataSplit[1] 206 .equalsIgnoreCase(HEAP)) || 207 dataSplit[0].equalsIgnoreCase("Total")) { 208 if (dataSplit.length > 10) { 209 if (dataSplit[0].contains(NATIVE_HEAP) 210 || dataSplit[0].contains(DALVIK_HEAP)) { 211 mMetrics.put(dataSplit[0] + ":PSS_TOTAL", dataSplit[2]); 212 mMetrics.put(dataSplit[0] + ":SHARED_DIRTY", dataSplit[4]); 213 mMetrics.put(dataSplit[0] + ":PRIVATE_DIRTY", dataSplit[5]); 214 mMetrics.put(dataSplit[0] + ":HEAP_TOTAL", dataSplit[9]); 215 mMetrics.put(dataSplit[0] + ":HEAP_ALLOC", dataSplit[10]); 216 } else { 217 mMetrics.put(dataSplit[0] + ":PSS", dataSplit[1]); 218 } 219 } 220 } 221 } 222 } 223 224 /** 225 * Method to parse the system memory details 226 */ parseProcInfo(String memInfo)227 private void parseProcInfo(String memInfo) { 228 String lineSplit[] = memInfo.split(LINE_SEPARATOR); 229 long memTotal = 0; 230 long memFree = 0; 231 long cached = 0; 232 for (int lineCount = 0; lineCount < lineSplit.length; lineCount++) { 233 String line = lineSplit[lineCount].replace(":", "").trim(); 234 String dataSplit[] = line.split(SEPARATOR); 235 if (dataSplit[0].equalsIgnoreCase(MEMTOTAL) || 236 dataSplit[0].equalsIgnoreCase(MEMFREE) || 237 dataSplit[0].equalsIgnoreCase(CACHED)) { 238 if (dataSplit[0].equalsIgnoreCase(MEMTOTAL)) { 239 memTotal = Long.parseLong(dataSplit[1]); 240 } 241 if (dataSplit[0].equalsIgnoreCase(MEMFREE)) { 242 memFree = Long.parseLong(dataSplit[1]); 243 } 244 if (dataSplit[0].equalsIgnoreCase(CACHED)) { 245 cached = Long.parseLong(dataSplit[1]); 246 } 247 mMetrics.put("System_" + dataSplit[0], dataSplit[1]); 248 } 249 } 250 mMetrics.put("System_Kernal_Firmware", String.valueOf((mTotalMemory - memTotal))); 251 mMetrics.put("System_Framework_Apps", String.valueOf((memTotal - (memFree + cached)))); 252 } 253 254 /** 255 * Method to parse the free memory based on total memory available from proc/meminfo and 256 * private dirty and private clean information of the cached processess 257 * from dumpsys meminfo. 258 */ calculateFreeMem()259 private void calculateFreeMem() throws DeviceNotAvailableException { 260 String memAvailable[] = mTestDevice.executeShellCommand(MEM_AVAILABLE).split(SEPARATOR); 261 int cacheProcDirty = Integer.parseInt(memAvailable[1]); 262 263 String cachedProcesses = mTestDevice.executeShellCommand(CACHED_PROCESSES); 264 String processes[] = cachedProcesses.split(LINE_SEPARATOR); 265 for (String process : processes) { 266 Matcher match = null; 267 if (((match = matches(PID_PATTERN, process))) != null) { 268 String processId = match.group("processid"); 269 String processInfoStr = mTestDevice.executeShellCommand(String.format( 270 DUMPSYS_PROCESS, processId)); 271 String processInfo[] = null; 272 if (processInfoStr != null && !processInfoStr.isEmpty()) { 273 processInfo = processInfoStr.split(LINE_SEPARATOR); 274 } 275 if (null != processInfo && processInfo.length > 0) { 276 String procDetails[] = processInfo[0].trim().split(SEPARATOR); 277 cacheProcDirty = cacheProcDirty + Integer.parseInt(procDetails[2].trim()) 278 + Integer.parseInt(procDetails[3]); 279 } 280 } 281 } 282 mMetrics.put("MemAvailable_CacheProcDirty", String.valueOf(cacheProcDirty)); 283 } 284 285 /** 286 * Report run metrics by creating an empty test run to stick them in 287 * 288 * @param listener the {@link ITestInvocationListener} of test results 289 * @param runName the test name 290 * @param metrics the {@link Map} that contains metrics for the given test 291 */ reportMetrics(ITestInvocationListener listener, String runName, Map<String, String> metrics)292 void reportMetrics(ITestInvocationListener listener, String runName, 293 Map<String, String> metrics) { 294 // Create an empty testRun to report the parsed runMetrics 295 CLog.d("About to report metrics: %s", metrics); 296 listener.testRunStarted(runName, 0); 297 listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(metrics)); 298 } 299 300 /** 301 * Checks whether {@code line} matches the given {@link Pattern}. 302 * 303 * @return The resulting {@link Matcher} obtained by matching the {@code line} against 304 * {@code pattern}, or null if the {@code line} does not match. 305 */ matches(Pattern pattern, String line)306 private static Matcher matches(Pattern pattern, String line) { 307 Matcher ret = pattern.matcher(line); 308 return ret.matches() ? ret : null; 309 } 310 311 @Override setDevice(ITestDevice device)312 public void setDevice(ITestDevice device) { 313 mTestDevice = device; 314 } 315 316 @Override getDevice()317 public ITestDevice getDevice() { 318 return mTestDevice; 319 } 320 } 321