普通文本  |  490行  |  16.76 KB

#!/usr/bin/env python2
#
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# pylint: disable=cros-logging-import

# This is the script to run specified benchmark with different toolchain
# settings. It includes the process of building benchmark locally and running
# benchmark on DUT.

"""Main script to run the benchmark suite from building to testing."""
from __future__ import print_function

import argparse
import config
import ConfigParser
import logging
import os
import subprocess
import sys

logging.basicConfig(level=logging.INFO)

def _parse_arguments(argv):
    parser = argparse.ArgumentParser(description='Build and run specific '
                                     'benchamrk')
    parser.add_argument(
        '-b',
        '--bench',
        action='append',
        default=[],
        help='Select which benchmark to run')

    # Only one of compiler directory and llvm prebuilts version can be indicated
    # at the beginning, so set -c and -l into a exclusive group.
    group = parser.add_mutually_exclusive_group()

    # The toolchain setting arguments has action of 'append', so that users
    # could compare performance with several toolchain settings together.
    group.add_argument(
        '-c',
        '--compiler_dir',
        metavar='DIR',
        action='append',
        default=[],
        help='Specify path to the compiler\'s bin directory. '
        'You shall give several paths, each with a -c, to '
        'compare performance differences in '
        'each compiler.')

    parser.add_argument(
        '-o',
        '--build_os',
        action='append',
        default=[],
        help='Specify the host OS to build the benchmark.')

    group.add_argument(
        '-l',
        '--llvm_prebuilts_version',
        action='append',
        default=[],
        help='Specify the version of prebuilt LLVM. When '
        'specific prebuilt version of LLVM already '
        'exists, no need to pass the path to compiler '
        'directory.')

    parser.add_argument(
        '-f',
        '--cflags',
        action='append',
        default=[],
        help='Specify the cflags options for the toolchain. '
        'Be sure to quote all the cflags with quotation '
        'mark("") or use equal(=).')
    parser.add_argument(
        '--ldflags',
        action='append',
        default=[],
        help='Specify linker flags for the toolchain.')

    parser.add_argument(
        '-i',
        '--iterations',
        type=int,
        default=1,
        help='Specify how many iterations does the test '
        'take.')

    # Arguments -s and -r are for connecting to DUT.
    parser.add_argument(
        '-s',
        '--serials',
        help='Comma separate list of device serials under '
        'test.')

    parser.add_argument(
        '-r',
        '--remote',
        default='localhost',
        help='hostname[:port] if the ADB device is connected '
        'to a remote machine. Ensure this workstation '
        'is configured for passwordless ssh access as '
        'users "root" or "adb"')

    # Arguments -frequency and -m are for device settings
    parser.add_argument(
        '--frequency',
        type=int,
        default=979200,
        help='Specify the CPU frequency of the device. The '
        'unit is KHZ. The available value is defined in'
        'cpufreq/scaling_available_frequency file in '
        'device\'s each core directory. '
        'The default value is 979200, which shows a '
        'balance in noise and performance. Lower '
        'frequency will slow down the performance but '
        'reduce noise.')

    parser.add_argument(
        '-m',
        '--mode',
        default='little',
        help='User can specify whether \'little\' or \'big\' '
        'mode to use. The default one is little mode. '
        'The little mode runs on a single core of '
        'Cortex-A53, while big mode runs on single core '
        'of Cortex-A57.')

    # Configure file for benchmark test
    parser.add_argument(
        '-t',
        '--test',
        help='Specify the test settings with configuration '
        'file.')

    # Whether to keep old json result or not
    parser.add_argument(
        '-k',
        '--keep',
        default='False',
        help='User can specify whether to keep the old json '
        'results from last run. This can be useful if you '
        'want to compare performance differences in two or '
        'more different runs. Default is False(off).')

    return parser.parse_args(argv)


# Clear old log files in bench suite directory
def clear_logs():
    logging.info('Removing old logfiles...')
    for f in ['build_log', 'device_log', 'test_log']:
        logfile = os.path.join(config.bench_suite_dir, f)
        try:
            os.remove(logfile)
        except OSError:
            logging.info('No logfile %s need to be removed. Ignored.', f)
    logging.info('Old logfiles been removed.')


# Clear old json files in bench suite directory
def clear_results():
    logging.info('Clearing old json results...')
    for bench in config.bench_list:
        result = os.path.join(config.bench_suite_dir, bench + '.json')
        try:
            os.remove(result)
        except OSError:
            logging.info('no %s json file need to be removed. Ignored.', bench)
    logging.info('Old json results been removed.')


# Use subprocess.check_call to run other script, and put logs to files
def check_call_with_log(cmd, log_file):
    log_file = os.path.join(config.bench_suite_dir, log_file)
    with open(log_file, 'a') as logfile:
        log_header = 'Log for command: %s\n' % (cmd)
        logfile.write(log_header)
        try:
            subprocess.check_call(cmd, stdout=logfile)
        except subprocess.CalledProcessError:
            logging.error('Error running %s, please check %s for more info.',
                          cmd, log_file)
            raise
    logging.info('Logs for %s are written to %s.', cmd, log_file)


