1 /* 2 * Copyright (C) 2011 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.ddmlib.IDevice; 20 import com.android.ddmlib.MultiLineReceiver; 21 import com.android.ddmlib.NullOutputReceiver; 22 import com.android.tradefed.config.Option; 23 import com.android.tradefed.device.DeviceNotAvailableException; 24 import com.android.tradefed.device.ITestDevice; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.result.FileInputStreamSource; 27 import com.android.tradefed.result.ITestInvocationListener; 28 import com.android.tradefed.result.InputStreamSource; 29 import com.android.tradefed.result.LogDataType; 30 import com.android.tradefed.testtype.IDeviceTest; 31 import com.android.tradefed.testtype.IRemoteTest; 32 import com.android.tradefed.util.FileUtil; 33 import com.android.tradefed.util.StreamUtil; 34 35 import junit.framework.TestCase; 36 37 import org.junit.Assert; 38 39 import java.io.File; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.LinkedList; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Map.Entry; 46 import java.util.Set; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Runs the FIO benchmarks. 51 * <p> 52 * This test pushes the FIO executable to the device and runs several benchmarks. Running each 53 * benchmark consists of creating a config file, creating one or more data files, clearing the disk 54 * cache and then running FIO. The test runs a variety of different configurations including a 55 * simple benchmark with a single thread, a storage benchmark with 4 threads, a media server 56 * emulator, and a media scanner emulator. 57 * </p> 58 */ 59 public class FioBenchmarkTest implements IDeviceTest, IRemoteTest { 60 // TODO: Refactor this to only pick out fields we care about. 61 private static final String[] FIO_V0_RESULT_FIELDS = { 62 "jobname", "groupid", "error", 63 // Read stats 64 "read-kb-io", "read-bandwidth", "read-runtime", 65 "read-slat-min", "read-slat-max", "read-slat-mean", "read-slat-stddev", 66 "read-clat-min", "read-clat-max", "read-clat-mean", "read-clat-stddev", 67 "read-bandwidth-min", "read-bandwidth-max", "read-bandwidth-percent", "read-bandwidth-mean", 68 "read-bandwidth-stddev", 69 // Write stats 70 "write-kb-io", "write-bandwidth", "write-runtime", 71 "write-slat-min", "write-slat-max", "write-slat-mean", "write-slat-stddev", 72 "write-clat-min", "write-clat-max", "write-clat-mean", "write-clat-stddev", 73 "write-bandwidth-min", "write-bandwidth-max", "write-bandwidth-percent", 74 "write-bandwidth-mean", "write-bandwidth-stddev", 75 // CPU stats 76 "cpu-user", "cpu-system", "cpu-context-switches", "cpu-major-page-faults", 77 "cpu-minor-page-faults", 78 // IO depth stats 79 "io-depth-1", "io-depth-2", "io-depth-4", "io-depth-8", "io-depth-16", "io-depth-32", 80 "io-depth-64", 81 // IO lat stats 82 "io-lat-2-ms", "io-lat-4-ms", "io-lat-10-ms", "io-lat-20-ms", "io-lat-50-ms", 83 "io-lat-100-ms", "io-lat-250-ms", "io-lat-500-ms", "io-lat-750-ms", "io-lat-1000-ms", 84 "io-lat-2000-ms" 85 }; 86 private static final String[] FIO_V3_RESULT_FIELDS = { 87 "terse-version", "fio-version", "jobname", "groupid", "error", 88 // Read stats 89 "read-kb-io", "read-bandwidth", "read-iops", "read-runtime", 90 "read-slat-min", "read-slat-max", "read-slat-mean", "read-slat-stddev", 91 "read-clat-min", "read-clat-max", "read-clat-mean", "read-clat-stddev", 92 null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 93 null, null, null, null, null, 94 "read-lat-min", "read-lat-max", "read-lat-mean", "read-lat-stddev", 95 "read-bandwidth-min", "read-bandwidth-max", "read-bandwidth-percent", "read-bandwidth-mean", 96 "read-bandwidth-stddev", 97 // Write stats 98 "write-kb-io", "write-bandwidth", "write-iops", "write-runtime", 99 "write-slat-min", "write-slat-max", "write-slat-mean", "write-slat-stddev", 100 "write-clat-min", "write-clat-max", "write-clat-mean", "write-clat-stddev", 101 null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 102 null, null, null, null, null, 103 "write-lat-min", "write-lat-max", "write-lat-mean", "write-lat-stddev", 104 "write-bandwidth-min", "write-bandwidth-max", "write-bandwidth-percent", 105 "write-bandwidth-mean", "write-bandwidth-stddev", 106 // CPU stats 107 "cpu-user", "cpu-system", "cpu-context-switches", "cpu-major-page-faults", 108 "cpu-minor-page-faults", 109 // IO depth stats 110 "io-depth-1", "io-depth-2", "io-depth-4", "io-depth-8", "io-depth-16", "io-depth-32", 111 "io-depth-64", 112 // IO lat stats 113 "io-lat-2-us", "io-lat-4-us", "io-lat-10-us", "io-lat-20-us", "io-lat-50-us", 114 "io-lat-100-us", "io-lat-250-us", "io-lat-500-us", "io-lat-750-us", "io-lat-1000-us", 115 "io-lat-2-ms", "io-lat-4-ms", "io-lat-10-ms", "io-lat-20-ms", "io-lat-50-ms", 116 "io-lat-100-ms", "io-lat-250-ms", "io-lat-500-ms", "io-lat-750-ms", "io-lat-1000-ms", 117 "io-lat-2000-ms", "io-lat-greater" 118 }; 119 120 private List<TestInfo> mTestCases = null; 121 122 /** 123 * Holds info about a job. The job translates into a job in the FIO config file. Contains the 124 * job name and a map from keys to values. 125 */ 126 private static class JobInfo { 127 public String mJobName = null; 128 public Map<String, String> mParameters = new HashMap<>(); 129 130 /** 131 * Gets the job as a string. 132 * 133 * @return a string of the job formatted for the config file. 134 */ 135 public String createJob() { 136 if (mJobName == null) { 137 return ""; 138 } 139 StringBuilder sb = new StringBuilder(); 140 sb.append(String.format("[%s]\n", mJobName)); 141 for (Entry<String, String> parameter : mParameters.entrySet()) { 142 if (parameter.getValue() == null) { 143 sb.append(String.format("%s\n", parameter.getKey())); 144 } else { 145 sb.append(String.format("%s=%s\n", parameter.getKey(), parameter.getValue())); 146 } 147 } 148 return sb.toString(); 149 } 150 } 151 152 /** 153 * Holds info about a file used in the benchmark. Because of limitations in FIO on Android, 154 * the file needs to be created before the tests are run. Contains the file name and size in kB. 155 */ 156 private static class TestFileInfo { 157 public String mFileName = null; 158 public int mSize = -1; 159 } 160 161 /** 162 * Holds info about the perf metric that are cared about for a given job. 163 */ 164 private static class PerfMetricInfo { 165 public enum ResultType { 166 STRING, INT, FLOAT, PERCENT; 167 168 String value(String input) { 169 switch(this) { 170 case STRING: 171 case INT: 172 case FLOAT: 173 return input; 174 case PERCENT: 175 if (input.length() < 2 || !input.endsWith("%")) { 176 return null; 177 } 178 try { 179 return String.format("%f", Double.parseDouble( 180 input.substring(0, input.length() - 1)) / 100); 181 } catch (NumberFormatException e) { 182 return null; 183 } 184 default: return null; 185 } 186 } 187 } 188 189 public String mJobName = null; 190 public String mFieldName = null; 191 public String mPostKey = null; 192 public ResultType mType = ResultType.STRING; 193 } 194 195 /** 196 * Holds the info associated with a test. 197 * <p> 198 * Contains the test name, key, a list of {@link JobInfo}, a set of {@link TestFileInfo}, 199 * and a set of {@link PerfMetricInfo}. 200 * </p> 201 */ 202 private static class TestInfo { 203 public String mTestName = null; 204 public String mKey = null; 205 public List<JobInfo> mJobs = new LinkedList<>(); 206 public Set<TestFileInfo> mTestFiles = new HashSet<>(); 207 public Set<PerfMetricInfo> mPerfMetrics = new HashSet<>(); 208 209 /** 210 * Gets the config file. 211 * 212 * @return a string containing the contents of the config file needed to run the benchmark. 213 */ 214 private String createConfig() { 215 StringBuilder sb = new StringBuilder(); 216 for (JobInfo job : mJobs) { 217 sb.append(String.format("%s\n", job.createJob())); 218 } 219 return sb.toString(); 220 } 221 } 222 223 /** 224 * Parses the output of the FIO and allows the values to be looked up by job name and property. 225 */ 226 private static class FioParser extends MultiLineReceiver { 227 public Map<String, Map<String, String>> mResults = new HashMap<>(); 228 229 /** 230 * Gets the result for a job and property, or null if the job or the property do not exist. 231 * 232 * @param job the name of the job. 233 * @param property the name of the property. See {@code FIO_RESULT_FIELDS}. 234 * @return the fio results for the job and property or null if it does not exist. 235 */ 236 public String getResult(String job, String property) { 237 if (!mResults.containsKey(job)) { 238 return null; 239 } 240 return mResults.get(job).get(property); 241 } 242 243 /** 244 * {@inheritDoc} 245 */ 246 @Override 247 public void processNewLines(String[] lines) { 248 for (String line : lines) { 249 CLog.d(line); 250 String[] fields = line.split(";"); 251 if (fields.length < FIO_V0_RESULT_FIELDS.length) { 252 continue; 253 } 254 if (fields.length < FIO_V3_RESULT_FIELDS.length) { 255 Map<String, String> r = new HashMap<>(); 256 for (int i = 0; i < FIO_V0_RESULT_FIELDS.length; i++) { 257 r.put(FIO_V0_RESULT_FIELDS[i], fields[i]); 258 } 259 mResults.put(fields[0], r); // Job name is index 0 260 } else if ("3".equals(fields[0])) { 261 Map<String, String> r = new HashMap<>(); 262 for (int i = 0; i < FIO_V3_RESULT_FIELDS.length; i++) { 263 r.put(FIO_V3_RESULT_FIELDS[i], fields[i]); 264 } 265 mResults.put(fields[2], r); // Job name is index 2 266 } else { 267 Assert.fail("Unknown fio terse output version"); 268 } 269 } 270 } 271 272 /** 273 * {@inheritDoc} 274 */ 275 @Override 276 public boolean isCancelled() { 277 return false; 278 } 279 } 280 281 ITestDevice mTestDevice = null; 282 283 private String mFioDir = null; 284 private String mFioBin = null; 285 private String mFioConfig = null; 286 287 @Option(name="fio-location", description="The path to the precompiled FIO executable. If " 288 + "unset, try to use fio from the system image.") 289 private String mFioLocation = null; 290 291 @Option(name="tmp-dir", description="The directory used for interal benchmarks.") 292 private String mTmpDir = "/data/tmp/fio"; 293 294 @Option(name="internal-test-dir", description="The directory used for interal benchmarks.") 295 private String mInternalTestDir = "/data/fio/data"; 296 297 @Option(name="media-test-dir", description="The directory used for media benchmarks.") 298 private String mMediaTestDir = "${EXTERNAL_STORAGE}/fio"; 299 300 @Option(name="external-test-dir", description="The directory used for external benchmarks.") 301 private String mExternalTestDir = "${EXTERNAL_STORAGE}/fio"; 302 303 @Option(name="collect-yaffs-logs", description="Collect yaffs logs before and after tests") 304 private Boolean mCollectYaffsLogs = false; 305 306 @Option(name="run-simple-internal-test", description="Run the simple internal benchmark.") 307 private Boolean mRunSimpleInternalTest = true; 308 309 @Option(name="simple-internal-file-size", 310 description="The file size of the simple internal benchmark in MB.") 311 private int mSimpleInternalFileSize = 256; 312 313 @Option(name="run-simple-external-test", description="Run the simple external benchmark.") 314 private Boolean mRunSimpleExternalTest = false; 315 316 @Option(name="simple-external-file-size", 317 description="The file size of the simple external benchmark in MB.") 318 private int mSimpleExternalFileSize = 256; 319 320 @Option(name="run-storage-internal-test", description="Run the storage internal benchmark.") 321 private Boolean mRunStorageInternalTest = true; 322 323 @Option(name="storage-internal-file-size", 324 description="The file size of the storage internal benchmark in MB.") 325 private int mStorageInternalFileSize = 256; 326 327 @Option(name="storage-internal-job-count", 328 description="The number of jobs for the storage internal benchmark.") 329 private int mStorageInternalJobCount = 4; 330 331 @Option(name="run-storage-external-test", description="Run the storage external benchmark.") 332 private Boolean mRunStorageExternalTest = false; 333 334 @Option(name="storage-external-file-size", 335 description="The file size of the storage external benchmark in MB.") 336 private int mStorageExternalFileSize = 256; 337 338 @Option(name="storage-external-job-count", 339 description="The number of jobs for the storage external benchmark.") 340 private int mStorageExternalJobCount = 4; 341 342 @Option(name="run-media-server-test", description="Run the media server benchmark.") 343 private Boolean mRunMediaServerTest = false; 344 345 @Option(name="media-server-duration", 346 description="The duration of the media server benchmark in secs.") 347 private long mMediaServerDuration = 30; 348 349 @Option(name="media-server-media-file-size", 350 description="The media file size of the media server benchmark in MB.") 351 private int mMediaServerMediaFileSize = 256; 352 353 @Option(name="media-server-worker-file-size", 354 description="The worker file size of the media server benchmark in MB.") 355 private int mMediaServerWorkerFileSize = 256; 356 357 @Option(name="media-server-worker-job-count", 358 description="The number of worker jobs for the media server benchmark.") 359 private int mMediaServerWorkerJobCount = 4; 360 361 @Option(name="run-media-scanner-test", description="Run the media scanner benchmark.") 362 private Boolean mRunMediaScannerTest = false; 363 364 @Option(name="media-scanner-media-file-size", 365 description="The media file size of the media scanner benchmark in kB.") 366 private int mMediaScannerMediaFileSize = 8; 367 368 @Option(name="media-scanner-media-file-count", 369 description="The number of media files to scan.") 370 private int mMediaScannerMediaFileCount = 256; 371 372 @Option(name="media-scanner-worker-file-size", 373 description="The worker file size of the media scanner benchmark in MB.") 374 private int mMediaScannerWorkerFileSize = 256; 375 376 @Option(name="media-scanner-worker-job-count", 377 description="The number of worker jobs for the media server benchmark.") 378 private int mMediaScannerWorkerJobCount = 4; 379 380 @Option(name="key-suffix", 381 description="The suffix to add to the reporting key in order to override the default") 382 private String mKeySuffix = null; 383 384 /** 385 * Sets up all the benchmarks. 386 */ 387 private void setupTests() { 388 if (mTestCases != null) { 389 // assume already set up 390 return; 391 } 392 393 mTestCases = new LinkedList<>(); 394 395 if (mRunSimpleInternalTest) { 396 addSimpleTest("read", "sync", true); 397 addSimpleTest("write", "sync", true); 398 addSimpleTest("randread", "sync", true); 399 addSimpleTest("randwrite", "sync", true); 400 addSimpleTest("randread", "mmap", true); 401 addSimpleTest("randwrite", "mmap", true); 402 } 403 404 if (mRunSimpleExternalTest) { 405 addSimpleTest("read", "sync", false); 406 addSimpleTest("write", "sync", false); 407 addSimpleTest("randread", "sync", false); 408 addSimpleTest("randwrite", "sync", false); 409 addSimpleTest("randread", "mmap", false); 410 addSimpleTest("randwrite", "mmap", false); 411 } 412 413 if (mRunStorageInternalTest) { 414 addStorageTest("read", "sync", true); 415 addStorageTest("write", "sync", true); 416 addStorageTest("randread", "sync", true); 417 addStorageTest("randwrite", "sync", true); 418 addStorageTest("randread", "mmap", true); 419 addStorageTest("randwrite", "mmap", true); 420 } 421 422 if (mRunStorageExternalTest) { 423 addStorageTest("read", "sync", false); 424 addStorageTest("write", "sync", false); 425 addStorageTest("randread", "sync", false); 426 addStorageTest("randwrite", "sync", false); 427 addStorageTest("randread", "mmap", false); 428 addStorageTest("randwrite", "mmap", false); 429 } 430 431 if (mRunMediaServerTest) { 432 addMediaServerTest("read"); 433 addMediaServerTest("write"); 434 } 435 436 if (mRunMediaScannerTest) { 437 addMediaScannerTest(); 438 } 439 } 440 441 /** 442 * Sets up the simple FIO benchmark. 443 * <p> 444 * The test consists of a single process reading or writing to a file. 445 * </p> 446 * 447 * @param rw the type of IO pattern. One of {@code read}, {@code write}, {@code randread}, or 448 * {@code randwrite}. 449 * @param ioengine defines how the job issues I/O. Such as {@code sync}, {@code vsync}, 450 * {@code mmap}, or {@code cpuio} and others. 451 * @param internal whether the test should be run on the internal (/data) partition or the 452 * external partition. 453 */ 454 private void addSimpleTest(String rw, String ioengine, boolean internal) { 455 String fileName = "testfile"; 456 String jobName = "job"; 457 458 String directory; 459 int fileSize; 460 461 TestInfo t = new TestInfo(); 462 if (internal) { 463 t.mTestName = String.format("SimpleBenchmark-int-%s-%s", ioengine, rw); 464 t.mKey = "fio_simple_int_benchmark"; 465 directory = mInternalTestDir; 466 fileSize = mSimpleInternalFileSize; 467 } else { 468 t.mTestName = String.format("SimpleBenchmark-ext-%s-%s", ioengine, rw); 469 t.mKey = "fio_simple_ext_benchmark"; 470 directory = mExternalTestDir; 471 fileSize = mSimpleExternalFileSize; 472 } 473 474 TestFileInfo f = new TestFileInfo(); 475 f.mFileName = new File(directory, fileName).getAbsolutePath(); 476 f.mSize = fileSize * 1024; // fileSize is in MB but we want it in kB. 477 t.mTestFiles.add(f); 478 479 JobInfo j = new JobInfo(); 480 j.mJobName = jobName; 481 j.mParameters.put("directory", directory); 482 j.mParameters.put("filename", fileName); 483 j.mParameters.put("fsync", "1024"); 484 j.mParameters.put("ioengine", ioengine); 485 j.mParameters.put("rw", rw); 486 j.mParameters.put("size", String.format("%dM",fileSize)); 487 t.mJobs.add(j); 488 489 PerfMetricInfo m = new PerfMetricInfo(); 490 m.mJobName = jobName; 491 if ("sync".equals(ioengine)) { 492 m.mPostKey = String.format("%s_bandwidth", rw); 493 } else { 494 m.mPostKey = String.format("%s_%s_bandwidth", rw, ioengine); 495 } 496 m.mType = PerfMetricInfo.ResultType.FLOAT; 497 if (rw.endsWith("read")) { 498 m.mFieldName = "read-bandwidth-mean"; 499 } else if (rw.endsWith("write")) { 500 m.mFieldName = "write-bandwidth-mean"; 501 } 502 t.mPerfMetrics.add(m); 503 504 mTestCases.add(t); 505 } 506 507 /** 508 * Sets up the storage FIO benchmark. 509 * <p> 510 * The test consists of several processes reading or writing to a file. 511 * </p> 512 * 513 * @param rw the type of IO pattern. One of {@code read}, {@code write}, {@code randread}, or 514 * {@code randwrite}. 515 * @param ioengine defines how the job issues I/O. Such as {@code sync}, {@code vsync}, 516 * {@code mmap}, or {@code cpuio} and others. 517 * @param internal whether the test should be run on the internal (/data) partition or the 518 * external partition. 519 */ 520 private void addStorageTest(String rw, String ioengine, boolean internal) { 521 String fileName = "testfile"; 522 String jobName = "workers"; 523 524 String directory; 525 int fileSize; 526 int jobCount; 527 528 TestInfo t = new TestInfo(); 529 if (internal) { 530 t.mTestName = String.format("StorageBenchmark-int-%s-%s", ioengine, rw); 531 t.mKey = "fio_storage_int_benchmark"; 532 directory = mInternalTestDir; 533 fileSize = mStorageInternalFileSize; 534 jobCount = mStorageInternalJobCount; 535 } else { 536 t.mTestName = String.format("StorageBenchmark-ext-%s-%s", ioengine, rw); 537 t.mKey = "fio_storage_ext_benchmark"; 538 directory = mExternalTestDir; 539 fileSize = mStorageExternalFileSize; 540 jobCount = mStorageExternalJobCount; 541 } 542 543 TestFileInfo f = new TestFileInfo(); 544 f.mFileName = new File(directory, fileName).getAbsolutePath(); 545 f.mSize = fileSize * 1024; // fileSize is in MB but we want it in kB. 546 t.mTestFiles.add(f); 547 548 JobInfo j = new JobInfo(); 549 j.mJobName = jobName; 550 j.mParameters.put("directory", directory); 551 j.mParameters.put("filename", fileName); 552 j.mParameters.put("fsync", "1024"); 553 j.mParameters.put("group_reporting", null); 554 j.mParameters.put("ioengine", ioengine); 555 j.mParameters.put("new_group", null); 556 j.mParameters.put("numjobs", String.format("%d", jobCount)); 557 j.mParameters.put("rw", rw); 558 j.mParameters.put("size", String.format("%dM",fileSize)); 559 t.mJobs.add(j); 560 561 PerfMetricInfo m = new PerfMetricInfo(); 562 m.mJobName = jobName; 563 if ("sync".equals(ioengine)) { 564 m.mPostKey = String.format("%s_bandwidth", rw); 565 } else { 566 m.mPostKey = String.format("%s_%s_bandwidth", rw, ioengine); 567 } 568 m.mType = PerfMetricInfo.ResultType.FLOAT; 569 if (rw.endsWith("read")) { 570 m.mFieldName = "read-bandwidth-mean"; 571 } else if (rw.endsWith("write")) { 572 m.mFieldName = "write-bandwidth-mean"; 573 } 574 t.mPerfMetrics.add(m); 575 576 m = new PerfMetricInfo(); 577 m.mJobName = jobName; 578 if ("sync".equals(ioengine)) { 579 m.mPostKey = String.format("%s_latency", rw); 580 } else { 581 m.mPostKey = String.format("%s_%s_latency", rw, ioengine); 582 } 583 m.mType = PerfMetricInfo.ResultType.FLOAT; 584 if (rw.endsWith("read")) { 585 m.mFieldName = "read-clat-mean"; 586 } else if (rw.endsWith("write")) { 587 m.mFieldName = "write-clat-mean"; 588 } 589 t.mPerfMetrics.add(m); 590 591 mTestCases.add(t); 592 } 593 594 /** 595 * Sets up the media server benchmark. 596 * <p> 597 * The test consists of a single process at a higher priority reading or writing to a file 598 * while several other worker processes read and write to a different file. 599 * </p> 600 * 601 * @param rw the type of IO pattern. One of {@code read}, {@code write} 602 */ 603 private void addMediaServerTest(String rw) { 604 String mediaJob = "media-server"; 605 String mediaFile = "mediafile"; 606 String workerJob = "workers"; 607 String workerFile = "workerfile"; 608 609 TestInfo t = new TestInfo(); 610 t.mTestName = String.format("MediaServerBenchmark-%s", rw); 611 t.mKey = "fio_media_server_benchmark"; 612 613 TestFileInfo f = new TestFileInfo(); 614 f.mFileName = new File(mMediaTestDir, mediaFile).getAbsolutePath(); 615 f.mSize = mMediaServerMediaFileSize * 1024; // File size is in MB but we want it in kB. 616 t.mTestFiles.add(f); 617 618 f = new TestFileInfo(); 619 f.mFileName = new File(mMediaTestDir, workerFile).getAbsolutePath(); 620 f.mSize = mMediaServerWorkerFileSize * 1024; // File size is in MB but we want it in kB. 621 t.mTestFiles.add(f); 622 623 JobInfo j = new JobInfo(); 624 j.mJobName = "global"; 625 j.mParameters.put("directory", mMediaTestDir); 626 j.mParameters.put("fsync", "1024"); 627 j.mParameters.put("ioengine", "sync"); 628 j.mParameters.put("runtime", String.format("%d", mMediaServerDuration)); 629 j.mParameters.put("time_based", null); 630 t.mJobs.add(j); 631 632 j = new JobInfo(); 633 j.mJobName = mediaJob; 634 j.mParameters.put("filename", mediaFile); 635 j.mParameters.put("iodepth", "32"); 636 j.mParameters.put("nice", "-16"); 637 j.mParameters.put("rate", "6m"); 638 j.mParameters.put("rw", rw); 639 j.mParameters.put("size", String.format("%dM", mMediaServerMediaFileSize)); 640 t.mJobs.add(j); 641 642 j = new JobInfo(); 643 j.mJobName = workerJob; 644 j.mParameters.put("filename", workerFile); 645 j.mParameters.put("group_reporting", null); 646 j.mParameters.put("new_group", null); 647 j.mParameters.put("nice", "0"); 648 j.mParameters.put("numjobs", String.format("%d", mMediaServerWorkerJobCount)); 649 j.mParameters.put("rw", "randrw"); 650 j.mParameters.put("size", String.format("%dM", mMediaServerWorkerFileSize)); 651 t.mJobs.add(j); 652 653 PerfMetricInfo m = new PerfMetricInfo(); 654 m.mJobName = mediaJob; 655 m.mPostKey = String.format("%s_media_bandwidth", rw); 656 m.mType = PerfMetricInfo.ResultType.FLOAT; 657 if (rw.endsWith("read")) { 658 m.mFieldName = "read-bandwidth-mean"; 659 } else if (rw.endsWith("write")) { 660 m.mFieldName = "write-bandwidth-mean"; 661 } 662 t.mPerfMetrics.add(m); 663 664 m = new PerfMetricInfo(); 665 m.mJobName = mediaJob; 666 m.mPostKey = String.format("%s_media_latency", rw); 667 m.mType = PerfMetricInfo.ResultType.FLOAT; 668 if (rw.endsWith("read")) { 669 m.mFieldName = "read-clat-mean"; 670 } else if (rw.endsWith("write")) { 671 m.mFieldName = "write-clat-mean"; 672 } 673 t.mPerfMetrics.add(m); 674 675 m = new PerfMetricInfo(); 676 m.mJobName = workerJob; 677 m.mPostKey = String.format("%s_workers_read_bandwidth", rw); 678 m.mFieldName = "read-bandwidth-mean"; 679 m.mType = PerfMetricInfo.ResultType.FLOAT; 680 t.mPerfMetrics.add(m); 681 682 m = new PerfMetricInfo(); 683 m.mJobName = workerJob; 684 m.mPostKey = String.format("%s_workers_write_bandwidth", rw); 685 m.mFieldName = "write-bandwidth-mean"; 686 m.mType = PerfMetricInfo.ResultType.FLOAT; 687 t.mPerfMetrics.add(m); 688 689 mTestCases.add(t); 690 } 691 692 /** 693 * Sets up the media scanner benchmark. 694 * <p> 695 * The test consists of a single process reading several small files while several other worker 696 * processes read and write to a different file. 697 * </p> 698 */ 699 private void addMediaScannerTest() { 700 String mediaJob = "media-server"; 701 String mediaFile = "mediafile.%d"; 702 String workerJob = "workers"; 703 String workerFile = "workerfile"; 704 705 TestInfo t = new TestInfo(); 706 t.mTestName = "MediaScannerBenchmark"; 707 t.mKey = "fio_media_scanner_benchmark"; 708 709 TestFileInfo f; 710 for (int i = 0; i < mMediaScannerMediaFileCount; i++) { 711 f = new TestFileInfo(); 712 f.mFileName = new File(mMediaTestDir, String.format(mediaFile, i)).getAbsolutePath(); 713 f.mSize = mMediaScannerMediaFileSize; // File size is already in kB so do nothing. 714 t.mTestFiles.add(f); 715 } 716 717 f = new TestFileInfo(); 718 f.mFileName = new File(mMediaTestDir, workerFile).getAbsolutePath(); 719 f.mSize = mMediaScannerWorkerFileSize * 1024; // File size is in MB but we want it in kB. 720 t.mTestFiles.add(f); 721 722 JobInfo j = new JobInfo(); 723 j.mJobName = "global"; 724 j.mParameters.put("directory", mMediaTestDir); 725 j.mParameters.put("fsync", "1024"); 726 j.mParameters.put("ioengine", "sync"); 727 t.mJobs.add(j); 728 729 j = new JobInfo(); 730 j.mJobName = mediaJob; 731 StringBuilder fileNames = new StringBuilder(); 732 fileNames.append(String.format(mediaFile, 0)); 733 for (int i = 1; i < mMediaScannerMediaFileCount; i++) { 734 fileNames.append(String.format(":%s", String.format(mediaFile, i))); 735 } 736 j.mParameters.put("filename", fileNames.toString()); 737 j.mParameters.put("exitall", null); 738 j.mParameters.put("openfiles", "4"); 739 j.mParameters.put("rw", "read"); 740 t.mJobs.add(j); 741 742 j = new JobInfo(); 743 j.mJobName = workerJob; 744 j.mParameters.put("filename", workerFile); 745 j.mParameters.put("group_reporting", null); 746 j.mParameters.put("new_group", null); 747 j.mParameters.put("numjobs", String.format("%d", mMediaScannerWorkerJobCount)); 748 j.mParameters.put("rw", "randrw"); 749 j.mParameters.put("size", String.format("%dM", mMediaScannerWorkerFileSize)); 750 t.mJobs.add(j); 751 752 PerfMetricInfo m = new PerfMetricInfo(); 753 m.mJobName = mediaJob; 754 m.mPostKey = "media_bandwidth"; 755 m.mFieldName = "read-bandwidth-mean"; 756 m.mType = PerfMetricInfo.ResultType.FLOAT; 757 t.mPerfMetrics.add(m); 758 759 m = new PerfMetricInfo(); 760 m.mJobName = mediaJob; 761 m.mPostKey = "media_latency"; 762 m.mFieldName = "read-clat-mean"; 763 m.mType = PerfMetricInfo.ResultType.FLOAT; 764 t.mPerfMetrics.add(m); 765 766 m = new PerfMetricInfo(); 767 m.mJobName = workerJob; 768 m.mPostKey = "workers_read_bandwidth"; 769 m.mFieldName = "read-bandwidth-mean"; 770 m.mType = PerfMetricInfo.ResultType.FLOAT; 771 t.mPerfMetrics.add(m); 772 773 m = new PerfMetricInfo(); 774 m.mJobName = workerJob; 775 m.mPostKey = "workers_write_bandwidth"; 776 m.mFieldName = "write-bandwidth-mean"; 777 m.mType = PerfMetricInfo.ResultType.FLOAT; 778 t.mPerfMetrics.add(m); 779 780 mTestCases.add(t); 781 } 782 783 /** 784 * Creates the directories needed to run FIO, pushes the FIO executable to the device, and 785 * stops the runtime. 786 * 787 * @throws DeviceNotAvailableException if the device is not available. 788 */ 789 private void setupDevice() throws DeviceNotAvailableException { 790 mTestDevice.executeShellCommand("stop"); 791 mTestDevice.executeShellCommand(String.format("mkdir -p %s", mFioDir)); 792 mTestDevice.executeShellCommand(String.format("mkdir -p %s", mTmpDir)); 793 mTestDevice.executeShellCommand(String.format("mkdir -p %s", mInternalTestDir)); 794 mTestDevice.executeShellCommand(String.format("mkdir -p %s", mMediaTestDir)); 795 if (mExternalTestDir != null) { 796 mTestDevice.executeShellCommand(String.format("mkdir -p %s", mExternalTestDir)); 797 } 798 if (mFioLocation != null) { 799 mTestDevice.pushFile(new File(mFioLocation), mFioBin); 800 mTestDevice.executeShellCommand(String.format("chmod 755 %s", mFioBin)); 801 } 802 } 803 804 /** 805 * Reverses the actions of {@link #setDevice(ITestDevice)}. 806 * 807 * @throws DeviceNotAvailableException If the device is not available. 808 */ 809 private void cleanupDevice() throws DeviceNotAvailableException { 810 if (mExternalTestDir != null) { 811 mTestDevice.executeShellCommand(String.format("rm -r %s", mExternalTestDir)); 812 } 813 mTestDevice.executeShellCommand(String.format("rm -r %s", mMediaTestDir)); 814 mTestDevice.executeShellCommand(String.format("rm -r %s", mInternalTestDir)); 815 mTestDevice.executeShellCommand(String.format("rm -r %s", mTmpDir)); 816 mTestDevice.executeShellCommand(String.format("rm -r %s", mFioDir)); 817 mTestDevice.executeShellCommand("start"); 818 mTestDevice.waitForDeviceAvailable(); 819 } 820 821 /** 822 * Runs a single test, including creating the test files, clearing the cache, collecting before 823 * and after files, running the benchmark, and reporting the results. 824 * 825 * @param test the benchmark. 826 * @param listener the ITestInvocationListener 827 * @throws DeviceNotAvailableException if the device is not available. 828 */ 829 private void runTest(TestInfo test, ITestInvocationListener listener) 830 throws DeviceNotAvailableException { 831 CLog.i("Running %s benchmark", test.mTestName); 832 mTestDevice.executeShellCommand(String.format("rm -r %s/*", mTmpDir)); 833 mTestDevice.executeShellCommand(String.format("rm -r %s/*", mInternalTestDir)); 834 mTestDevice.executeShellCommand(String.format("rm -r %s/*", mMediaTestDir)); 835 if (mExternalTestDir != null) { 836 mTestDevice.executeShellCommand(String.format("rm -r %s/*", mExternalTestDir)); 837 } 838 839 for (TestFileInfo file : test.mTestFiles) { 840 CLog.v("Creating file: %s, size: %dkB", file.mFileName, file.mSize); 841 String cmd = String.format("dd if=/dev/urandom of=%s bs=1024 count=%d", file.mFileName, 842 file.mSize); 843 int timeout = file.mSize * 2 * 1000; // Timeout is 2 seconds per kB. 844 mTestDevice.executeShellCommand(cmd, new NullOutputReceiver(), 845 timeout, TimeUnit.MILLISECONDS, 2); 846 } 847 848 CLog.i("Creating config"); 849 CLog.d("Config file:\n%s", test.createConfig()); 850 mTestDevice.pushString(test.createConfig(), mFioConfig); 851 852 CLog.i("Dropping cache"); 853 mTestDevice.executeShellCommand("echo 3 > /proc/sys/vm/drop_caches"); 854 855 collectLogs(test, listener, "before"); 856 857 CLog.i("Running test"); 858 FioParser output = new FioParser(); 859 // Run FIO with a timeout of 1 hour. 860 mTestDevice.executeShellCommand(String.format("%s --minimal %s", mFioBin, mFioConfig), 861 output, 60 * 60 * 1000, TimeUnit.MILLISECONDS, 2); 862 863 collectLogs(test, listener, "after"); 864 865 // Report metrics 866 Map<String, String> metrics = new HashMap<>(); 867 String key = mKeySuffix == null ? test.mKey : test.mKey + mKeySuffix; 868 869 listener.testRunStarted(key, 0); 870 for (PerfMetricInfo m : test.mPerfMetrics) { 871 if (!output.mResults.containsKey(m.mJobName)) { 872 CLog.w("Job name %s was not found in the results", m.mJobName); 873 continue; 874 } 875 876 String value = output.getResult(m.mJobName, m.mFieldName); 877 if (value != null) { 878 metrics.put(m.mPostKey, m.mType.value(value)); 879 } else { 880 CLog.w("%s was not in results for the job %s", m.mFieldName, m.mJobName); 881 } 882 } 883 884 CLog.d("About to report metrics to %s: %s", key, metrics); 885 listener.testRunEnded(0, metrics); 886 } 887 888 private void collectLogs(TestInfo testInfo, ITestInvocationListener listener, 889 String descriptor) throws DeviceNotAvailableException { 890 if (mCollectYaffsLogs && mTestDevice.doesFileExist("/proc/yaffs")) { 891 logFile("/proc/yaffs", String.format("%s-yaffs-%s", testInfo.mTestName, descriptor), 892 mTestDevice, listener); 893 } 894 } 895 896 private void logFile(String remoteFileName, String localFileName, ITestDevice testDevice, 897 ITestInvocationListener listener) throws DeviceNotAvailableException { 898 File outputFile = null; 899 InputStreamSource outputSource = null; 900 try { 901 outputFile = testDevice.pullFile(remoteFileName); 902 if (outputFile != null) { 903 CLog.d("Sending %d byte file %s to logosphere!", outputFile.length(), outputFile); 904 outputSource = new FileInputStreamSource(outputFile); 905 listener.testLog(localFileName, LogDataType.TEXT, outputSource); 906 } 907 } finally { 908 FileUtil.deleteFile(outputFile); 909 StreamUtil.cancel(outputSource); 910 } 911 } 912 913 /** 914 * {@inheritDoc} 915 */ 916 @Override 917 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 918 Assert.assertNotNull(mTestDevice); 919 920 mFioDir = new File(mTestDevice.getMountPoint(IDevice.MNT_DATA), "fio").getAbsolutePath(); 921 if (mFioLocation != null) { 922 mFioBin = new File(mFioDir, "fio").getAbsolutePath(); 923 } else { 924 mFioBin = "fio"; 925 } 926 mFioConfig = new File(mFioDir, "config.fio").getAbsolutePath(); 927 928 setupTests(); 929 setupDevice(); 930 931 for (TestInfo test : mTestCases) { 932 runTest(test, listener); 933 } 934 935 cleanupDevice(); 936 } 937 938 /** 939 * {@inheritDoc} 940 */ 941 @Override 942 public void setDevice(ITestDevice device) { 943 mTestDevice = device; 944 } 945 946 /** 947 * {@inheritDoc} 948 */ 949 @Override 950 public ITestDevice getDevice() { 951 return mTestDevice; 952 } 953 954 /** 955 * A meta-test to ensure that the bits of FioBenchmarkTest are working properly. 956 */ 957 public static class MetaTest extends TestCase { 958 959 /** 960 * Test that {@link JobInfo#createJob()} properly formats a job. 961 */ 962 public void testCreateJob() { 963 JobInfo j = new JobInfo(); 964 assertEquals("", j.createJob()); 965 j.mJobName = "job"; 966 assertEquals("[job]\n", j.createJob()); 967 j.mParameters.put("param1", null); 968 j.mParameters.put("param2", "value"); 969 String[] lines = j.createJob().split("\n"); 970 assertEquals(3, lines.length); 971 assertEquals("[job]", lines[0]); 972 Set<String> params = new HashSet<>(2); 973 params.add(lines[1]); 974 params.add(lines[2]); 975 assertTrue(params.contains("param1")); 976 assertTrue(params.contains("param2=value")); 977 } 978 979 /** 980 * Test that {@link TestInfo#createConfig()} properly formats a config. 981 */ 982 public void testCreateConfig() { 983 TestInfo t = new TestInfo(); 984 JobInfo j = new JobInfo(); 985 j.mJobName = "job1"; 986 j.mParameters.put("param1", "value1"); 987 t.mJobs.add(j); 988 989 j = new JobInfo(); 990 j.mJobName = "job2"; 991 j.mParameters.put("param2", "value2"); 992 t.mJobs.add(j); 993 994 j = new JobInfo(); 995 j.mJobName = "job3"; 996 j.mParameters.put("param3", "value3"); 997 t.mJobs.add(j); 998 999 assertEquals("[job1]\nparam1=value1\n\n" + 1000 "[job2]\nparam2=value2\n\n" + 1001 "[job3]\nparam3=value3\n\n", t.createConfig()); 1002 } 1003 1004 /** 1005 * Test that output lines are parsed correctly by the FioParser, invalid lines are ignored, 1006 * and that the various fields are accessible with 1007 * {@link FioParser#getResult(String, String)}. 1008 */ 1009 public void testFioParser() { 1010 String[] lines = new String[4]; 1011 // We build the lines up as follows (assuming FIO_RESULTS_FIELDS.length == 58): 1012 // 0;3;6;...;171 1013 // 1;4;7;...;172 1014 // 2;5;8;...;173 1015 for (int i = 0; i < 3; i++) { 1016 StringBuilder sb = new StringBuilder(); 1017 sb.append(i); 1018 for (int j = 1; j < FIO_V0_RESULT_FIELDS.length; j++) { 1019 sb.append(";"); 1020 sb.append(j * 3 + i); 1021 } 1022 lines[i] = sb.toString(); 1023 } 1024 // A line may have an optional description on the end which we don't care about, make 1025 // sure it still parses. 1026 lines[2] = lines[2] += ";description"; 1027 // Make sure an invalid output line does not parse. 1028 lines[3] = "invalid"; 1029 1030 FioParser p = new FioParser(); 1031 p.processNewLines(lines); 1032 1033 1034 for (int i = 0; i < 3; i++) { 1035 for (int j = 0; j < FIO_V0_RESULT_FIELDS.length; j++) { 1036 assertEquals(String.format("job=%d, field=%s", i, FIO_V0_RESULT_FIELDS[j]), 1037 String.format("%d", j * 3 + i), 1038 p.getResult(String.format("%d", i), FIO_V0_RESULT_FIELDS[j])); 1039 } 1040 } 1041 assertNull(p.getResult("missing", "jobname")); 1042 assertNull(p.getResult("invalid", "jobname")); 1043 assertNull(p.getResult("0", "missing")); 1044 } 1045 1046 /** 1047 * Test that {@link PerfMetricInfo.ResultType#value(String)} correctly transforms strings 1048 * based on the result type. 1049 */ 1050 public void testResultTypeValue() { 1051 assertEquals("test", PerfMetricInfo.ResultType.STRING.value("test")); 1052 assertEquals("1", PerfMetricInfo.ResultType.INT.value("1")); 1053 assertEquals("3.14159", PerfMetricInfo.ResultType.FLOAT.value("3.14159")); 1054 assertEquals(String.format("%f", 0.34567), 1055 PerfMetricInfo.ResultType.PERCENT.value("34.567%")); 1056 assertNull(PerfMetricInfo.ResultType.PERCENT.value("")); 1057 assertNull(PerfMetricInfo.ResultType.PERCENT.value("34.567")); 1058 assertNull(PerfMetricInfo.ResultType.PERCENT.value("test%")); 1059 } 1060 } 1061 } 1062