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.tradefed.config.Option; 19 import com.android.tradefed.config.Option.Importance; 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.metrics.proto.MetricMeasurement.Metric; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.result.TestDescription; 26 import com.android.tradefed.testtype.IDeviceTest; 27 import com.android.tradefed.testtype.IRemoteTest; 28 import com.android.tradefed.util.AbiFormatter; 29 import com.android.tradefed.util.SimplePerfResult; 30 import com.android.tradefed.util.SimplePerfUtil; 31 import com.android.tradefed.util.SimplePerfUtil.SimplePerfType; 32 import com.android.tradefed.util.SimpleStats; 33 import com.android.tradefed.util.proto.TfMetricProtoUtil; 34 35 import org.junit.Assert; 36 37 import java.text.NumberFormat; 38 import java.text.ParseException; 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Locale; 43 import java.util.Map; 44 import java.util.regex.Matcher; 45 import java.util.regex.Pattern; 46 47 /** This test is targeting eMMC performance on read/ write. */ 48 public class EmmcPerformanceTest implements IDeviceTest, IRemoteTest { 49 private enum TestType { 50 DD, 51 RANDOM; 52 } 53 54 private static final String RUN_KEY = "emmc_performance_tests"; 55 56 private static final String SEQUENTIAL_READ_KEY = "sequential_read"; 57 private static final String SEQUENTIAL_WRITE_KEY = "sequential_write"; 58 private static final String RANDOM_READ_KEY = "random_read"; 59 private static final String RANDOM_WRITE_KEY = "random_write"; 60 private static final String PERF_RANDOM = "/data/local/tmp/rand_emmc_perf|#ABI32#|"; 61 62 private static final Pattern DD_PATTERN = 63 Pattern.compile("\\d+ bytes transferred in \\d+\\.\\d+ secs \\((\\d+) bytes/sec\\)"); 64 65 private static final Pattern EMMC_RANDOM_PATTERN = 66 Pattern.compile("(\\d+) (\\d+)byte iops/sec"); 67 private static final int BLOCK_SIZE = 1048576; 68 private static final int SEQ_COUNT = 200; 69 70 @Option(name = "cpufreq", description = "The path to the cpufreq directory on the DUT.") 71 private String mCpufreq = "/sys/devices/system/cpu/cpu0/cpufreq"; 72 73 @Option( 74 name = "auto-discover-cache-info", 75 description = 76 "Indicate if test should attempt auto discover cache path and partition size " 77 + "from the test device. Default to be false, ie. manually set " 78 + "cache-device and cache-partition-size, or use default." 79 + " If fail to discover, it will fallback to what is set in " 80 + "cache-device") 81 private boolean mAutoDiscoverCacheInfo = false; 82 83 @Option( 84 name = "cache-device", 85 description = 86 "The path to the cache block device on the DUT." 87 + " Nakasi: /dev/block/platform/sdhci-tegra.3/by-name/CAC\n" 88 + " Prime: /dev/block/platform/omap/omap_hsmmc.0/by-name/cache\n" 89 + " Stingray: /dev/block/platform/sdhci-tegra.3/by-name/cache\n" 90 + " Crespo: /dev/block/platform/s3c-sdhci.0/by-name/userdata\n", 91 importance = Importance.IF_UNSET) 92 private String mCache = null; 93 94 @Option(name = "iterations", description = "The number of iterations to run") 95 private int mIterations = 100; 96 97 @Option( 98 name = AbiFormatter.FORCE_ABI_STRING, 99 description = AbiFormatter.FORCE_ABI_DESCRIPTION, 100 importance = Importance.IF_UNSET) 101 private String mForceAbi = null; 102 103 @Option(name = "cache-partition-size", description = "Cache partiton size in MB") 104 private static int mCachePartitionSize = 100; 105 106 @Option( 107 name = "simpleperf-mode", 108 description = "Whether use simpleperf to get low level metrics") 109 private boolean mSimpleperfMode = false; 110 111 @Option(name = "simpleperf-argu", description = "simpleperf arguments") 112 private List<String> mSimpleperfArgu = new ArrayList<>(); 113 114 ITestDevice mTestDevice = null; 115 SimplePerfUtil mSpUtil = null; 116 117 /** {@inheritDoc} */ 118 @Override run(ITestInvocationListener listener)119 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 120 try { 121 setUp(); 122 123 listener.testRunStarted(RUN_KEY, 5); 124 long beginTime = System.currentTimeMillis(); 125 Map<String, String> metrics = new HashMap<>(); 126 127 runSequentialRead(mIterations, listener, metrics); 128 runSequentialWrite(mIterations, listener, metrics); 129 // FIXME: Figure out cache issues with random read and reenable test. 130 // runRandomRead(mIterations, listener, metrics); 131 // runRandomWrite(mIterations, listener, metrics); 132 133 CLog.d("Metrics: %s", metrics.toString()); 134 listener.testRunEnded( 135 (System.currentTimeMillis() - beginTime), 136 TfMetricProtoUtil.upgradeConvert(metrics)); 137 } finally { 138 cleanUp(); 139 } 140 } 141 142 /** Run the sequential read test. */ runSequentialRead( int iterations, ITestInvocationListener listener, Map<String, String> metrics)143 private void runSequentialRead( 144 int iterations, ITestInvocationListener listener, Map<String, String> metrics) 145 throws DeviceNotAvailableException { 146 String command = 147 String.format( 148 "dd if=%s of=/dev/null bs=%d count=%d", mCache, BLOCK_SIZE, SEQ_COUNT); 149 runTest(SEQUENTIAL_READ_KEY, command, TestType.DD, true, iterations, listener, metrics); 150 } 151 152 /** Run the sequential write test. */ runSequentialWrite( int iterations, ITestInvocationListener listener, Map<String, String> metrics)153 private void runSequentialWrite( 154 int iterations, ITestInvocationListener listener, Map<String, String> metrics) 155 throws DeviceNotAvailableException { 156 String command = 157 String.format( 158 "dd if=/dev/zero of=%s bs=%d count=%d", mCache, BLOCK_SIZE, SEQ_COUNT); 159 runTest(SEQUENTIAL_WRITE_KEY, command, TestType.DD, false, iterations, listener, metrics); 160 } 161 162 /** Run the random read test. */ 163 @SuppressWarnings("unused") runRandomRead( int iterations, ITestInvocationListener listener, Map<String, String> metrics)164 private void runRandomRead( 165 int iterations, ITestInvocationListener listener, Map<String, String> metrics) 166 throws DeviceNotAvailableException { 167 String command = 168 String.format( 169 "%s -r %d %s", 170 AbiFormatter.formatCmdForAbi(PERF_RANDOM, mForceAbi), 171 mCachePartitionSize, 172 mCache); 173 runTest(RANDOM_READ_KEY, command, TestType.RANDOM, true, iterations, listener, metrics); 174 } 175 176 /** Run the random write test with OSYNC disabled. */ runRandomWrite( int iterations, ITestInvocationListener listener, Map<String, String> metrics)177 private void runRandomWrite( 178 int iterations, ITestInvocationListener listener, Map<String, String> metrics) 179 throws DeviceNotAvailableException { 180 String command = 181 String.format( 182 "%s -w %d %s", 183 AbiFormatter.formatCmdForAbi(PERF_RANDOM, mForceAbi), 184 mCachePartitionSize, 185 mCache); 186 runTest(RANDOM_WRITE_KEY, command, TestType.RANDOM, false, iterations, listener, metrics); 187 } 188 189 /** 190 * Run a test for a number of iterations. 191 * 192 * @param testKey the key used to report metrics. 193 * @param command the command to be run on the device. 194 * @param type the {@link TestType}, which determines how each iteration should be run. 195 * @param dropCache whether to drop the cache before starting each iteration. 196 * @param iterations the number of iterations to run. 197 * @param listener the {@link ITestInvocationListener}. 198 * @param metrics the map to store metrics of. 199 * @throws DeviceNotAvailableException If the device was not available. 200 */ runTest( String testKey, String command, TestType type, boolean dropCache, int iterations, ITestInvocationListener listener, Map<String, String> metrics)201 private void runTest( 202 String testKey, 203 String command, 204 TestType type, 205 boolean dropCache, 206 int iterations, 207 ITestInvocationListener listener, 208 Map<String, String> metrics) 209 throws DeviceNotAvailableException { 210 CLog.i("Starting test %s", testKey); 211 212 TestDescription id = new TestDescription(RUN_KEY, testKey); 213 listener.testStarted(id); 214 215 Map<String, SimpleStats> simpleperfMetricsMap = new HashMap<>(); 216 SimpleStats stats = new SimpleStats(); 217 for (int i = 0; i < iterations; i++) { 218 if (dropCache) { 219 dropCache(); 220 } 221 222 Double kbps = null; 223 switch (type) { 224 case DD: 225 kbps = runDdIteration(command, simpleperfMetricsMap); 226 break; 227 case RANDOM: 228 kbps = runRandomIteration(command, simpleperfMetricsMap); 229 break; 230 } 231 232 if (kbps != null) { 233 CLog.i("Result for %s, iteration %d: %f KBps", testKey, i + 1, kbps); 234 stats.add(kbps); 235 } else { 236 CLog.w("Skipping %s, iteration %d", testKey, i + 1); 237 } 238 } 239 240 if (stats.mean() != null) { 241 metrics.put(testKey, Double.toString(stats.median())); 242 for (Map.Entry<String, SimpleStats> entry : simpleperfMetricsMap.entrySet()) { 243 metrics.put( 244 String.format("%s_%s", testKey, entry.getKey()), 245 Double.toString(entry.getValue().median())); 246 } 247 } else { 248 listener.testFailed(id, "No metrics to report (see log)"); 249 } 250 CLog.i( 251 "Test %s finished: mean=%f, stdev=%f, samples=%d", 252 testKey, stats.mean(), stats.stdev(), stats.size()); 253 listener.testEnded(id, new HashMap<String, Metric>()); 254 } 255 256 /** 257 * Run a single iteration of the dd (sequential) test. 258 * 259 * @param command the command to run on the device. 260 * @param simpleperfMetricsMap the map contain simpleperf metrics aggregated results 261 * @return The speed of the test in KBps or null if there was an error running or parsing the 262 * test. 263 * @throws DeviceNotAvailableException If the device was not available. 264 */ runDdIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap)265 private Double runDdIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap) 266 throws DeviceNotAvailableException { 267 String[] output; 268 SimplePerfResult spResult = null; 269 if (mSimpleperfMode) { 270 spResult = mSpUtil.executeCommand(command); 271 output = spResult.getCommandRawOutput().split("\n"); 272 } else { 273 output = mTestDevice.executeShellCommand(command).split("\n"); 274 } 275 String line = output[output.length - 1].trim(); 276 277 Matcher m = DD_PATTERN.matcher(line); 278 if (m.matches()) { 279 simpleperfResultAggregation(spResult, simpleperfMetricsMap); 280 return convertBpsToKBps(Double.parseDouble(m.group(1))); 281 } else { 282 CLog.w("Line \"%s\" did not match expected output, ignoring", line); 283 return null; 284 } 285 } 286 287 /** 288 * Run a single iteration of the random test. 289 * 290 * @param command the command to run on the device. 291 * @param simpleperfMetricsMap the map contain simpleperf metrics aggregated results 292 * @return The speed of the test in KBps or null if there was an error running or parsing the 293 * test. 294 * @throws DeviceNotAvailableException If the device was not available. 295 */ runRandomIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap)296 private Double runRandomIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap) 297 throws DeviceNotAvailableException { 298 String output; 299 SimplePerfResult spResult = null; 300 if (mSimpleperfMode) { 301 spResult = mSpUtil.executeCommand(command); 302 output = spResult.getCommandRawOutput(); 303 } else { 304 output = mTestDevice.executeShellCommand(command); 305 } 306 Matcher m = EMMC_RANDOM_PATTERN.matcher(output.trim()); 307 if (m.matches()) { 308 simpleperfResultAggregation(spResult, simpleperfMetricsMap); 309 return convertIopsToKBps(Double.parseDouble(m.group(1))); 310 } else { 311 CLog.w("Line \"%s\" did not match expected output, ignoring", output); 312 return null; 313 } 314 } 315 316 /** 317 * Helper function to aggregate simpleperf results 318 * 319 * @param spResult object that holds simpleperf results 320 * @param simpleperfMetricsMap map holds aggregated simpleperf results 321 */ simpleperfResultAggregation( SimplePerfResult spResult, Map<String, SimpleStats> simpleperfMetricsMap)322 private void simpleperfResultAggregation( 323 SimplePerfResult spResult, Map<String, SimpleStats> simpleperfMetricsMap) { 324 if (mSimpleperfMode) { 325 Assert.assertNotNull("simpleperf result is null object", spResult); 326 for (Map.Entry<String, String> entry : spResult.getBenchmarkMetrics().entrySet()) { 327 try { 328 Double metricValue = 329 NumberFormat.getNumberInstance(Locale.US) 330 .parse(entry.getValue()) 331 .doubleValue(); 332 if (!simpleperfMetricsMap.containsKey(entry.getKey())) { 333 SimpleStats newStat = new SimpleStats(); 334 simpleperfMetricsMap.put(entry.getKey(), newStat); 335 } 336 simpleperfMetricsMap.get(entry.getKey()).add(metricValue); 337 } catch (ParseException e) { 338 CLog.e("Simpleperf metrics parse failure: " + e.toString()); 339 } 340 } 341 } 342 } 343 344 /** Drop the disk cache on the device. */ dropCache()345 private void dropCache() throws DeviceNotAvailableException { 346 mTestDevice.executeShellCommand("echo 3 > /proc/sys/vm/drop_caches"); 347 } 348 349 /** Convert bytes / sec reported by the dd tests into KBps. */ convertBpsToKBps(double bps)350 private double convertBpsToKBps(double bps) { 351 return bps / 1024; 352 } 353 354 /** 355 * Convert the iops reported by the random tests into KBps. 356 * 357 * <p>The iops is number of 4kB block reads/writes per sec. This makes the conversion factor 4. 358 */ convertIopsToKBps(double iops)359 private double convertIopsToKBps(double iops) { 360 return 4 * iops; 361 } 362 363 /** Setup the device for tests by unmounting partitions and maxing the cpu speed. */ setUp()364 private void setUp() throws DeviceNotAvailableException { 365 if (mAutoDiscoverCacheInfo) { 366 discoverCacheInfo(); 367 } 368 mTestDevice.executeShellCommand("umount /sdcard"); 369 mTestDevice.executeShellCommand("umount /data"); 370 mTestDevice.executeShellCommand("umount /cache"); 371 372 mTestDevice.executeShellCommand( 373 String.format("cat %s/cpuinfo_max_freq > %s/scaling_max_freq", mCpufreq, mCpufreq)); 374 mTestDevice.executeShellCommand( 375 String.format("cat %s/cpuinfo_max_freq > %s/scaling_min_freq", mCpufreq, mCpufreq)); 376 377 if (mSimpleperfMode) { 378 mSpUtil = SimplePerfUtil.newInstance(mTestDevice, SimplePerfType.STAT); 379 if (mSimpleperfArgu.size() == 0) { 380 mSimpleperfArgu.add("-e cpu-cycles:k,cpu-cycles:u"); 381 } 382 mSpUtil.setArgumentList(mSimpleperfArgu); 383 } 384 } 385 386 /** Attempt to detect cache path and cache partition size automatically */ discoverCacheInfo()387 private void discoverCacheInfo() throws DeviceNotAvailableException { 388 // Expected output look similar to the following: 389 // 390 // > ... vdc dump | grep cache 391 // 0 4123 /dev/block/platform/soc/7824900.sdhci/by-name/cache /cache ext4 rw, \ 392 // seclabel,nosuid,nodev,noatime,discard,data=ordered 0 0 393 if (mTestDevice.enableAdbRoot()) { 394 String output = mTestDevice.executeShellCommand("vdc dump | grep cache"); 395 CLog.d("Output from shell command 'vdc dump | grep cache':\n%s", output); 396 String[] segments = output.split("\\s+"); 397 if (segments.length >= 3) { 398 mCache = segments[2]; 399 } else { 400 CLog.w("Fail to detect cache path. Fall back to use '%s'", mCache); 401 } 402 } else { 403 CLog.d( 404 "Cannot get cache path because device %s is not rooted.", 405 mTestDevice.getSerialNumber()); 406 } 407 408 // Expected output looks similar to the following: 409 // 410 // > ... df cache 411 // Filesystem 1K-blocks Used Available Use% Mounted on 412 // /dev/block/mmcblk0p34 60400 56 60344 1% /cache 413 String output = mTestDevice.executeShellCommand("df cache"); 414 CLog.d(String.format("Output from shell command 'df cache':\n%s", output)); 415 String[] lines = output.split("\r?\n"); 416 if (lines.length >= 2) { 417 String[] segments = lines[1].split("\\s+"); 418 if (segments.length >= 2) { 419 if (lines[0].toLowerCase().contains("1k-blocks")) { 420 mCachePartitionSize = Integer.parseInt(segments[1]) / 1024; 421 } else { 422 throw new IllegalArgumentException("Unknown unit for the cache size."); 423 } 424 } 425 } 426 427 CLog.d("cache-device is set to %s ...", mCache); 428 CLog.d("cache-partition-size is set to %d ...", mCachePartitionSize); 429 } 430 431 /** Clean up the device by formatting a new cache partition. */ cleanUp()432 private void cleanUp() throws DeviceNotAvailableException { 433 mTestDevice.executeShellCommand(String.format("mke2fs %s", mCache)); 434 } 435 436 /** {@inheritDoc} */ 437 @Override setDevice(ITestDevice device)438 public void setDevice(ITestDevice device) { 439 mTestDevice = device; 440 } 441 442 /** {@inheritDoc} */ 443 @Override getDevice()444 public ITestDevice getDevice() { 445 return mTestDevice; 446 } 447 } 448