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