blob: 83b119fa923b8b146c6a382a86164e3b91fdb1bd [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.performance.tests;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.NullOutputReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import junit.framework.TestCase;
import org.junit.Assert;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Runs the FIO benchmarks.
*
* <p>This test pushes the FIO executable to the device and runs several benchmarks. Running each
* benchmark consists of creating a config file, creating one or more data files, clearing the disk
* cache and then running FIO. The test runs a variety of different configurations including a
* simple benchmark with a single thread, a storage benchmark with 4 threads, a media server
* emulator, and a media scanner emulator.
*/
public class FioBenchmarkTest implements IDeviceTest, IRemoteTest {
// TODO: Refactor this to only pick out fields we care about.
private static final String[] FIO_V0_RESULT_FIELDS = {
"jobname",
"groupid",
"error",
// Read stats
"read-kb-io",
"read-bandwidth",
"read-runtime",
"read-slat-min",
"read-slat-max",
"read-slat-mean",
"read-slat-stddev",
"read-clat-min",
"read-clat-max",
"read-clat-mean",
"read-clat-stddev",
"read-bandwidth-min",
"read-bandwidth-max",
"read-bandwidth-percent",
"read-bandwidth-mean",
"read-bandwidth-stddev",
// Write stats
"write-kb-io",
"write-bandwidth",
"write-runtime",
"write-slat-min",
"write-slat-max",
"write-slat-mean",
"write-slat-stddev",
"write-clat-min",
"write-clat-max",
"write-clat-mean",
"write-clat-stddev",
"write-bandwidth-min",
"write-bandwidth-max",
"write-bandwidth-percent",
"write-bandwidth-mean",
"write-bandwidth-stddev",
// CPU stats
"cpu-user",
"cpu-system",
"cpu-context-switches",
"cpu-major-page-faults",
"cpu-minor-page-faults",
// IO depth stats
"io-depth-1",
"io-depth-2",
"io-depth-4",
"io-depth-8",
"io-depth-16",
"io-depth-32",
"io-depth-64",
// IO lat stats
"io-lat-2-ms",
"io-lat-4-ms",
"io-lat-10-ms",
"io-lat-20-ms",
"io-lat-50-ms",
"io-lat-100-ms",
"io-lat-250-ms",
"io-lat-500-ms",
"io-lat-750-ms",
"io-lat-1000-ms",
"io-lat-2000-ms"
};
private static final String[] FIO_V3_RESULT_FIELDS = {
"terse-version",
"fio-version",
"jobname",
"groupid",
"error",
// Read stats
"read-kb-io",
"read-bandwidth",
"read-iops",
"read-runtime",
"read-slat-min",
"read-slat-max",
"read-slat-mean",
"read-slat-stddev",
"read-clat-min",
"read-clat-max",
"read-clat-mean",
"read-clat-stddev",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"read-lat-min",
"read-lat-max",
"read-lat-mean",
"read-lat-stddev",
"read-bandwidth-min",
"read-bandwidth-max",
"read-bandwidth-percent",
"read-bandwidth-mean",
"read-bandwidth-stddev",
// Write stats
"write-kb-io",
"write-bandwidth",
"write-iops",
"write-runtime",
"write-slat-min",
"write-slat-max",
"write-slat-mean",
"write-slat-stddev",
"write-clat-min",
"write-clat-max",
"write-clat-mean",
"write-clat-stddev",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"write-lat-min",
"write-lat-max",
"write-lat-mean",
"write-lat-stddev",
"write-bandwidth-min",
"write-bandwidth-max",
"write-bandwidth-percent",
"write-bandwidth-mean",
"write-bandwidth-stddev",
// CPU stats
"cpu-user",
"cpu-system",
"cpu-context-switches",
"cpu-major-page-faults",
"cpu-minor-page-faults",
// IO depth stats
"io-depth-1",
"io-depth-2",
"io-depth-4",
"io-depth-8",
"io-depth-16",
"io-depth-32",
"io-depth-64",
// IO lat stats
"io-lat-2-us",
"io-lat-4-us",
"io-lat-10-us",
"io-lat-20-us",
"io-lat-50-us",
"io-lat-100-us",
"io-lat-250-us",
"io-lat-500-us",
"io-lat-750-us",
"io-lat-1000-us",
"io-lat-2-ms",
"io-lat-4-ms",
"io-lat-10-ms",
"io-lat-20-ms",
"io-lat-50-ms",
"io-lat-100-ms",
"io-lat-250-ms",
"io-lat-500-ms",
"io-lat-750-ms",
"io-lat-1000-ms",
"io-lat-2000-ms",
"io-lat-greater"
};
private List<TestInfo> mTestCases = null;
/**
* Holds info about a job. The job translates into a job in the FIO config file. Contains the
* job name and a map from keys to values.
*/
private static class JobInfo {
public String mJobName = null;
public Map<String, String> mParameters = new HashMap<>();
/**
* Gets the job as a string.
*
* @return a string of the job formatted for the config file.
*/
public String createJob() {
if (mJobName == null) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(String.format("[%s]\n", mJobName));
for (Entry<String, String> parameter : mParameters.entrySet()) {
if (parameter.getValue() == null) {
sb.append(String.format("%s\n", parameter.getKey()));
} else {
sb.append(String.format("%s=%s\n", parameter.getKey(), parameter.getValue()));
}
}
return sb.toString();
}
}
/**
* Holds info about a file used in the benchmark. Because of limitations in FIO on Android, the
* file needs to be created before the tests are run. Contains the file name and size in kB.
*/
private static class TestFileInfo {
public String mFileName = null;
public int mSize = -1;
}
/** Holds info about the perf metric that are cared about for a given job. */
private static class PerfMetricInfo {
public enum ResultType {
STRING,
INT,
FLOAT,
PERCENT;
String value(String input) {
switch (this) {
case STRING:
case INT:
case FLOAT:
return input;
case PERCENT:
if (input.length() < 2 || !input.endsWith("%")) {
return null;
}
try {
return String.format(
"%f",
Double.parseDouble(input.substring(0, input.length() - 1))
/ 100);
} catch (NumberFormatException e) {
return null;
}
default:
return null;
}
}
}
public String mJobName = null;
public String mFieldName = null;
public String mPostKey = null;
public ResultType mType = ResultType.STRING;
}
/**
* Holds the info associated with a test.
*
* <p>Contains the test name, key, a list of {@link JobInfo}, a set of {@link TestFileInfo}, and
* a set of {@link PerfMetricInfo}.
*/
private static class TestInfo {
public String mTestName = null;
public String mKey = null;
public List<JobInfo> mJobs = new LinkedList<>();
public Set<TestFileInfo> mTestFiles = new HashSet<>();
public Set<PerfMetricInfo> mPerfMetrics = new HashSet<>();
/**
* Gets the config file.
*
* @return a string containing the contents of the config file needed to run the benchmark.
*/
private String createConfig() {
StringBuilder sb = new StringBuilder();
for (JobInfo job : mJobs) {
sb.append(String.format("%s\n", job.createJob()));
}
return sb.toString();
}
}
/**
* Parses the output of the FIO and allows the values to be looked up by job name and property.
*/
private static class FioParser extends MultiLineReceiver {
public Map<String, Map<String, String>> mResults = new HashMap<>();
/**
* Gets the result for a job and property, or null if the job or the property do not exist.
*
* @param job the name of the job.
* @param property the name of the property. See {@code FIO_RESULT_FIELDS}.
* @return the fio results for the job and property or null if it does not exist.
*/
public String getResult(String job, String property) {
if (!mResults.containsKey(job)) {
return null;
}
return mResults.get(job).get(property);
}
/** {@inheritDoc} */
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
CLog.d(line);
String[] fields = line.split(";");
if (fields.length < FIO_V0_RESULT_FIELDS.length) {
continue;
}
if (fields.length < FIO_V3_RESULT_FIELDS.length) {
Map<String, String> r = new HashMap<>();
for (int i = 0; i < FIO_V0_RESULT_FIELDS.length; i++) {
r.put(FIO_V0_RESULT_FIELDS[i], fields[i]);
}
mResults.put(fields[0], r); // Job name is index 0
} else if ("3".equals(fields[0])) {
Map<String, String> r = new HashMap<>();
for (int i = 0; i < FIO_V3_RESULT_FIELDS.length; i++) {
r.put(FIO_V3_RESULT_FIELDS[i], fields[i]);
}
mResults.put(fields[2], r); // Job name is index 2
} else {
Assert.fail("Unknown fio terse output version");
}
}
}
/** {@inheritDoc} */
@Override
public boolean isCancelled() {
return false;
}
}
ITestDevice mTestDevice = null;
private String mFioDir = null;
private String mFioBin = null;
private String mFioConfig = null;
@Option(
name = "fio-location",
description =
"The path to the precompiled FIO executable. If "
+ "unset, try to use fio from the system image.")
private String mFioLocation = null;
@Option(name = "tmp-dir", description = "The directory used for interal benchmarks.")
private String mTmpDir = "/data/tmp/fio";
@Option(name = "internal-test-dir", description = "The directory used for interal benchmarks.")
private String mInternalTestDir = "/data/fio/data";
@Option(name = "media-test-dir", description = "The directory used for media benchmarks.")
private String mMediaTestDir = "${EXTERNAL_STORAGE}/fio";
@Option(name = "external-test-dir", description = "The directory used for external benchmarks.")
private String mExternalTestDir = "${EXTERNAL_STORAGE}/fio";
@Option(name = "collect-yaffs-logs", description = "Collect yaffs logs before and after tests")
private Boolean mCollectYaffsLogs = false;
@Option(name = "run-simple-internal-test", description = "Run the simple internal benchmark.")
private Boolean mRunSimpleInternalTest = true;
@Option(
name = "simple-internal-file-size",
description = "The file size of the simple internal benchmark in MB.")
private int mSimpleInternalFileSize = 256;
@Option(name = "run-simple-external-test", description = "Run the simple external benchmark.")
private Boolean mRunSimpleExternalTest = false;
@Option(
name = "simple-external-file-size",
description = "The file size of the simple external benchmark in MB.")
private int mSimpleExternalFileSize = 256;
@Option(name = "run-storage-internal-test", description = "Run the storage internal benchmark.")
private Boolean mRunStorageInternalTest = true;
@Option(
name = "storage-internal-file-size",
description = "The file size of the storage internal benchmark in MB.")
private int mStorageInternalFileSize = 256;
@Option(
name = "storage-internal-job-count",
description = "The number of jobs for the storage internal benchmark.")
private int mStorageInternalJobCount = 4;
@Option(name = "run-storage-external-test", description = "Run the storage external benchmark.")
private Boolean mRunStorageExternalTest = false;
@Option(
name = "storage-external-file-size",
description = "The file size of the storage external benchmark in MB.")
private int mStorageExternalFileSize = 256;
@Option(
name = "storage-external-job-count",
description = "The number of jobs for the storage external benchmark.")
private int mStorageExternalJobCount = 4;
@Option(name = "run-media-server-test", description = "Run the media server benchmark.")
private Boolean mRunMediaServerTest = false;
@Option(
name = "media-server-duration",
description = "The duration of the media server benchmark in secs.")
private long mMediaServerDuration = 30;
@Option(
name = "media-server-media-file-size",
description = "The media file size of the media server benchmark in MB.")
private int mMediaServerMediaFileSize = 256;
@Option(
name = "media-server-worker-file-size",
description = "The worker file size of the media server benchmark in MB.")
private int mMediaServerWorkerFileSize = 256;
@Option(
name = "media-server-worker-job-count",
description = "The number of worker jobs for the media server benchmark.")
private int mMediaServerWorkerJobCount = 4;
@Option(name = "run-media-scanner-test", description = "Run the media scanner benchmark.")
private Boolean mRunMediaScannerTest = false;
@Option(
name = "media-scanner-media-file-size",
description = "The media file size of the media scanner benchmark in kB.")
private int mMediaScannerMediaFileSize = 8;
@Option(
name = "media-scanner-media-file-count",
description = "The number of media files to scan.")
private int mMediaScannerMediaFileCount = 256;
@Option(
name = "media-scanner-worker-file-size",
description = "The worker file size of the media scanner benchmark in MB.")
private int mMediaScannerWorkerFileSize = 256;
@Option(
name = "media-scanner-worker-job-count",
description = "The number of worker jobs for the media server benchmark.")
private int mMediaScannerWorkerJobCount = 4;
@Option(
name = "key-suffix",
description = "The suffix to add to the reporting key in order to override the default")
private String mKeySuffix = null;
/** Sets up all the benchmarks. */
private void setupTests() {
if (mTestCases != null) {
// assume already set up
return;
}
mTestCases = new LinkedList<>();
if (mRunSimpleInternalTest) {
addSimpleTest("read", "sync", true);
addSimpleTest("write", "sync", true);
addSimpleTest("randread", "sync", true);
addSimpleTest("randwrite", "sync", true);
addSimpleTest("randread", "mmap", true);
addSimpleTest("randwrite", "mmap", true);
}
if (mRunSimpleExternalTest) {
addSimpleTest("read", "sync", false);
addSimpleTest("write", "sync", false);
addSimpleTest("randread", "sync", false);
addSimpleTest("randwrite", "sync", false);
addSimpleTest("randread", "mmap", false);
addSimpleTest("randwrite", "mmap", false);
}
if (mRunStorageInternalTest) {
addStorageTest("read", "sync", true);
addStorageTest("write", "sync", true);
addStorageTest("randread", "sync", true);
addStorageTest("randwrite", "sync", true);
addStorageTest("randread", "mmap", true);
addStorageTest("randwrite", "mmap", true);
}
if (mRunStorageExternalTest) {
addStorageTest("read", "sync", false);
addStorageTest("write", "sync", false);
addStorageTest("randread", "sync", false);
addStorageTest("randwrite", "sync", false);
addStorageTest("randread", "mmap", false);
addStorageTest("randwrite", "mmap", false);
}
if (mRunMediaServerTest) {
addMediaServerTest("read");
addMediaServerTest("write");
}
if (mRunMediaScannerTest) {
addMediaScannerTest();
}
}
/**
* Sets up the simple FIO benchmark.
*
* <p>The test consists of a single process reading or writing to a file.
*
* @param rw the type of IO pattern. One of {@code read}, {@code write}, {@code randread}, or
* {@code randwrite}.
* @param ioengine defines how the job issues I/O. Such as {@code sync}, {@code vsync}, {@code
* mmap}, or {@code cpuio} and others.
* @param internal whether the test should be run on the internal (/data) partition or the
* external partition.
*/
private void addSimpleTest(String rw, String ioengine, boolean internal) {
String fileName = "testfile";
String jobName = "job";
String directory;
int fileSize;
TestInfo t = new TestInfo();
if (internal) {
t.mTestName = String.format("SimpleBenchmark-int-%s-%s", ioengine, rw);
t.mKey = "fio_simple_int_benchmark";
directory = mInternalTestDir;
fileSize = mSimpleInternalFileSize;
} else {
t.mTestName = String.format("SimpleBenchmark-ext-%s-%s", ioengine, rw);
t.mKey = "fio_simple_ext_benchmark";
directory = mExternalTestDir;
fileSize = mSimpleExternalFileSize;
}
TestFileInfo f = new TestFileInfo();
f.mFileName = new File(directory, fileName).getAbsolutePath();
f.mSize = fileSize * 1024; // fileSize is in MB but we want it in kB.
t.mTestFiles.add(f);
JobInfo j = new JobInfo();
j.mJobName = jobName;
j.mParameters.put("directory", directory);
j.mParameters.put("filename", fileName);
j.mParameters.put("fsync", "1024");
j.mParameters.put("ioengine", ioengine);
j.mParameters.put("rw", rw);
j.mParameters.put("size", String.format("%dM", fileSize));
t.mJobs.add(j);
PerfMetricInfo m = new PerfMetricInfo();
m.mJobName = jobName;
if ("sync".equals(ioengine)) {
m.mPostKey = String.format("%s_bandwidth", rw);
} else {
m.mPostKey = String.format("%s_%s_bandwidth", rw, ioengine);
}
m.mType = PerfMetricInfo.ResultType.FLOAT;
if (rw.endsWith("read")) {
m.mFieldName = "read-bandwidth-mean";
} else if (rw.endsWith("write")) {
m.mFieldName = "write-bandwidth-mean";
}
t.mPerfMetrics.add(m);
mTestCases.add(t);
}
/**
* Sets up the storage FIO benchmark.
*
* <p>The test consists of several processes reading or writing to a file.
*
* @param rw the type of IO pattern. One of {@code read}, {@code write}, {@code randread}, or
* {@code randwrite}.
* @param ioengine defines how the job issues I/O. Such as {@code sync}, {@code vsync}, {@code
* mmap}, or {@code cpuio} and others.
* @param internal whether the test should be run on the internal (/data) partition or the
* external partition.
*/
private void addStorageTest(String rw, String ioengine, boolean internal) {
String fileName = "testfile";
String jobName = "workers";
String directory;
int fileSize;
int jobCount;
TestInfo t = new TestInfo();
if (internal) {
t.mTestName = String.format("StorageBenchmark-int-%s-%s", ioengine, rw);
t.mKey = "fio_storage_int_benchmark";
directory = mInternalTestDir;
fileSize = mStorageInternalFileSize;
jobCount = mStorageInternalJobCount;
} else {
t.mTestName = String.format("StorageBenchmark-ext-%s-%s", ioengine, rw);
t.mKey = "fio_storage_ext_benchmark";
directory = mExternalTestDir;
fileSize = mStorageExternalFileSize;
jobCount = mStorageExternalJobCount;
}
TestFileInfo f = new TestFileInfo();
f.mFileName = new File(directory, fileName).getAbsolutePath();
f.mSize = fileSize * 1024; // fileSize is in MB but we want it in kB.
t.mTestFiles.add(f);
JobInfo j = new JobInfo();
j.mJobName = jobName;
j.mParameters.put("directory", directory);
j.mParameters.put("filename", fileName);
j.mParameters.put("fsync", "1024");
j.mParameters.put("group_reporting", null);
j.mParameters.put("ioengine", ioengine);
j.mParameters.put("new_group", null);
j.mParameters.put("numjobs", String.format("%d", jobCount));
j.mParameters.put("rw", rw);
j.mParameters.put("size", String.format("%dM", fileSize));
t.mJobs.add(j);
PerfMetricInfo m = new PerfMetricInfo();
m.mJobName = jobName;
if ("sync".equals(ioengine)) {
m.mPostKey = String.format("%s_bandwidth", rw);
} else {
m.mPostKey = String.format("%s_%s_bandwidth", rw, ioengine);
}
m.mType = PerfMetricInfo.ResultType.FLOAT;
if (rw.endsWith("read")) {
m.mFieldName = "read-bandwidth-mean";
} else if (rw.endsWith("write")) {
m.mFieldName = "write-bandwidth-mean";
}
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = jobName;
if ("sync".equals(ioengine)) {
m.mPostKey = String.format("%s_latency", rw);
} else {
m.mPostKey = String.format("%s_%s_latency", rw, ioengine);
}
m.mType = PerfMetricInfo.ResultType.FLOAT;
if (rw.endsWith("read")) {
m.mFieldName = "read-clat-mean";
} else if (rw.endsWith("write")) {
m.mFieldName = "write-clat-mean";
}
t.mPerfMetrics.add(m);
mTestCases.add(t);
}
/**
* Sets up the media server benchmark.
*
* <p>The test consists of a single process at a higher priority reading or writing to a file
* while several other worker processes read and write to a different file.
*
* @param rw the type of IO pattern. One of {@code read}, {@code write}
*/
private void addMediaServerTest(String rw) {
String mediaJob = "media-server";
String mediaFile = "mediafile";
String workerJob = "workers";
String workerFile = "workerfile";
TestInfo t = new TestInfo();
t.mTestName = String.format("MediaServerBenchmark-%s", rw);
t.mKey = "fio_media_server_benchmark";
TestFileInfo f = new TestFileInfo();
f.mFileName = new File(mMediaTestDir, mediaFile).getAbsolutePath();
f.mSize = mMediaServerMediaFileSize * 1024; // File size is in MB but we want it in kB.
t.mTestFiles.add(f);
f = new TestFileInfo();
f.mFileName = new File(mMediaTestDir, workerFile).getAbsolutePath();
f.mSize = mMediaServerWorkerFileSize * 1024; // File size is in MB but we want it in kB.
t.mTestFiles.add(f);
JobInfo j = new JobInfo();
j.mJobName = "global";
j.mParameters.put("directory", mMediaTestDir);
j.mParameters.put("fsync", "1024");
j.mParameters.put("ioengine", "sync");
j.mParameters.put("runtime", String.format("%d", mMediaServerDuration));
j.mParameters.put("time_based", null);
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = mediaJob;
j.mParameters.put("filename", mediaFile);
j.mParameters.put("iodepth", "32");
j.mParameters.put("nice", "-16");
j.mParameters.put("rate", "6m");
j.mParameters.put("rw", rw);
j.mParameters.put("size", String.format("%dM", mMediaServerMediaFileSize));
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = workerJob;
j.mParameters.put("filename", workerFile);
j.mParameters.put("group_reporting", null);
j.mParameters.put("new_group", null);
j.mParameters.put("nice", "0");
j.mParameters.put("numjobs", String.format("%d", mMediaServerWorkerJobCount));
j.mParameters.put("rw", "randrw");
j.mParameters.put("size", String.format("%dM", mMediaServerWorkerFileSize));
t.mJobs.add(j);
PerfMetricInfo m = new PerfMetricInfo();
m.mJobName = mediaJob;
m.mPostKey = String.format("%s_media_bandwidth", rw);
m.mType = PerfMetricInfo.ResultType.FLOAT;
if (rw.endsWith("read")) {
m.mFieldName = "read-bandwidth-mean";
} else if (rw.endsWith("write")) {
m.mFieldName = "write-bandwidth-mean";
}
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = mediaJob;
m.mPostKey = String.format("%s_media_latency", rw);
m.mType = PerfMetricInfo.ResultType.FLOAT;
if (rw.endsWith("read")) {
m.mFieldName = "read-clat-mean";
} else if (rw.endsWith("write")) {
m.mFieldName = "write-clat-mean";
}
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = workerJob;
m.mPostKey = String.format("%s_workers_read_bandwidth", rw);
m.mFieldName = "read-bandwidth-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = workerJob;
m.mPostKey = String.format("%s_workers_write_bandwidth", rw);
m.mFieldName = "write-bandwidth-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
mTestCases.add(t);
}
/**
* Sets up the media scanner benchmark.
*
* <p>The test consists of a single process reading several small files while several other
* worker processes read and write to a different file.
*/
private void addMediaScannerTest() {
String mediaJob = "media-server";
String mediaFile = "mediafile.%d";
String workerJob = "workers";
String workerFile = "workerfile";
TestInfo t = new TestInfo();
t.mTestName = "MediaScannerBenchmark";
t.mKey = "fio_media_scanner_benchmark";
TestFileInfo f;
for (int i = 0; i < mMediaScannerMediaFileCount; i++) {
f = new TestFileInfo();
f.mFileName = new File(mMediaTestDir, String.format(mediaFile, i)).getAbsolutePath();
f.mSize = mMediaScannerMediaFileSize; // File size is already in kB so do nothing.
t.mTestFiles.add(f);
}
f = new TestFileInfo();
f.mFileName = new File(mMediaTestDir, workerFile).getAbsolutePath();
f.mSize = mMediaScannerWorkerFileSize * 1024; // File size is in MB but we want it in kB.
t.mTestFiles.add(f);
JobInfo j = new JobInfo();
j.mJobName = "global";
j.mParameters.put("directory", mMediaTestDir);
j.mParameters.put("fsync", "1024");
j.mParameters.put("ioengine", "sync");
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = mediaJob;
StringBuilder fileNames = new StringBuilder();
fileNames.append(String.format(mediaFile, 0));
for (int i = 1; i < mMediaScannerMediaFileCount; i++) {
fileNames.append(String.format(":%s", String.format(mediaFile, i)));
}
j.mParameters.put("filename", fileNames.toString());
j.mParameters.put("exitall", null);
j.mParameters.put("openfiles", "4");
j.mParameters.put("rw", "read");
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = workerJob;
j.mParameters.put("filename", workerFile);
j.mParameters.put("group_reporting", null);
j.mParameters.put("new_group", null);
j.mParameters.put("numjobs", String.format("%d", mMediaScannerWorkerJobCount));
j.mParameters.put("rw", "randrw");
j.mParameters.put("size", String.format("%dM", mMediaScannerWorkerFileSize));
t.mJobs.add(j);
PerfMetricInfo m = new PerfMetricInfo();
m.mJobName = mediaJob;
m.mPostKey = "media_bandwidth";
m.mFieldName = "read-bandwidth-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = mediaJob;
m.mPostKey = "media_latency";
m.mFieldName = "read-clat-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = workerJob;
m.mPostKey = "workers_read_bandwidth";
m.mFieldName = "read-bandwidth-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
m = new PerfMetricInfo();
m.mJobName = workerJob;
m.mPostKey = "workers_write_bandwidth";
m.mFieldName = "write-bandwidth-mean";
m.mType = PerfMetricInfo.ResultType.FLOAT;
t.mPerfMetrics.add(m);
mTestCases.add(t);
}
/**
* Creates the directories needed to run FIO, pushes the FIO executable to the device, and stops
* the runtime.
*
* @throws DeviceNotAvailableException if the device is not available.
*/
private void setupDevice() throws DeviceNotAvailableException {
mTestDevice.executeShellCommand("stop");
mTestDevice.executeShellCommand(String.format("mkdir -p %s", mFioDir));
mTestDevice.executeShellCommand(String.format("mkdir -p %s", mTmpDir));
mTestDevice.executeShellCommand(String.format("mkdir -p %s", mInternalTestDir));
mTestDevice.executeShellCommand(String.format("mkdir -p %s", mMediaTestDir));
if (mExternalTestDir != null) {
mTestDevice.executeShellCommand(String.format("mkdir -p %s", mExternalTestDir));
}
if (mFioLocation != null) {
mTestDevice.pushFile(new File(mFioLocation), mFioBin);
mTestDevice.executeShellCommand(String.format("chmod 755 %s", mFioBin));
}
}
/**
* Reverses the actions of {@link #setDevice(ITestDevice)}.
*
* @throws DeviceNotAvailableException If the device is not available.
*/
private void cleanupDevice() throws DeviceNotAvailableException {
if (mExternalTestDir != null) {
mTestDevice.executeShellCommand(String.format("rm -r %s", mExternalTestDir));
}
mTestDevice.executeShellCommand(String.format("rm -r %s", mMediaTestDir));
mTestDevice.executeShellCommand(String.format("rm -r %s", mInternalTestDir));
mTestDevice.executeShellCommand(String.format("rm -r %s", mTmpDir));
mTestDevice.executeShellCommand(String.format("rm -r %s", mFioDir));
mTestDevice.executeShellCommand("start");
mTestDevice.waitForDeviceAvailable();
}
/**
* Runs a single test, including creating the test files, clearing the cache, collecting before
* and after files, running the benchmark, and reporting the results.
*
* @param test the benchmark.
* @param listener the ITestInvocationListener
* @throws DeviceNotAvailableException if the device is not available.
*/
private void runTest(TestInfo test, ITestInvocationListener listener)
throws DeviceNotAvailableException {
CLog.i("Running %s benchmark", test.mTestName);
mTestDevice.executeShellCommand(String.format("rm -r %s/*", mTmpDir));
mTestDevice.executeShellCommand(String.format("rm -r %s/*", mInternalTestDir));
mTestDevice.executeShellCommand(String.format("rm -r %s/*", mMediaTestDir));
if (mExternalTestDir != null) {
mTestDevice.executeShellCommand(String.format("rm -r %s/*", mExternalTestDir));
}
for (TestFileInfo file : test.mTestFiles) {
CLog.v("Creating file: %s, size: %dkB", file.mFileName, file.mSize);
String cmd =
String.format(
"dd if=/dev/urandom of=%s bs=1024 count=%d",
file.mFileName, file.mSize);
int timeout = file.mSize * 2 * 1000; // Timeout is 2 seconds per kB.
mTestDevice.executeShellCommand(
cmd, new NullOutputReceiver(), timeout, TimeUnit.MILLISECONDS, 2);
}
CLog.i("Creating config");
CLog.d("Config file:\n%s", test.createConfig());
mTestDevice.pushString(test.createConfig(), mFioConfig);
CLog.i("Dropping cache");
mTestDevice.executeShellCommand("echo 3 > /proc/sys/vm/drop_caches");
collectLogs(test, listener, "before");
CLog.i("Running test");
FioParser output = new FioParser();
// Run FIO with a timeout of 1 hour.
mTestDevice.executeShellCommand(
String.format("%s --minimal %s", mFioBin, mFioConfig),
output,
60 * 60 * 1000,
TimeUnit.MILLISECONDS,
2);
collectLogs(test, listener, "after");
// Report metrics
Map<String, String> metrics = new HashMap<>();
String key = mKeySuffix == null ? test.mKey : test.mKey + mKeySuffix;
listener.testRunStarted(key, 0);
for (PerfMetricInfo m : test.mPerfMetrics) {
if (!output.mResults.containsKey(m.mJobName)) {
CLog.w("Job name %s was not found in the results", m.mJobName);
continue;
}
String value = output.getResult(m.mJobName, m.mFieldName);
if (value != null) {
metrics.put(m.mPostKey, m.mType.value(value));
} else {
CLog.w("%s was not in results for the job %s", m.mFieldName, m.mJobName);
}
}
CLog.d("About to report metrics to %s: %s", key, metrics);
listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(metrics));
}
private void collectLogs(TestInfo testInfo, ITestInvocationListener listener, String descriptor)
throws DeviceNotAvailableException {
if (mCollectYaffsLogs && mTestDevice.doesFileExist("/proc/yaffs")) {
logFile(
"/proc/yaffs",
String.format("%s-yaffs-%s", testInfo.mTestName, descriptor),
mTestDevice,
listener);
}
}
private void logFile(
String remoteFileName,
String localFileName,
ITestDevice testDevice,
ITestInvocationListener listener)
throws DeviceNotAvailableException {
File outputFile = null;
InputStreamSource outputSource = null;
try {
outputFile = testDevice.pullFile(remoteFileName);
if (outputFile != null) {
CLog.d("Sending %d byte file %s to logosphere!", outputFile.length(), outputFile);
outputSource = new FileInputStreamSource(outputFile);
listener.testLog(localFileName, LogDataType.TEXT, outputSource);
}
} finally {
FileUtil.deleteFile(outputFile);
StreamUtil.cancel(outputSource);
}
}
/** {@inheritDoc} */
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
mFioDir = new File(mTestDevice.getMountPoint(IDevice.MNT_DATA), "fio").getAbsolutePath();
if (mFioLocation != null) {
mFioBin = new File(mFioDir, "fio").getAbsolutePath();
} else {
mFioBin = "fio";
}
mFioConfig = new File(mFioDir, "config.fio").getAbsolutePath();
setupTests();
setupDevice();
for (TestInfo test : mTestCases) {
runTest(test, listener);
}
cleanupDevice();
}
/** {@inheritDoc} */
@Override
public void setDevice(ITestDevice device) {
mTestDevice = device;
}
/** {@inheritDoc} */
@Override
public ITestDevice getDevice() {
return mTestDevice;
}
/** A meta-test to ensure that the bits of FioBenchmarkTest are working properly. */
public static class MetaTest extends TestCase {
/** Test that {@link JobInfo#createJob()} properly formats a job. */
public void testCreateJob() {
JobInfo j = new JobInfo();
assertEquals("", j.createJob());
j.mJobName = "job";
assertEquals("[job]\n", j.createJob());
j.mParameters.put("param1", null);
j.mParameters.put("param2", "value");
String[] lines = j.createJob().split("\n");
assertEquals(3, lines.length);
assertEquals("[job]", lines[0]);
Set<String> params = new HashSet<>(2);
params.add(lines[1]);
params.add(lines[2]);
assertTrue(params.contains("param1"));
assertTrue(params.contains("param2=value"));
}
/** Test that {@link TestInfo#createConfig()} properly formats a config. */
public void testCreateConfig() {
TestInfo t = new TestInfo();
JobInfo j = new JobInfo();
j.mJobName = "job1";
j.mParameters.put("param1", "value1");
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = "job2";
j.mParameters.put("param2", "value2");
t.mJobs.add(j);
j = new JobInfo();
j.mJobName = "job3";
j.mParameters.put("param3", "value3");
t.mJobs.add(j);
assertEquals(
"[job1]\nparam1=value1\n\n"
+ "[job2]\nparam2=value2\n\n"
+ "[job3]\nparam3=value3\n\n",
t.createConfig());
}
/**
* Test that output lines are parsed correctly by the FioParser, invalid lines are ignored,
* and that the various fields are accessible with {@link FioParser#getResult(String,
* String)}.
*/
public void testFioParser() {
String[] lines = new String[4];
// We build the lines up as follows (assuming FIO_RESULTS_FIELDS.length == 58):
// 0;3;6;...;171
// 1;4;7;...;172
// 2;5;8;...;173
for (int i = 0; i < 3; i++) {
StringBuilder sb = new StringBuilder();
sb.append(i);
for (int j = 1; j < FIO_V0_RESULT_FIELDS.length; j++) {
sb.append(";");
sb.append(j * 3 + i);
}
lines[i] = sb.toString();
}
// A line may have an optional description on the end which we don't care about, make
// sure it still parses.
lines[2] = lines[2] += ";description";
// Make sure an invalid output line does not parse.
lines[3] = "invalid";
FioParser p = new FioParser();
p.processNewLines(lines);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < FIO_V0_RESULT_FIELDS.length; j++) {
assertEquals(
String.format("job=%d, field=%s", i, FIO_V0_RESULT_FIELDS[j]),
String.format("%d", j * 3 + i),
p.getResult(String.format("%d", i), FIO_V0_RESULT_FIELDS[j]));
}
}
assertNull(p.getResult("missing", "jobname"));
assertNull(p.getResult("invalid", "jobname"));
assertNull(p.getResult("0", "missing"));
}
/**
* Test that {@link PerfMetricInfo.ResultType#value(String)} correctly transforms strings
* based on the result type.
*/
public void testResultTypeValue() {
assertEquals("test", PerfMetricInfo.ResultType.STRING.value("test"));
assertEquals("1", PerfMetricInfo.ResultType.INT.value("1"));
assertEquals("3.14159", PerfMetricInfo.ResultType.FLOAT.value("3.14159"));
assertEquals(
String.format("%f", 0.34567),
PerfMetricInfo.ResultType.PERCENT.value("34.567%"));
assertNull(PerfMetricInfo.ResultType.PERCENT.value(""));
assertNull(PerfMetricInfo.ResultType.PERCENT.value("34.567"));
assertNull(PerfMetricInfo.ResultType.PERCENT.value("test%"));
}
}
}