blob: dc81b9cb262a15f1d404af972485a3063edee40a [file] [log] [blame]
/*
* Copyright (C) 2016 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.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.AbiFormatter;
import com.android.tradefed.util.SimplePerfResult;
import com.android.tradefed.util.SimplePerfUtil;
import com.android.tradefed.util.SimplePerfUtil.SimplePerfType;
import com.android.tradefed.util.SimpleStats;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import org.junit.Assert;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** This test is targeting eMMC performance on read/ write. */
public class EmmcPerformanceTest implements IDeviceTest, IRemoteTest {
private enum TestType {
DD,
RANDOM;
}
private static final String RUN_KEY = "emmc_performance_tests";
private static final String SEQUENTIAL_READ_KEY = "sequential_read";
private static final String SEQUENTIAL_WRITE_KEY = "sequential_write";
private static final String RANDOM_READ_KEY = "random_read";
private static final String RANDOM_WRITE_KEY = "random_write";
private static final String PERF_RANDOM = "/data/local/tmp/rand_emmc_perf|#ABI32#|";
private static final Pattern DD_PATTERN =
Pattern.compile("\\d+ bytes transferred in \\d+\\.\\d+ secs \\((\\d+) bytes/sec\\)");
private static final Pattern EMMC_RANDOM_PATTERN =
Pattern.compile("(\\d+) (\\d+)byte iops/sec");
private static final int BLOCK_SIZE = 1048576;
private static final int SEQ_COUNT = 200;
@Option(name = "cpufreq", description = "The path to the cpufreq directory on the DUT.")
private String mCpufreq = "/sys/devices/system/cpu/cpu0/cpufreq";
@Option(
name = "auto-discover-cache-info",
description =
"Indicate if test should attempt auto discover cache path and partition size "
+ "from the test device. Default to be false, ie. manually set "
+ "cache-device and cache-partition-size, or use default."
+ " If fail to discover, it will fallback to what is set in "
+ "cache-device")
private boolean mAutoDiscoverCacheInfo = false;
@Option(
name = "cache-device",
description =
"The path to the cache block device on the DUT."
+ " Nakasi: /dev/block/platform/sdhci-tegra.3/by-name/CAC\n"
+ " Prime: /dev/block/platform/omap/omap_hsmmc.0/by-name/cache\n"
+ " Stingray: /dev/block/platform/sdhci-tegra.3/by-name/cache\n"
+ " Crespo: /dev/block/platform/s3c-sdhci.0/by-name/userdata\n",
importance = Importance.IF_UNSET)
private String mCache = null;
@Option(name = "iterations", description = "The number of iterations to run")
private int mIterations = 100;
@Option(
name = AbiFormatter.FORCE_ABI_STRING,
description = AbiFormatter.FORCE_ABI_DESCRIPTION,
importance = Importance.IF_UNSET)
private String mForceAbi = null;
@Option(name = "cache-partition-size", description = "Cache partiton size in MB")
private static int mCachePartitionSize = 100;
@Option(
name = "simpleperf-mode",
description = "Whether use simpleperf to get low level metrics")
private boolean mSimpleperfMode = false;
@Option(name = "simpleperf-argu", description = "simpleperf arguments")
private List<String> mSimpleperfArgu = new ArrayList<>();
ITestDevice mTestDevice = null;
SimplePerfUtil mSpUtil = null;
/** {@inheritDoc} */
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
try {
setUp();
listener.testRunStarted(RUN_KEY, 5);
long beginTime = System.currentTimeMillis();
Map<String, String> metrics = new HashMap<>();
runSequentialRead(mIterations, listener, metrics);
runSequentialWrite(mIterations, listener, metrics);
// FIXME: Figure out cache issues with random read and reenable test.
// runRandomRead(mIterations, listener, metrics);
// runRandomWrite(mIterations, listener, metrics);
CLog.d("Metrics: %s", metrics.toString());
listener.testRunEnded(
(System.currentTimeMillis() - beginTime),
TfMetricProtoUtil.upgradeConvert(metrics));
} finally {
cleanUp();
}
}
/** Run the sequential read test. */
private void runSequentialRead(
int iterations, ITestInvocationListener listener, Map<String, String> metrics)
throws DeviceNotAvailableException {
String command =
String.format(
"dd if=%s of=/dev/null bs=%d count=%d", mCache, BLOCK_SIZE, SEQ_COUNT);
runTest(SEQUENTIAL_READ_KEY, command, TestType.DD, true, iterations, listener, metrics);
}
/** Run the sequential write test. */
private void runSequentialWrite(
int iterations, ITestInvocationListener listener, Map<String, String> metrics)
throws DeviceNotAvailableException {
String command =
String.format(
"dd if=/dev/zero of=%s bs=%d count=%d", mCache, BLOCK_SIZE, SEQ_COUNT);
runTest(SEQUENTIAL_WRITE_KEY, command, TestType.DD, false, iterations, listener, metrics);
}
/** Run the random read test. */
@SuppressWarnings("unused")
private void runRandomRead(
int iterations, ITestInvocationListener listener, Map<String, String> metrics)
throws DeviceNotAvailableException {
String command =
String.format(
"%s -r %d %s",
AbiFormatter.formatCmdForAbi(PERF_RANDOM, mForceAbi),
mCachePartitionSize,
mCache);
runTest(RANDOM_READ_KEY, command, TestType.RANDOM, true, iterations, listener, metrics);
}
/** Run the random write test with OSYNC disabled. */
private void runRandomWrite(
int iterations, ITestInvocationListener listener, Map<String, String> metrics)
throws DeviceNotAvailableException {
String command =
String.format(
"%s -w %d %s",
AbiFormatter.formatCmdForAbi(PERF_RANDOM, mForceAbi),
mCachePartitionSize,
mCache);
runTest(RANDOM_WRITE_KEY, command, TestType.RANDOM, false, iterations, listener, metrics);
}
/**
* Run a test for a number of iterations.
*
* @param testKey the key used to report metrics.
* @param command the command to be run on the device.
* @param type the {@link TestType}, which determines how each iteration should be run.
* @param dropCache whether to drop the cache before starting each iteration.
* @param iterations the number of iterations to run.
* @param listener the {@link ITestInvocationListener}.
* @param metrics the map to store metrics of.
* @throws DeviceNotAvailableException If the device was not available.
*/
private void runTest(
String testKey,
String command,
TestType type,
boolean dropCache,
int iterations,
ITestInvocationListener listener,
Map<String, String> metrics)
throws DeviceNotAvailableException {
CLog.i("Starting test %s", testKey);
TestDescription id = new TestDescription(RUN_KEY, testKey);
listener.testStarted(id);
Map<String, SimpleStats> simpleperfMetricsMap = new HashMap<>();
SimpleStats stats = new SimpleStats();
for (int i = 0; i < iterations; i++) {
if (dropCache) {
dropCache();
}
Double kbps = null;
switch (type) {
case DD:
kbps = runDdIteration(command, simpleperfMetricsMap);
break;
case RANDOM:
kbps = runRandomIteration(command, simpleperfMetricsMap);
break;
}
if (kbps != null) {
CLog.i("Result for %s, iteration %d: %f KBps", testKey, i + 1, kbps);
stats.add(kbps);
} else {
CLog.w("Skipping %s, iteration %d", testKey, i + 1);
}
}
if (stats.mean() != null) {
metrics.put(testKey, Double.toString(stats.median()));
for (Map.Entry<String, SimpleStats> entry : simpleperfMetricsMap.entrySet()) {
metrics.put(
String.format("%s_%s", testKey, entry.getKey()),
Double.toString(entry.getValue().median()));
}
} else {
listener.testFailed(id, "No metrics to report (see log)");
}
CLog.i(
"Test %s finished: mean=%f, stdev=%f, samples=%d",
testKey, stats.mean(), stats.stdev(), stats.size());
listener.testEnded(id, new HashMap<String, Metric>());
}
/**
* Run a single iteration of the dd (sequential) test.
*
* @param command the command to run on the device.
* @param simpleperfMetricsMap the map contain simpleperf metrics aggregated results
* @return The speed of the test in KBps or null if there was an error running or parsing the
* test.
* @throws DeviceNotAvailableException If the device was not available.
*/
private Double runDdIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap)
throws DeviceNotAvailableException {
String[] output;
SimplePerfResult spResult = null;
if (mSimpleperfMode) {
spResult = mSpUtil.executeCommand(command);
output = spResult.getCommandRawOutput().split("\n");
} else {
output = mTestDevice.executeShellCommand(command).split("\n");
}
String line = output[output.length - 1].trim();
Matcher m = DD_PATTERN.matcher(line);
if (m.matches()) {
simpleperfResultAggregation(spResult, simpleperfMetricsMap);
return convertBpsToKBps(Double.parseDouble(m.group(1)));
} else {
CLog.w("Line \"%s\" did not match expected output, ignoring", line);
return null;
}
}
/**
* Run a single iteration of the random test.
*
* @param command the command to run on the device.
* @param simpleperfMetricsMap the map contain simpleperf metrics aggregated results
* @return The speed of the test in KBps or null if there was an error running or parsing the
* test.
* @throws DeviceNotAvailableException If the device was not available.
*/
private Double runRandomIteration(String command, Map<String, SimpleStats> simpleperfMetricsMap)
throws DeviceNotAvailableException {
String output;
SimplePerfResult spResult = null;
if (mSimpleperfMode) {
spResult = mSpUtil.executeCommand(command);
output = spResult.getCommandRawOutput();
} else {
output = mTestDevice.executeShellCommand(command);
}
Matcher m = EMMC_RANDOM_PATTERN.matcher(output.trim());
if (m.matches()) {
simpleperfResultAggregation(spResult, simpleperfMetricsMap);
return convertIopsToKBps(Double.parseDouble(m.group(1)));
} else {
CLog.w("Line \"%s\" did not match expected output, ignoring", output);
return null;
}
}
/**
* Helper function to aggregate simpleperf results
*
* @param spResult object that holds simpleperf results
* @param simpleperfMetricsMap map holds aggregated simpleperf results
*/
private void simpleperfResultAggregation(
SimplePerfResult spResult, Map<String, SimpleStats> simpleperfMetricsMap) {
if (mSimpleperfMode) {
Assert.assertNotNull("simpleperf result is null object", spResult);
for (Map.Entry<String, String> entry : spResult.getBenchmarkMetrics().entrySet()) {
try {
Double metricValue =
NumberFormat.getNumberInstance(Locale.US)
.parse(entry.getValue())
.doubleValue();
if (!simpleperfMetricsMap.containsKey(entry.getKey())) {
SimpleStats newStat = new SimpleStats();
simpleperfMetricsMap.put(entry.getKey(), newStat);
}
simpleperfMetricsMap.get(entry.getKey()).add(metricValue);
} catch (ParseException e) {
CLog.e("Simpleperf metrics parse failure: " + e.toString());
}
}
}
}
/** Drop the disk cache on the device. */
private void dropCache() throws DeviceNotAvailableException {
mTestDevice.executeShellCommand("echo 3 > /proc/sys/vm/drop_caches");
}
/** Convert bytes / sec reported by the dd tests into KBps. */
private double convertBpsToKBps(double bps) {
return bps / 1024;
}
/**
* Convert the iops reported by the random tests into KBps.
*
* <p>The iops is number of 4kB block reads/writes per sec. This makes the conversion factor 4.
*/
private double convertIopsToKBps(double iops) {
return 4 * iops;
}
/** Setup the device for tests by unmounting partitions and maxing the cpu speed. */
private void setUp() throws DeviceNotAvailableException {
if (mAutoDiscoverCacheInfo) {
discoverCacheInfo();
}
mTestDevice.executeShellCommand("umount /sdcard");
mTestDevice.executeShellCommand("umount /data");
mTestDevice.executeShellCommand("umount /cache");
mTestDevice.executeShellCommand(
String.format("cat %s/cpuinfo_max_freq > %s/scaling_max_freq", mCpufreq, mCpufreq));
mTestDevice.executeShellCommand(
String.format("cat %s/cpuinfo_max_freq > %s/scaling_min_freq", mCpufreq, mCpufreq));
if (mSimpleperfMode) {
mSpUtil = SimplePerfUtil.newInstance(mTestDevice, SimplePerfType.STAT);
if (mSimpleperfArgu.size() == 0) {
mSimpleperfArgu.add("-e cpu-cycles:k,cpu-cycles:u");
}
mSpUtil.setArgumentList(mSimpleperfArgu);
}
}
/** Attempt to detect cache path and cache partition size automatically */
private void discoverCacheInfo() throws DeviceNotAvailableException {
// Expected output look similar to the following:
//
// > ... vdc dump | grep cache
// 0 4123 /dev/block/platform/soc/7824900.sdhci/by-name/cache /cache ext4 rw, \
// seclabel,nosuid,nodev,noatime,discard,data=ordered 0 0
if (mTestDevice.enableAdbRoot()) {
String output = mTestDevice.executeShellCommand("vdc dump | grep cache");
CLog.d("Output from shell command 'vdc dump | grep cache':\n%s", output);
String[] segments = output.split("\\s+");
if (segments.length >= 3) {
mCache = segments[2];
} else {
CLog.w("Fail to detect cache path. Fall back to use '%s'", mCache);
}
} else {
CLog.d(
"Cannot get cache path because device %s is not rooted.",
mTestDevice.getSerialNumber());
}
// Expected output looks similar to the following:
//
// > ... df cache
// Filesystem 1K-blocks Used Available Use% Mounted on
// /dev/block/mmcblk0p34 60400 56 60344 1% /cache
String output = mTestDevice.executeShellCommand("df cache");
CLog.d(String.format("Output from shell command 'df cache':\n%s", output));
String[] lines = output.split("\r?\n");
if (lines.length >= 2) {
String[] segments = lines[1].split("\\s+");
if (segments.length >= 2) {
if (lines[0].toLowerCase().contains("1k-blocks")) {
mCachePartitionSize = Integer.parseInt(segments[1]) / 1024;
} else {
throw new IllegalArgumentException("Unknown unit for the cache size.");
}
}
}
CLog.d("cache-device is set to %s ...", mCache);
CLog.d("cache-partition-size is set to %d ...", mCachePartitionSize);
}
/** Clean up the device by formatting a new cache partition. */
private void cleanUp() throws DeviceNotAvailableException {
mTestDevice.executeShellCommand(String.format("mke2fs %s", mCache));
}
/** {@inheritDoc} */
@Override
public void setDevice(ITestDevice device) {
mTestDevice = device;
}
/** {@inheritDoc} */
@Override
public ITestDevice getDevice() {
return mTestDevice;
}
}