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