def set_device(serials, remote, frequency):
    setting_cmd = [
        os.path.join(
            os.path.join(config.android_home, config.autotest_dir),
            'site_utils/set_device.py')
    ]
    setting_cmd.append('-r=' + remote)
    setting_cmd.append('-q=' + str(frequency))

    # Deal with serials.
    # If there is no serails specified, try to run test on the only device.
    # If specified, split the serials into a list and run test on each device.
    if serials:
        for serial in serials.split(','):
            setting_cmd.append('-s=' + serial)
            check_call_with_log(setting_cmd, 'device_log')
            setting_cmd.pop()
    else:
        check_call_with_log(setting_cmd, 'device_log')

    logging.info('CPU mode and frequency set successfully!')


def log_ambiguous_args():
    logging.error('The count of arguments does not match!')
    raise ValueError('The count of arguments does not match.')


# Check if the count of building arguments are log_ambiguous or not.  The
# number of -c/-l, -f, and -os should be either all 0s or all the same.
def check_count(compiler, llvm_version, build_os, cflags, ldflags):
    # Count will be set to 0 if no compiler or llvm_version specified.
    # Otherwise, one of these two args length should be 0 and count will be
    # the other one.
    count = max(len(compiler), len(llvm_version))

    # Check if number of cflags is 0 or the same with before.
    if len(cflags) != 0:
        if count != 0 and len(cflags) != count:
            log_ambiguous_args()
        count = len(cflags)

    if len(ldflags) != 0:
        if count != 0 and len(ldflags) != count:
            log_ambiguous_args()
        count = len(ldflags)

    if len(build_os) != 0:
        if count != 0 and len(build_os) != count:
            log_ambiguous_args()
        count = len(build_os)

    # If no settings are passed, only run default once.
    return max(1, count)


# Build benchmark binary with toolchain settings
def build_bench(setting_no, bench, compiler, llvm_version, build_os, cflags,
                ldflags):
    # Build benchmark locally
    build_cmd = ['./build_bench.py', '-b=' + bench]
    if compiler:
        build_cmd.append('-c=' + compiler[setting_no])
    if llvm_version:
        build_cmd.append('-l=' + llvm_version[setting_no])
    if build_os:
        build_cmd.append('-o=' + build_os[setting_no])
    if cflags:
        build_cmd.append('-f=' + cflags[setting_no])
    if ldflags:
        build_cmd.append('--ldflags=' + ldflags[setting_no])

    logging.info('Building benchmark for toolchain setting No.%d...',
                 setting_no)
    logging.info('Command: %s', build_cmd)

    try:
        subprocess.check_call(build_cmd)
    except:
        logging.error('Error while building benchmark!')
        raise


def run_and_collect_result(test_cmd, setting_no, i, bench, serial='default'):

    # Run autotest script for benchmark on DUT
    check_call_with_log(test_cmd, 'test_log')

    logging.info('Benchmark with setting No.%d, iter.%d finished testing on '
                 'device %s.', setting_no, i, serial)

    # Rename results from the bench_result generated in autotest
    bench_result = os.path.join(config.bench_suite_dir, 'bench_result')
    if not os.path.exists(bench_result):
        logging.error('No result found at %s, '
                      'please check test_log for details.', bench_result)
        raise OSError('Result file %s not found.' % bench_result)

    new_bench_result = 'bench_result_%s_%s_%d_%d' % (bench, serial,
                                                     setting_no, i)
    new_bench_result_path = os.path.join(config.bench_suite_dir,
                                         new_bench_result)
    try:
        os.rename(bench_result, new_bench_result_path)
    except OSError:
        logging.error('Error while renaming raw result %s to %s',
                      bench_result, new_bench_result_path)
        raise

    logging.info('Benchmark result saved at %s.', new_bench_result_path)


def test_bench(bench, setting_no, iterations, serials, remote, mode):
    logging.info('Start running benchmark on device...')

    # Run benchmark and tests on DUT
    for i in xrange(iterations):
        logging.info('Iteration No.%d:', i)
        test_cmd = [
            os.path.join(
                os.path.join(config.android_home, config.autotest_dir),
                'site_utils/test_bench.py')
        ]
        test_cmd.append('-b=' + bench)
        test_cmd.append('-r=' + remote)
        test_cmd.append('-m=' + mode)

        # Deal with serials. If there is no serails specified, try to run test
        # on the only device. If specified, split the serials into a list and
        # run test on each device.
        if serials:
            for serial in serials.split(','):
                test_cmd.append('-s=' + serial)

                run_and_collect_result(test_cmd, setting_no, i, bench, serial)
                test_cmd.pop()
        else:
            run_and_collect_result(test_cmd, setting_no, i, bench)


