Home | History | Annotate | Download | only in tests
      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.ddmlib.MultiLineReceiver;
     19 import com.android.tradefed.config.Option;
     20 import com.android.tradefed.config.Option.Importance;
     21 import com.android.tradefed.device.DeviceNotAvailableException;
     22 import com.android.tradefed.device.ITestDevice;
     23 import com.android.tradefed.log.LogUtil.CLog;
     24 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
     25 import com.android.tradefed.result.ITestInvocationListener;
     26 import com.android.tradefed.result.TestDescription;
     27 import com.android.tradefed.testtype.IDeviceTest;
     28 import com.android.tradefed.testtype.IRemoteTest;
     29 import com.android.tradefed.util.AbiFormatter;
     30 import com.android.tradefed.util.SimplePerfStatResultParser;
     31 import com.android.tradefed.util.SimplePerfUtil;
     32 import com.android.tradefed.util.SimplePerfUtil.SimplePerfType;
     33 import com.android.tradefed.util.SimpleStats;
     34 import com.android.tradefed.util.proto.TfMetricProtoUtil;
     35 
     36 import org.junit.Assert;
     37 
     38 import java.text.NumberFormat;
     39 import java.text.ParseException;
     40 import java.util.ArrayList;
     41 import java.util.HashMap;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.Map;
     45 import java.util.concurrent.TimeUnit;
     46 import java.util.regex.Matcher;
     47 import java.util.regex.Pattern;
     48 
     49 /**
     50  * Test which runs various micro_bench tests and reports the results.
     51  * <p>
     52  * Tests include:
     53  * </p><ul>
     54  * <li>{@code micro_bench sleep 1 ITERATIONS}</li>
     55  * <li>{@code micro_bench memset ARG ITERATIONS} where ARG is defined by {@code memset-small}</li>
     56  * <li>{@code micro_bench memset ARG ITERATIONS} where ARG is defined by {@code memset-large}</li>
     57  * <li>{@code micro_bench memcpy ARG ITERATIONS} where ARG is defined by {@code memcpy-small}</li>
     58  * <li>{@code micro_bench memcpy ARG ITERATIONS} where ARG is defined by {@code memcpy-large}</li>
     59  * <li>{@code micro_bench memread ARG ITERATIONS} where ARG is defined by {@code memread}</li>
     60  * </ul>
     61  */
     62 public class MicroBenchTest implements IDeviceTest, IRemoteTest {
     63     private final static String RUN_KEY = "micro_bench";
     64 
     65     private final static Pattern SLEEP_PATTERN = Pattern.compile(
     66             "sleep\\(\\d+\\) took (\\d+.\\d+) seconds");
     67     private final static Pattern MEMSET_PATTERN = Pattern.compile(
     68             "memset \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)");
     69     private final static Pattern MEMCPY_PATTERN = Pattern.compile(
     70             "memcpy \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)");
     71     private final static Pattern MEMREAD_PATTERN = Pattern.compile(
     72             "memread \\d+x\\d+ bytes took (\\d+.\\d+) seconds \\((\\d+.\\d+) MB/s\\)");
     73 
     74     private final static String BINARY_NAME = "micro_bench|#ABI32#|";
     75     private final static String SLEEP_CMD = "%s sleep 1 %d";
     76     // memset, memcpy, and memread pass in another argument befor passing the cmd format string to
     77     // TestCase, so escape the command and iteration formats.
     78     private final static String MEMSET_CMD = "%%s memset %d %%d";
     79     private final static String MEMCPY_CMD = "%%s memcpy %d %%d";
     80     private final static String MEMREAD_CMD = "%%s memread %d %%d";
     81 
     82     @Option(name = AbiFormatter.FORCE_ABI_STRING,
     83             description = AbiFormatter.FORCE_ABI_DESCRIPTION,
     84             importance = Importance.IF_UNSET)
     85     private String mForceAbi = null;
     86 
     87     @Option(name = "sleep-iterations")
     88     private Integer mSleepIterations = 10;
     89 
     90     @Option(name = "memset-iterations")
     91     private Integer mMemsetIterations = 100;
     92 
     93     @Option(name = "memset-small")
     94     private Integer mMemsetSmall = 8 * 1024; // 8 KB
     95 
     96     @Option(name = "memset-large")
     97     private Integer mMemsetLarge = 2 * 1024 * 1024; // 2 MB
     98 
     99     @Option(name = "memcpy-iterations")
    100     private Integer mMemcpyIterations = 100;
    101 
    102     @Option(name = "memcpy-small")
    103     private Integer mMemcpySmall = 8 * 1024; // 8 KB
    104 
    105     @Option(name = "memcpy-large")
    106     private Integer mMemcpyLarge = 2 * 1024 * 1024; // 2 MB
    107 
    108     @Option(name = "memread-iterations")
    109     private Integer mMemreadIterations = 100;
    110 
    111     @Option(name = "memread")
    112     private Integer mMemreadArg = 8 * 1024; // 8 KB
    113 
    114     @Option(name = "stop-framework", description = "Stop the framework before running tests")
    115     private boolean mStopFramework = true;
    116 
    117     @Option(name = "profile-mode", description =
    118             "In profile mode, microbench runs memset, memcpy, and "
    119                     + "memread from 32 bytes to 8 MB, doubling the size each time.")
    120     private boolean mProfileMode = false;
    121 
    122     @Option(name = "simpleperf-mode",
    123             description = "Whether use simpleperf to get low level metrics")
    124     private static boolean mSimpleperfMode = false;
    125 
    126     @Option(name = "simpleperf-argu", description = "simpleperf arguments")
    127     private List<String> mSimpleperfArgu = new ArrayList<String>();
    128 
    129     @Option(
    130         name = "simpleperf-cmd-timeout",
    131         description = "Timeout (in seconds) while running simpleperf shell command."
    132     )
    133     private int mSimpleperfCmdTimeout = 240;
    134 
    135     String mFormattedBinaryName = null;
    136     ITestDevice mTestDevice = null;
    137     SimplePerfUtil mSpUtil = null;
    138 
    139     /**
    140      * Class used to read and parse micro_bench output.
    141      */
    142     private static class MicroBenchReceiver extends MultiLineReceiver {
    143         private Pattern mPattern;
    144         private int mTimeGroup = -1;
    145         private int mPerfGroup = -1;
    146         private SimpleStats mTimeStats = null;
    147         private SimpleStats mPerfStats = null;
    148         private Map<String, Double> mSimpleperfMetricsMap = new HashMap<String, Double>();
    149 
    150         private boolean mSimpleperfOutput;
    151 
    152         /**
    153          * Create a receiver for parsing micro_bench output.
    154          *
    155          * @param pattern The regex {@link Pattern} to parse each line of output. Time and/or
    156          * performance numbers should be their own group.
    157          * @param timeGroup the index of the time data in the pattern.
    158          * @param perfGroup the index of the performance data in the pattern.
    159          */
    160         public MicroBenchReceiver(Pattern pattern, int timeGroup, int perfGroup) {
    161             mPattern = pattern;
    162             mTimeGroup = timeGroup;
    163             if (mTimeGroup > 0) {
    164                 mTimeStats = new SimpleStats();
    165             }
    166             mPerfGroup = perfGroup;
    167             if (mPerfGroup > 0) {
    168                 mPerfStats = new SimpleStats();
    169             }
    170             mSimpleperfOutput = false;
    171         }
    172 
    173         /**
    174          * {@inheritDoc}
    175          */
    176         @Override
    177         public void processNewLines(String[] lines) {
    178             for (String line : lines) {
    179                 if (!mSimpleperfOutput || !mSimpleperfMode) {
    180                     if (line.contains("Performance counter statistics:")) {
    181                         mSimpleperfOutput = true;
    182                         continue;
    183                     }
    184                     CLog.v(line);
    185                     Matcher m = mPattern.matcher(line);
    186                     if (m.matches()) {
    187                         if (mTimeStats != null) {
    188                             mTimeStats.add(
    189                                     Double.valueOf(m.group(mTimeGroup)) * 1000d); // secs to ms
    190                         }
    191                         if (mPerfStats != null) {
    192                             mPerfStats.add(Double.valueOf(m.group(mPerfGroup)));
    193                         }
    194                     }
    195                 } else {
    196                     List<String> spResult = SimplePerfStatResultParser.parseSingleLine(line);
    197                     if (spResult.size() == 1) {
    198                         CLog.d(String.format("Total run time: %s", spResult.get(0)));
    199                     } else if (spResult.size() == 3) {
    200                         try {
    201                             Double metricValue = NumberFormat.getNumberInstance(Locale.US)
    202                                     .parse(spResult.get(0)).doubleValue();
    203                             mSimpleperfMetricsMap.put(spResult.get(1), metricValue);
    204                         } catch (ParseException e) {
    205                             CLog.e("Simpleperf metrics parse failure: " + e.toString());
    206                         }
    207                     } else {
    208                         CLog.d("Skipping line: " + line);
    209                     }
    210                 }
    211             }
    212         }
    213 
    214         /**
    215          * Get the time stats.
    216          *
    217          * @return a {@link SimpleStats} object containing the time stats in ms.
    218          */
    219         public SimpleStats getTimeStats() {
    220             return mTimeStats;
    221         }
    222 
    223         /**
    224          * Get the performance stats.
    225          *
    226          * @return a {@link SimpleStats} object containing the performance stats in MB/s.
    227          */
    228         public SimpleStats getPerfStats() {
    229             return mPerfStats;
    230         }
    231 
    232         public Map<String, Double> getSimpleperfMetricsMap() {
    233             return mSimpleperfMetricsMap;
    234         }
    235 
    236         /**
    237          * {@inheritDoc}
    238          */
    239         @Override
    240         public boolean isCancelled() {
    241             return false;
    242         }
    243     }
    244 
    245     /**
    246      * Class used to hold test data and run individual tests.
    247      */
    248     private class TestCase {
    249         private String mTestName;
    250         private String mTestKey;
    251         private String mCommand;
    252         private int mIterations = 0;
    253         private MicroBenchReceiver mReceiver;
    254 
    255         /**
    256          * Create a test case.
    257          *
    258          * @param testName The name of the test
    259          * @param testKey The key of the test used to report metrics.
    260          * @param command The command to run. Must contain a {@code %d} in the place of the number
    261          * of iterations.
    262          * @param iterations The amount of iterations to run.
    263          * @param pattern The regex {@link Pattern} to parse each line of output. Time and/or
    264          * performance numbers should be their own group.
    265          * @param timeGroup the index of the time data in the pattern.
    266          * @param perfGroup the index of the performance data in the pattern.
    267          */
    268         public TestCase(String testName, String testKey, String command, int iterations,
    269                 Pattern pattern, int timeGroup, int perfGroup) {
    270             mTestName = testName;
    271             mTestKey = testKey;
    272             mCommand = command;
    273             mIterations = iterations;
    274             mReceiver = new MicroBenchReceiver(pattern, timeGroup, perfGroup);
    275         }
    276 
    277         /**
    278          * Runs the test and adds the results to the test metrics.
    279          *
    280          * @param listener the {@link ITestInvocationListener}.
    281          * @param metrics the metrics map for the results.
    282          * @throws DeviceNotAvailableException if the device is not available while running the
    283          * test.
    284          */
    285         public void runTest(ITestInvocationListener listener, Map<String, String> metrics)
    286                 throws DeviceNotAvailableException {
    287             if (mIterations == 0) {
    288                 return;
    289             }
    290 
    291             CLog.i("Running %s test", mTestName);
    292 
    293             TestDescription testId = new TestDescription(getClass().getCanonicalName(), mTestKey);
    294             listener.testStarted(testId);
    295             final String cmd = String.format(mCommand, mFormattedBinaryName, mIterations);
    296 
    297             if (mSimpleperfMode) {
    298                 mSpUtil.executeCommand(cmd, mReceiver, mSimpleperfCmdTimeout, TimeUnit.SECONDS, 1);
    299             } else {
    300                 mTestDevice.executeShellCommand(cmd, mReceiver);
    301             }
    302 
    303             Boolean timePass = parseStats(mReceiver.getTimeStats(), "time", metrics);
    304             Boolean perfPass = parseStats(mReceiver.getPerfStats(), "perf", metrics);
    305 
    306             for (Map.Entry<String, Double> entry :
    307                     mReceiver.getSimpleperfMetricsMap().entrySet()) {
    308                 metrics.put(String.format("%s_%s", mTestKey, entry.getKey()),
    309                         Double.toString(entry.getValue() / mIterations));
    310             }
    311 
    312             if (!timePass || !perfPass) {
    313                 listener.testFailed(testId,
    314                         "Iteration count mismatch (see host log).");
    315             }
    316             listener.testEnded(testId, new HashMap<String, Metric>());
    317         }
    318 
    319         /**
    320          * Parse the stats from {@link MicroBenchReceiver} and add them to the metrics
    321          *
    322          * @param stats {@link SimpleStats} from {@link MicroBenchReceiver} or {@code null} if
    323          * there are no stats.
    324          * @param category the category of the stats, either {@code perf} or {@code time}.
    325          * @param metrics the metrics map to add the parsed stats to.
    326          * @return {@code true} if the stats were successfully parsed and added or if
    327          * {@code stats == null}, {@code false} if there was an error.
    328          */
    329         private boolean parseStats(SimpleStats stats, String category,
    330                 Map<String, String> metrics) {
    331             if (stats == null) {
    332                 return true;
    333             }
    334 
    335             String unit = "perf".equals(category) ? "MB/s" : "ms";
    336 
    337             CLog.i("%s %s results: size = %d, mean = %f %s, stdev = %f %s", mTestName, category,
    338                     stats.size(), stats.mean(), unit, stats.stdev(), unit);
    339 
    340             if (stats.mean() == null) {
    341                 CLog.e("Expected non null mean for %s %s", mTestName, category);
    342                 return false;
    343             }
    344 
    345             if (stats.size() != mIterations) {
    346                 CLog.e("Expected iterations (%d) not equal to sample size (%d) for %s %s",
    347                         mIterations, stats.size(), mTestName, category);
    348                 return false;
    349             }
    350 
    351             if (metrics != null) {
    352                 metrics.put(String.format("%s_%s", mTestKey, category),
    353                         Double.toString(stats.mean()));
    354             }
    355             return true;
    356         }
    357     }
    358 
    359     /**
    360      * {@inheritDoc}
    361      */
    362     @Override
    363     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    364         String name, cmd;
    365         List<TestCase> testCases = new ArrayList<TestCase>();
    366 
    367         mFormattedBinaryName = AbiFormatter.formatCmdForAbi(BINARY_NAME, mForceAbi);
    368 
    369         Assert.assertNotNull(mTestDevice);
    370         // TODO: Replace this with a more generic way to check for the presence of a binary
    371         final String output = mTestDevice.executeShellCommand(mFormattedBinaryName);
    372         Assert.assertFalse(output.contains(mFormattedBinaryName + ": not found"));
    373 
    374         if (mSimpleperfMode) {
    375             mSpUtil = SimplePerfUtil.newInstance(mTestDevice, SimplePerfType.STAT);
    376             if (mSimpleperfArgu.size() == 0) {
    377                 mSimpleperfArgu.add("-e cpu-cycles:k,cpu-cycles:u");
    378             }
    379             mSpUtil.setArgumentList(mSimpleperfArgu);
    380         }
    381 
    382         if (mStopFramework) {
    383             mTestDevice.executeShellCommand("stop");
    384         }
    385 
    386         if (mProfileMode) {
    387             for (int i = 1 << 5; i < 1 << 24; i <<= 1) {
    388                 name = String.format("memset %d", i);
    389                 cmd = String.format(MEMSET_CMD, i);
    390                 testCases.add(new TestCase(name, String.format("memset_%d", i), cmd,
    391                         mMemsetIterations, MEMSET_PATTERN, 1, 2));
    392             }
    393 
    394             for (int i = 1 << 5; i < 1 << 24; i <<= 1) {
    395                 name = String.format("memcpy %d", i);
    396                 cmd = String.format(MEMCPY_CMD, i);
    397                 testCases.add(new TestCase(name, String.format("memcpy_%d", i), cmd,
    398                         mMemcpyIterations, MEMCPY_PATTERN, 1, 2));
    399             }
    400 
    401             for (int i = 1 << 5; i < 1 << 24; i <<= 1) {
    402                 name = String.format("memread %d", i);
    403                 cmd = String.format(MEMREAD_CMD, i);
    404                 testCases.add(new TestCase(name, String.format("memread_%d", i), cmd,
    405                         mMemreadIterations, MEMREAD_PATTERN, 1, 2));
    406             }
    407 
    408             // Don't want to report test metrics in profile mode
    409             for (TestCase test : testCases) {
    410                 test.runTest(listener, null);
    411             }
    412         } else {
    413             // Sleep test case
    414             testCases.add(new TestCase("sleep", "sleep", SLEEP_CMD, mSleepIterations, SLEEP_PATTERN,
    415                     1, -1));
    416 
    417             // memset small test case
    418             name = String.format("memset %d", mMemsetSmall);
    419             cmd = String.format(MEMSET_CMD, mMemsetSmall);
    420             testCases.add(new TestCase(name, "memset_small", cmd, mMemsetIterations, MEMSET_PATTERN,
    421                     1, 2));
    422 
    423             // memset large test case
    424             name = String.format("memset %d", mMemsetLarge);
    425             cmd = String.format(MEMSET_CMD, mMemsetLarge);
    426             testCases.add(new TestCase(name, "memset_large", cmd, mMemsetIterations, MEMSET_PATTERN,
    427                     1, 2));
    428 
    429             // memcpy small test case
    430             name = String.format("memcpy %d", mMemcpySmall);
    431             cmd = String.format(MEMCPY_CMD, mMemcpySmall);
    432             testCases.add(new TestCase(name, "memcpy_small", cmd, mMemcpyIterations, MEMCPY_PATTERN,
    433                     1, 2));
    434 
    435             // memcpy large test case
    436             name = String.format("memcpy %d", mMemcpyLarge);
    437             cmd = String.format(MEMCPY_CMD, mMemcpyLarge);
    438             testCases.add(new TestCase(name, "memcpy_large", cmd, mMemcpyIterations, MEMCPY_PATTERN,
    439                     1, 2));
    440 
    441             // memread test case
    442             name = String.format("memread %d", mMemreadArg);
    443             cmd = String.format(MEMREAD_CMD, mMemreadArg);
    444             testCases.add(new TestCase(name, "memread", cmd, mMemreadIterations, MEMREAD_PATTERN,
    445                     1, 2));
    446 
    447             listener.testRunStarted(RUN_KEY, testCases.size());
    448             long beginTime = System.currentTimeMillis();
    449             Map<String, String> metrics = new HashMap<String, String>();
    450             for (TestCase test : testCases) {
    451                 test.runTest(listener, metrics);
    452             }
    453             listener.testRunEnded(
    454                     (System.currentTimeMillis() - beginTime),
    455                     TfMetricProtoUtil.upgradeConvert(metrics));
    456         }
    457 
    458         if (mStopFramework) {
    459             mTestDevice.executeShellCommand("start");
    460         }
    461     }
    462 
    463     /**
    464      * {@inheritDoc}
    465      */
    466     @Override
    467     public void setDevice(ITestDevice device) {
    468         mTestDevice = device;
    469     }
    470 
    471     /**
    472      * {@inheritDoc}
    473      */
    474     @Override
    475     public ITestDevice getDevice() {
    476         return mTestDevice;
    477     }
    478 }
    479