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 package com.android.performance.tests; 17 18 import com.android.ddmlib.MultiLineReceiver; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.config.Option.Importance; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 25 import com.android.tradefed.result.ITestInvocationListener; 26 import com.android.tradefed.result.TestDescription; 27 import com.android.tradefed.testtype.IDeviceTest; 28 import com.android.tradefed.testtype.IRemoteTest; 29 import com.android.tradefed.util.AbiFormatter; 30 import com.android.tradefed.util.SimplePerfStatResultParser; 31 import com.android.tradefed.util.SimplePerfUtil; 32 import com.android.tradefed.util.SimplePerfUtil.SimplePerfType; 33 import com.android.tradefed.util.SimpleStats; 34 import com.android.tradefed.util.proto.TfMetricProtoUtil; 35 36 import org.junit.Assert; 37 38 import java.text.NumberFormat; 39 import java.text.ParseException; 40 import java.util.ArrayList; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.concurrent.TimeUnit; 46 import java.util.regex.Matcher; 47 import java.util.regex.Pattern; 48 49 /** 50 * Test which runs various micro_bench tests and reports the results. 51 * <p> 52 * Tests include: 53 * </p><ul> 54 * <li>{@code micro_bench sleep 1 ITERATIONS}</li> 55 * <li>{@code micro_bench memset ARG ITERATIONS} where ARG is defined by {@code memset-small}</li> 56 * <li>{@code micro_bench memset ARG ITERATIONS} where ARG is defined by {@code memset-large}</li> 57 * <li>{@code micro_bench memcpy ARG ITERATIONS} where ARG is defined by {@code memcpy-small}</li> 58 * <li>{@code micro_bench memcpy ARG ITERATIONS} where ARG is defined by {@code memcpy-large}</li> 59 * <li>{@code micro_bench memread ARG ITERATIONS} where ARG is defined by {@code memread}</li> 60 * </ul> 61 */ 62 public class MicroBenchTest implements IDeviceTest, IRemoteTest { 63 private final static String RUN_KEY = "micro_bench"; 64 65 private final static Pattern SLEEP_PATTERN = Pattern.compile( 66 "sleep\\(\\d+\\) took (\\d+.\\d+) seconds"); 67 private final static Pattern MEMSET_PATTERN = Pattern.compile( 68 "memset \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)"); 69 private final static Pattern MEMCPY_PATTERN = Pattern.compile( 70 "memcpy \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)"); 71 private final static Pattern MEMREAD_PATTERN = Pattern.compile( 72 "memread \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)"); 73 74 private final static String BINARY_NAME = "micro_bench|#ABI32#|"; 75 private final static String SLEEP_CMD = "%s sleep 1 %d"; 76 // memset, memcpy, and memread pass in another argument befor passing the cmd format string to 77 // TestCase, so escape the command and iteration formats. 78 private final static String MEMSET_CMD = "%%s memset %d %%d"; 79 private final static String MEMCPY_CMD = "%%s memcpy %d %%d"; 80 private final static String MEMREAD_CMD = "%%s memread %d %%d"; 81 82 @Option(name = AbiFormatter.FORCE_ABI_STRING, 83 description = AbiFormatter.FORCE_ABI_DESCRIPTION, 84 importance = Importance.IF_UNSET) 85 private String mForceAbi = null; 86 87 @Option(name = "sleep-iterations") 88 private Integer mSleepIterations = 10; 89 90 @Option(name = "memset-iterations") 91 private Integer mMemsetIterations = 100; 92 93 @Option(name = "memset-small") 94 private Integer mMemsetSmall = 8 * 1024; // 8 KB 95 96 @Option(name = "memset-large") 97 private Integer mMemsetLarge = 2 * 1024 * 1024; // 2 MB 98 99 @Option(name = "memcpy-iterations") 100 private Integer mMemcpyIterations = 100; 101 102 @Option(name = "memcpy-small") 103 private Integer mMemcpySmall = 8 * 1024; // 8 KB 104 105 @Option(name = "memcpy-large") 106 private Integer mMemcpyLarge = 2 * 1024 * 1024; // 2 MB 107 108 @Option(name = "memread-iterations") 109 private Integer mMemreadIterations = 100; 110 111 @Option(name = "memread") 112 private Integer mMemreadArg = 8 * 1024; // 8 KB 113 114 @Option(name = "stop-framework", description = "Stop the framework before running tests") 115 private boolean mStopFramework = true; 116 117 @Option(name = "profile-mode", description = 118 "In profile mode, microbench runs memset, memcpy, and " 119 + "memread from 32 bytes to 8 MB, doubling the size each time.") 120 private boolean mProfileMode = false; 121 122 @Option(name = "simpleperf-mode", 123 description = "Whether use simpleperf to get low level metrics") 124 private static boolean mSimpleperfMode = false; 125 126 @Option(name = "simpleperf-argu", description = "simpleperf arguments") 127 private List<String> mSimpleperfArgu = new ArrayList<String>(); 128 129 @Option( 130 name = "simpleperf-cmd-timeout", 131 description = "Timeout (in seconds) while running simpleperf shell command." 132 ) 133 private int mSimpleperfCmdTimeout = 240; 134 135 String mFormattedBinaryName = null; 136 ITestDevice mTestDevice = null; 137 SimplePerfUtil mSpUtil = null; 138 139 /** 140 * Class used to read and parse micro_bench output. 141 */ 142 private static class MicroBenchReceiver extends MultiLineReceiver { 143 private Pattern mPattern; 144 private int mTimeGroup = -1; 145 private int mPerfGroup = -1; 146 private SimpleStats mTimeStats = null; 147 private SimpleStats mPerfStats = null; 148 private Map<String, Double> mSimpleperfMetricsMap = new HashMap<String, Double>(); 149 150 private boolean mSimpleperfOutput; 151 152 /** 153 * Create a receiver for parsing micro_bench output. 154 * 155 * @param pattern The regex {@link Pattern} to parse each line of output. Time and/or 156 * performance numbers should be their own group. 157 * @param timeGroup the index of the time data in the pattern. 158 * @param perfGroup the index of the performance data in the pattern. 159 */ 160 public MicroBenchReceiver(Pattern pattern, int timeGroup, int perfGroup) { 161 mPattern = pattern; 162 mTimeGroup = timeGroup; 163 if (mTimeGroup > 0) { 164 mTimeStats = new SimpleStats(); 165 } 166 mPerfGroup = perfGroup; 167 if (mPerfGroup > 0) { 168 mPerfStats = new SimpleStats(); 169 } 170 mSimpleperfOutput = false; 171 } 172 173 /** 174 * {@inheritDoc} 175 */ 176 @Override 177 public void processNewLines(String[] lines) { 178 for (String line : lines) { 179 if (!mSimpleperfOutput || !mSimpleperfMode) { 180 if (line.contains("Performance counter statistics:")) { 181 mSimpleperfOutput = true; 182 continue; 183 } 184 CLog.v(line); 185 Matcher m = mPattern.matcher(line); 186 if (m.matches()) { 187 if (mTimeStats != null) { 188 mTimeStats.add( 189 Double.valueOf(m.group(mTimeGroup)) * 1000d); // secs to ms 190 } 191 if (mPerfStats != null) { 192 mPerfStats.add(Double.valueOf(m.group(mPerfGroup))); 193 } 194 } 195 } else { 196 List<String> spResult = SimplePerfStatResultParser.parseSingleLine(line); 197 if (spResult.size() == 1) { 198 CLog.d(String.format("Total run time: %s", spResult.get(0))); 199 } else if (spResult.size() == 3) { 200 try { 201 Double metricValue = NumberFormat.getNumberInstance(Locale.US) 202 .parse(spResult.get(0)).doubleValue(); 203 mSimpleperfMetricsMap.put(spResult.get(1), metricValue); 204 } catch (ParseException e) { 205 CLog.e("Simpleperf metrics parse failure: " + e.toString()); 206 } 207 } else { 208 CLog.d("Skipping line: " + line); 209 } 210 } 211 } 212 } 213 214 /** 215 * Get the time stats. 216 * 217 * @return a {@link SimpleStats} object containing the time stats in ms. 218 */ 219 public SimpleStats getTimeStats() { 220 return mTimeStats; 221 } 222 223 /** 224 * Get the performance stats. 225 * 226 * @return a {@link SimpleStats} object containing the performance stats in MB/s. 227 */ 228 public SimpleStats getPerfStats() { 229 return mPerfStats; 230 } 231 232 public Map<String, Double> getSimpleperfMetricsMap() { 233 return mSimpleperfMetricsMap; 234 } 235 236 /** 237 * {@inheritDoc} 238 */ 239 @Override 240 public boolean isCancelled() { 241 return false; 242 } 243 } 244 245 /** 246 * Class used to hold test data and run individual tests. 247 */ 248 private class TestCase { 249 private String mTestName; 250 private String mTestKey; 251 private String mCommand; 252 private int mIterations = 0; 253 private MicroBenchReceiver mReceiver; 254 255 /** 256 * Create a test case. 257 * 258 * @param testName The name of the test 259 * @param testKey The key of the test used to report metrics. 260 * @param command The command to run. Must contain a {@code %d} in the place of the number 261 * of iterations. 262 * @param iterations The amount of iterations to run. 263 * @param pattern The regex {@link Pattern} to parse each line of output. Time and/or 264 * performance numbers should be their own group. 265 * @param timeGroup the index of the time data in the pattern. 266 * @param perfGroup the index of the performance data in the pattern. 267 */ 268 public TestCase(String testName, String testKey, String command, int iterations, 269 Pattern pattern, int timeGroup, int perfGroup) { 270 mTestName = testName; 271 mTestKey = testKey; 272 mCommand = command; 273 mIterations = iterations; 274 mReceiver = new MicroBenchReceiver(pattern, timeGroup, perfGroup); 275 } 276 277 /** 278 * Runs the test and adds the results to the test metrics. 279 * 280 * @param listener the {@link ITestInvocationListener}. 281 * @param metrics the metrics map for the results. 282 * @throws DeviceNotAvailableException if the device is not available while running the 283 * test. 284 */ 285 public void runTest(ITestInvocationListener listener, Map<String, String> metrics) 286 throws DeviceNotAvailableException { 287 if (mIterations == 0) { 288 return; 289 } 290 291 CLog.i("Running %s test", mTestName); 292 293 TestDescription testId = new TestDescription(getClass().getCanonicalName(), mTestKey); 294 listener.testStarted(testId); 295 final String cmd = String.format(mCommand, mFormattedBinaryName, mIterations); 296 297 if (mSimpleperfMode) { 298 mSpUtil.executeCommand(cmd, mReceiver, mSimpleperfCmdTimeout, TimeUnit.SECONDS, 1); 299 } else { 300 mTestDevice.executeShellCommand(cmd, mReceiver); 301 } 302 303 Boolean timePass = parseStats(mReceiver.getTimeStats(), "time", metrics); 304 Boolean perfPass = parseStats(mReceiver.getPerfStats(), "perf", metrics); 305 306 for (Map.Entry<String, Double> entry : 307 mReceiver.getSimpleperfMetricsMap().entrySet()) { 308 metrics.put(String.format("%s_%s", mTestKey, entry.getKey()), 309 Double.toString(entry.getValue() / mIterations)); 310 } 311 312 if (!timePass || !perfPass) { 313 listener.testFailed(testId, 314 "Iteration count mismatch (see host log)."); 315 } 316 listener.testEnded(testId, new HashMap<String, Metric>()); 317 } 318 319 /** 320 * Parse the stats from {@link MicroBenchReceiver} and add them to the metrics 321 * 322 * @param stats {@link SimpleStats} from {@link MicroBenchReceiver} or {@code null} if 323 * there are no stats. 324 * @param category the category of the stats, either {@code perf} or {@code time}. 325 * @param metrics the metrics map to add the parsed stats to. 326 * @return {@code true} if the stats were successfully parsed and added or if 327 * {@code stats == null}, {@code false} if there was an error. 328 */ 329 private boolean parseStats(SimpleStats stats, String category, 330 Map<String, String> metrics) { 331 if (stats == null) { 332 return true; 333 } 334 335 String unit = "perf".equals(category) ? "MB/s" : "ms"; 336 337 CLog.i("%s %s results: size = %d, mean = %f %s, stdev = %f %s", mTestName, category, 338 stats.size(), stats.mean(), unit, stats.stdev(), unit); 339 340 if (stats.mean() == null) { 341 CLog.e("Expected non null mean for %s %s", mTestName, category); 342 return false; 343 } 344 345 if (stats.size() != mIterations) { 346 CLog.e("Expected iterations (%d) not equal to sample size (%d) for %s %s", 347 mIterations, stats.size(), mTestName, category); 348 return false; 349 } 350 351 if (metrics != null) { 352 metrics.put(String.format("%s_%s", mTestKey, category), 353 Double.toString(stats.mean())); 354 } 355 return true; 356 } 357 } 358 359 /** 360 * {@inheritDoc} 361 */ 362 @Override 363 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 364 String name, cmd; 365 List<TestCase> testCases = new ArrayList<TestCase>(); 366 367 mFormattedBinaryName = AbiFormatter.formatCmdForAbi(BINARY_NAME, mForceAbi); 368 369 Assert.assertNotNull(mTestDevice); 370 // TODO: Replace this with a more generic way to check for the presence of a binary 371 final String output = mTestDevice.executeShellCommand(mFormattedBinaryName); 372 Assert.assertFalse(output.contains(mFormattedBinaryName + ": not found")); 373 374 if (mSimpleperfMode) { 375 mSpUtil = SimplePerfUtil.newInstance(mTestDevice, SimplePerfType.STAT); 376 if (mSimpleperfArgu.size() == 0) { 377 mSimpleperfArgu.add("-e cpu-cycles:k,cpu-cycles:u"); 378 } 379 mSpUtil.setArgumentList(mSimpleperfArgu); 380 } 381 382 if (mStopFramework) { 383 mTestDevice.executeShellCommand("stop"); 384 } 385 386 if (mProfileMode) { 387 for (int i = 1 << 5; i < 1 << 24; i <<= 1) { 388 name = String.format("memset %d", i); 389 cmd = String.format(MEMSET_CMD, i); 390 testCases.add(new TestCase(name, String.format("memset_%d", i), cmd, 391 mMemsetIterations, MEMSET_PATTERN, 1, 2)); 392 } 393 394 for (int i = 1 << 5; i < 1 << 24; i <<= 1) { 395 name = String.format("memcpy %d", i); 396 cmd = String.format(MEMCPY_CMD, i); 397 testCases.add(new TestCase(name, String.format("memcpy_%d", i), cmd, 398 mMemcpyIterations, MEMCPY_PATTERN, 1, 2)); 399 } 400 401 for (int i = 1 << 5; i < 1 << 24; i <<= 1) { 402 name = String.format("memread %d", i); 403 cmd = String.format(MEMREAD_CMD, i); 404 testCases.add(new TestCase(name, String.format("memread_%d", i), cmd, 405 mMemreadIterations, MEMREAD_PATTERN, 1, 2)); 406 } 407 408 // Don't want to report test metrics in profile mode 409 for (TestCase test : testCases) { 410 test.runTest(listener, null); 411 } 412 } else { 413 // Sleep test case 414 testCases.add(new TestCase("sleep", "sleep", SLEEP_CMD, mSleepIterations, SLEEP_PATTERN, 415 1, -1)); 416 417 // memset small test case 418 name = String.format("memset %d", mMemsetSmall); 419 cmd = String.format(MEMSET_CMD, mMemsetSmall); 420 testCases.add(new TestCase(name, "memset_small", cmd, mMemsetIterations, MEMSET_PATTERN, 421 1, 2)); 422 423 // memset large test case 424 name = String.format("memset %d", mMemsetLarge); 425 cmd = String.format(MEMSET_CMD, mMemsetLarge); 426 testCases.add(new TestCase(name, "memset_large", cmd, mMemsetIterations, MEMSET_PATTERN, 427 1, 2)); 428 429 // memcpy small test case 430 name = String.format("memcpy %d", mMemcpySmall); 431 cmd = String.format(MEMCPY_CMD, mMemcpySmall); 432 testCases.add(new TestCase(name, "memcpy_small", cmd, mMemcpyIterations, MEMCPY_PATTERN, 433 1, 2)); 434 435 // memcpy large test case 436 name = String.format("memcpy %d", mMemcpyLarge); 437 cmd = String.format(MEMCPY_CMD, mMemcpyLarge); 438 testCases.add(new TestCase(name, "memcpy_large", cmd, mMemcpyIterations, MEMCPY_PATTERN, 439 1, 2)); 440 441 // memread test case 442 name = String.format("memread %d", mMemreadArg); 443 cmd = String.format(MEMREAD_CMD, mMemreadArg); 444 testCases.add(new TestCase(name, "memread", cmd, mMemreadIterations, MEMREAD_PATTERN, 445 1, 2)); 446 447 listener.testRunStarted(RUN_KEY, testCases.size()); 448 long beginTime = System.currentTimeMillis(); 449 Map<String, String> metrics = new HashMap<String, String>(); 450 for (TestCase test : testCases) { 451 test.runTest(listener, metrics); 452 } 453 listener.testRunEnded( 454 (System.currentTimeMillis() - beginTime), 455 TfMetricProtoUtil.upgradeConvert(metrics)); 456 } 457 458 if (mStopFramework) { 459 mTestDevice.executeShellCommand("start"); 460 } 461 } 462 463 /** 464 * {@inheritDoc} 465 */ 466 @Override 467 public void setDevice(ITestDevice device) { 468 mTestDevice = device; 469 } 470 471 /** 472 * {@inheritDoc} 473 */ 474 @Override 475 public ITestDevice getDevice() { 476 return mTestDevice; 477 } 478 } 479