def gen_json(bench, setting_no, iterations, serials):
    bench_result = os.path.join(config.bench_suite_dir, 'bench_result')

    logging.info('Generating JSON file for Crosperf...')

    if not serials:
        serials = 'default'

    for serial in serials.split(','):

        # Platform will be used as device lunch combo instead
        #experiment = '_'.join([serial, str(setting_no)])
        experiment = config.product_combo

        # Input format: bench_result_{bench}_{serial}_{setting_no}_
        input_file = '_'.join([bench_result, bench,
                               serial, str(setting_no), ''])
        gen_json_cmd = [
            './gen_json.py', '--input=' + input_file,
            '--output=%s.json' % os.path.join(config.bench_suite_dir, bench),
            '--bench=' + bench, '--platform=' + experiment,
            '--iterations=' + str(iterations)
        ]

        logging.info('Command: %s', gen_json_cmd)
        if subprocess.call(gen_json_cmd):
            logging.error('Error while generating JSON file, please check raw'
                          ' data of the results at %s.', input_file)


def gen_crosperf(infile, outfile):
    # Set environment variable for crosperf
    os.environ['PYTHONPATH'] = os.path.dirname(config.toolchain_utils)

    logging.info('Generating Crosperf Report...')
    crosperf_cmd = [
        os.path.join(config.toolchain_utils, 'generate_report.py'),
        '-i=' + infile, '-o=' + outfile, '-f'
    ]

    # Run crosperf generate_report.py
    logging.info('Command: %s', crosperf_cmd)
    subprocess.call(crosperf_cmd)

    logging.info('Report generated successfully!')
    logging.info('Report Location: ' + outfile + '.html at bench'
                 'suite directory.')


def main(argv):
    # Set environment variable for the local loacation of benchmark suite.
    # This is for collecting testing results to benchmark suite directory.
    os.environ['BENCH_SUITE_DIR'] = config.bench_suite_dir

    # Set Android type, used for the difference part between aosp and internal.
    os.environ['ANDROID_TYPE'] = config.android_type

    # Set ANDROID_HOME for both building and testing.
    os.environ['ANDROID_HOME'] = config.android_home

    # Set environment variable for architecture, this will be used in
    # autotest.
    os.environ['PRODUCT'] = config.product

    arguments = _parse_arguments(argv)

    bench_list = arguments.bench
    if not bench_list:
        bench_list = config.bench_list

    compiler = arguments.compiler_dir
    build_os = arguments.build_os
    llvm_version = arguments.llvm_prebuilts_version
    cflags = arguments.cflags
    ldflags = arguments.ldflags
    iterations = arguments.iterations
    serials = arguments.serials
    remote = arguments.remote
    frequency = arguments.frequency
    mode = arguments.mode
    keep = arguments.keep

    # Clear old logs every time before run script
    clear_logs()

    if keep == 'False':
        clear_results()

    # Set test mode and frequency of CPU on the DUT
    set_device(serials, remote, frequency)

    test = arguments.test
    # if test configuration file has been given, use the build settings
    # in the configuration file and run the test.
    if test:
        test_config = ConfigParser.ConfigParser(allow_no_value=True)
        if not test_config.read(test):
            logging.error('Error while reading from building '
                          'configuration file %s.', test)
            raise RuntimeError('Error while reading configuration file %s.'
                               % test)

        for setting_no, section in enumerate(test_config.sections()):
            bench = test_config.get(section, 'bench')
            compiler = [test_config.get(section, 'compiler')]
            build_os = [test_config.get(section, 'build_os')]
            llvm_version = [test_config.get(section, 'llvm_version')]
            cflags = [test_config.get(section, 'cflags')]
            ldflags = [test_config.get(section, 'ldflags')]

            # Set iterations from test_config file, if not exist, use the one
            # from command line.
            it = test_config.get(section, 'iterations')
            if not it:
                it = iterations
            it = int(it)

            # Build benchmark for each single test configuration
            build_bench(0, bench, compiler, llvm_version,
                        build_os, cflags, ldflags)

            test_bench(bench, setting_no, it, serials, remote, mode)

            gen_json(bench, setting_no, it, serials)

        for bench in config.bench_list:
            infile = os.path.join(config.bench_suite_dir, bench + '.json')
            if os.path.exists(infile):
                outfile = os.path.join(config.bench_suite_dir,
                                       bench + '_report')
                gen_crosperf(infile, outfile)

        # Stop script if there is only config file provided
        return 0

    # If no configuration file specified, continue running.
    # Check if the count of the setting arguments are log_ambiguous.
    setting_count = check_count(compiler, llvm_version, build_os,
                                cflags, ldflags)

    for bench in bench_list:
        logging.info('Start building and running benchmark: [%s]', bench)
        # Run script for each toolchain settings
        for setting_no in xrange(setting_count):
            build_bench(setting_no, bench, compiler, llvm_version,
                        build_os, cflags, ldflags)

            # Run autotest script for benchmark test on device
            test_bench(bench, setting_no, iterations, serials, remote, mode)

            gen_json(bench, setting_no, iterations, serials)

        infile = os.path.join(config.bench_suite_dir, bench + '.json')
        outfile = os.path.join(config.bench_suite_dir, bench + '_report')
        gen_crosperf(infile, outfile)


if __name__ == '__main__':
    main(sys.argv[1:])