1#!/usr/bin/python3 2# 3# Copyright 2015 The ANGLE Project Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6# 7# perf_test_runner.py: 8# Helper script for running and analyzing perftest results. Runs the 9# tests in an infinite batch, printing out the mean and coefficient of 10# variation of the population continuously. 11# 12 13import argparse 14import glob 15import logging 16import os 17import re 18import subprocess 19import sys 20 21base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) 22 23# We look in this path for a recent build. 24TEST_SUITE_SEARCH_PATH = glob.glob('out/*') 25DEFAULT_METRIC = 'wall_time' 26DEFAULT_EXPERIMENTS = 10 27 28DEFAULT_TEST_SUITE = 'angle_perftests' 29 30if sys.platform == 'win32': 31 DEFAULT_TEST_NAME = 'DrawCallPerfBenchmark.Run/d3d11_null' 32else: 33 DEFAULT_TEST_NAME = 'DrawCallPerfBenchmark.Run/gl' 34 35EXIT_SUCCESS = 0 36EXIT_FAILURE = 1 37 38scores = [] 39 40 41# Danke to http://stackoverflow.com/a/27758326 42def mean(data): 43 """Return the sample arithmetic mean of data.""" 44 n = len(data) 45 if n < 1: 46 raise ValueError('mean requires at least one data point') 47 return float(sum(data)) / float(n) # in Python 2 use sum(data)/float(n) 48 49 50def sum_of_square_deviations(data, c): 51 """Return sum of square deviations of sequence data.""" 52 ss = sum((float(x) - c)**2 for x in data) 53 return ss 54 55 56def coefficient_of_variation(data): 57 """Calculates the population coefficient of variation.""" 58 n = len(data) 59 if n < 2: 60 raise ValueError('variance requires at least two data points') 61 c = mean(data) 62 ss = sum_of_square_deviations(data, c) 63 pvar = ss / n # the population variance 64 stddev = (pvar**0.5) # population standard deviation 65 return stddev / c 66 67 68def truncated_list(data, n): 69 """Compute a truncated list, n is truncation size""" 70 if len(data) < n * 2: 71 raise ValueError('list not large enough to truncate') 72 return sorted(data)[n:-n] 73 74 75def truncated_mean(data, n): 76 """Compute a truncated mean, n is truncation size""" 77 return mean(truncated_list(data, n)) 78 79 80def truncated_cov(data, n): 81 """Compute a truncated coefficient of variation, n is truncation size""" 82 return coefficient_of_variation(truncated_list(data, n)) 83 84 85def main(raw_args): 86 parser = argparse.ArgumentParser() 87 parser.add_argument( 88 '--suite', 89 help='Test suite binary. Default is "%s".' % DEFAULT_TEST_SUITE, 90 default=DEFAULT_TEST_SUITE) 91 parser.add_argument( 92 '-m', 93 '--metric', 94 help='Test metric. Default is "%s".' % DEFAULT_METRIC, 95 default=DEFAULT_METRIC) 96 parser.add_argument( 97 '--experiments', 98 help='Number of experiments to run. Default is %d.' % DEFAULT_EXPERIMENTS, 99 default=DEFAULT_EXPERIMENTS, 100 type=int) 101 parser.add_argument('-v', '--verbose', help='Extra verbose logging.', action='store_true') 102 parser.add_argument('test_name', help='Test to run', default=DEFAULT_TEST_NAME) 103 args, extra_args = parser.parse_known_args(raw_args) 104 105 if args.verbose: 106 logging.basicConfig(level='DEBUG') 107 108 if sys.platform == 'win32': 109 args.suite += '.exe' 110 111 # Find most recent binary 112 newest_binary = None 113 newest_mtime = None 114 115 for path in TEST_SUITE_SEARCH_PATH: 116 binary_path = os.path.join(base_path, path, args.suite) 117 if os.path.exists(binary_path): 118 binary_mtime = os.path.getmtime(binary_path) 119 if (newest_binary is None) or (binary_mtime > newest_mtime): 120 newest_binary = binary_path 121 newest_mtime = binary_mtime 122 123 perftests_path = newest_binary 124 125 if perftests_path == None or not os.path.exists(perftests_path): 126 print('Cannot find %s in %s!' % (args.suite, TEST_SUITE_SEARCH_PATH)) 127 return EXIT_FAILURE 128 129 print('Using test executable: %s' % perftests_path) 130 print('Test name: %s' % args.test_name) 131 132 def get_results(metric, extra_args=[]): 133 run = [perftests_path, '--gtest_filter=%s' % args.test_name] + extra_args 134 logging.info('running %s' % str(run)) 135 process = subprocess.Popen( 136 run, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8') 137 output, err = process.communicate() 138 139 m = re.search(r'Running (\d+) tests', output) 140 if m and int(m.group(1)) > 1: 141 print(output) 142 raise Exception('Found more than one test result in output') 143 144 # Results are reported in the format: 145 # name_backend.metric: story= value units. 146 pattern = r'\.' + metric + r':.*= ([0-9.]+)' 147 logging.debug('searching for %s in output' % pattern) 148 m = re.findall(pattern, output) 149 if not m: 150 print(output) 151 raise Exception('Did not find the metric "%s" in the test output' % metric) 152 153 return [float(value) for value in m] 154 155 # Calibrate the number of steps 156 steps = get_results("steps_to_run", ["--calibration"] + extra_args)[0] 157 print("running with %d steps." % steps) 158 159 # Loop 'args.experiments' times, running the tests. 160 for experiment in range(args.experiments): 161 experiment_scores = get_results(args.metric, 162 ["--steps-per-trial", str(steps)] + extra_args) 163 164 for score in experiment_scores: 165 sys.stdout.write("%s: %.2f" % (args.metric, score)) 166 scores.append(score) 167 168 if (len(scores) > 1): 169 sys.stdout.write(", mean: %.2f" % mean(scores)) 170 sys.stdout.write(", variation: %.2f%%" % 171 (coefficient_of_variation(scores) * 100.0)) 172 173 if (len(scores) > 7): 174 truncation_n = len(scores) >> 3 175 sys.stdout.write(", truncated mean: %.2f" % truncated_mean(scores, truncation_n)) 176 sys.stdout.write(", variation: %.2f%%" % 177 (truncated_cov(scores, truncation_n) * 100.0)) 178 179 print("") 180 181 return EXIT_SUCCESS 182 183 184if __name__ == "__main__": 185 sys.exit(main(sys.argv[1:])) 186