• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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