1 #!/usr/bin/env python 2 3 # Copyright 2016 Google Inc. 4 # 5 # Use of this source code is governed by a BSD-style license that can be 6 # found in the LICENSE file. 7 8 from __future__ import print_function 9 from _adb import Adb 10 from _benchresult import BenchResult 11 from _hardware import HardwareException, Hardware 12 from argparse import ArgumentParser 13 from multiprocessing import Queue 14 from threading import Thread, Timer 15 import collections 16 import glob 17 import math 18 import re 19 import subprocess 20 import sys 21 import time 22 23 __argparse = ArgumentParser(description=""" 24 25 Executes the skpbench binary with various configs and skps. 26 27 Also monitors the output in order to filter out and re-run results that have an 28 unacceptable stddev. 29 30 """) 31 32 __argparse.add_argument('skpbench', 33 help="path to the skpbench binary") 34 __argparse.add_argument('--adb', 35 action='store_true', help="execute skpbench over adb") 36 __argparse.add_argument('--adb_binary', default='adb', 37 help="The name of the adb binary to use.") 38 __argparse.add_argument('-s', '--device-serial', 39 help="if using adb, ID of the specific device to target " 40 "(only required if more than 1 device is attached)") 41 __argparse.add_argument('-m', '--max-stddev', 42 type=float, default=4, 43 help="initial max allowable relative standard deviation") 44 __argparse.add_argument('-x', '--suffix', 45 help="suffix to append on config (e.g. '_before', '_after')") 46 __argparse.add_argument('-w','--write-path', 47 help="directory to save .png proofs to disk.") 48 __argparse.add_argument('-v','--verbosity', 49 type=int, default=1, help="level of verbosity (0=none to 5=debug)") 50 __argparse.add_argument('-d', '--duration', 51 type=int, help="number of milliseconds to run each benchmark") 52 __argparse.add_argument('-l', '--sample-ms', 53 type=int, help="duration of a sample (minimum)") 54 __argparse.add_argument('--gpu', 55 action='store_true', 56 help="perform timing on the gpu clock instead of cpu (gpu work only)") 57 __argparse.add_argument('--fps', 58 action='store_true', help="use fps instead of ms") 59 __argparse.add_argument('--pr', 60 help="comma- or space-separated list of GPU path renderers, including: " 61 "[[~]all [~]default [~]dashline [~]nvpr [~]msaa [~]aaconvex " 62 "[~]aalinearizing [~]small [~]tess]") 63 __argparse.add_argument('--nocache', 64 action='store_true', help="disable caching of path mask textures") 65 __argparse.add_argument('-c', '--config', 66 default='gl', help="comma- or space-separated list of GPU configs") 67 __argparse.add_argument('-a', '--resultsfile', 68 help="optional file to append results into") 69 __argparse.add_argument('skps', 70 nargs='+', 71 help=".skp files or directories to expand for .skp files") 72 73 FLAGS = __argparse.parse_args() 74 if FLAGS.adb: 75 import _adb_path as _path 76 _path.init(FLAGS.device_serial, FLAGS.adb_binary) 77 else: 78 import _os_path as _path 79 80 def dump_commandline_if_verbose(commandline): 81 if FLAGS.verbosity >= 5: 82 quoted = ['\'%s\'' % re.sub(r'([\\\'])', r'\\\1', x) for x in commandline] 83 print(' '.join(quoted), file=sys.stderr) 84 85 86 class StddevException(Exception): 87 pass 88 89 class Message: 90 READLINE = 0, 91 POLL_HARDWARE = 1, 92 EXIT = 2 93 def __init__(self, message, value=None): 94 self.message = message 95 self.value = value 96 97 class SubprocessMonitor(Thread): 98 def __init__(self, queue, proc): 99 self._queue = queue 100 self._proc = proc 101 Thread.__init__(self) 102 103 def run(self): 104 """Runs on the background thread.""" 105 for line in iter(self._proc.stdout.readline, b''): 106 self._queue.put(Message(Message.READLINE, line.decode('utf-8').rstrip())) 107 self._queue.put(Message(Message.EXIT)) 108 109 class SKPBench: 110 ARGV = [FLAGS.skpbench, '--verbosity', str(FLAGS.verbosity)] 111 if FLAGS.duration: 112 ARGV.extend(['--duration', str(FLAGS.duration)]) 113 if FLAGS.sample_ms: 114 ARGV.extend(['--sampleMs', str(FLAGS.sample_ms)]) 115 if FLAGS.gpu: 116 ARGV.extend(['--gpuClock', 'true']) 117 if FLAGS.fps: 118 ARGV.extend(['--fps', 'true']) 119 if FLAGS.pr: 120 ARGV.extend(['--pr'] + re.split(r'[ ,]', FLAGS.pr)) 121 if FLAGS.nocache: 122 ARGV.extend(['--cachePathMasks', 'false']) 123 if FLAGS.adb: 124 if FLAGS.device_serial is None: 125 ARGV[:0] = [FLAGS.adb_binary, 'shell'] 126 else: 127 ARGV[:0] = [FLAGS.adb_binary, '-s', FLAGS.device_serial, 'shell'] 128 129 @classmethod 130 def get_header(cls, outfile=sys.stdout): 131 commandline = cls.ARGV + ['--duration', '0'] 132 dump_commandline_if_verbose(commandline) 133 out = subprocess.check_output(commandline, stderr=subprocess.STDOUT) 134 return out.rstrip() 135 136 @classmethod 137 def run_warmup(cls, warmup_time, config): 138 if not warmup_time: 139 return 140 print('running %i second warmup...' % warmup_time, file=sys.stderr) 141 commandline = cls.ARGV + ['--duration', str(warmup_time * 1000), 142 '--config', config, 143 '--skp', 'warmup'] 144 dump_commandline_if_verbose(commandline) 145 output = subprocess.check_output(commandline, stderr=subprocess.STDOUT) 146 147 # validate the warmup run output. 148 for line in output.decode('utf-8').split('\n'): 149 match = BenchResult.match(line.rstrip()) 150 if match and match.bench == 'warmup': 151 return 152 raise Exception('Invalid warmup output:\n%s' % output) 153 154 def __init__(self, skp, config, max_stddev, best_result=None): 155 self.skp = skp 156 self.config = config 157 self.max_stddev = max_stddev 158 self.best_result = best_result 159 self._queue = Queue() 160 self._proc = None 161 self._monitor = None 162 self._hw_poll_timer = None 163 164 def __enter__(self): 165 return self 166 167 def __exit__(self, exception_type, exception_value, traceback): 168 if self._proc: 169 self.terminate() 170 if self._hw_poll_timer: 171 self._hw_poll_timer.cancel() 172 173 def execute(self, hardware): 174 hardware.sanity_check() 175 self._schedule_hardware_poll() 176 177 commandline = self.ARGV + ['--config', self.config, 178 '--skp', self.skp, 179 '--suppressHeader', 'true'] 180 if FLAGS.write_path: 181 pngfile = _path.join(FLAGS.write_path, self.config, 182 _path.basename(self.skp) + '.png') 183 commandline.extend(['--png', pngfile]) 184 dump_commandline_if_verbose(commandline) 185 self._proc = subprocess.Popen(commandline, stdout=subprocess.PIPE, 186 stderr=subprocess.STDOUT) 187 self._monitor = SubprocessMonitor(self._queue, self._proc) 188 self._monitor.start() 189 190 while True: 191 message = self._queue.get() 192 if message.message == Message.READLINE: 193 result = BenchResult.match(message.value) 194 if result: 195 hardware.sanity_check() 196 self._process_result(result) 197 elif hardware.filter_line(message.value): 198 print(message.value, file=sys.stderr) 199 continue 200 if message.message == Message.POLL_HARDWARE: 201 hardware.sanity_check() 202 self._schedule_hardware_poll() 203 continue 204 if message.message == Message.EXIT: 205 self._monitor.join() 206 self._proc.wait() 207 if self._proc.returncode != 0: 208 raise Exception("skpbench exited with nonzero exit code %i" % 209 self._proc.returncode) 210 self._proc = None 211 break 212 213 def _schedule_hardware_poll(self): 214 if self._hw_poll_timer: 215 self._hw_poll_timer.cancel() 216 self._hw_poll_timer = \ 217 Timer(1, lambda: self._queue.put(Message(Message.POLL_HARDWARE))) 218 self._hw_poll_timer.start() 219 220 def _process_result(self, result): 221 if not self.best_result or result.stddev <= self.best_result.stddev: 222 self.best_result = result 223 elif FLAGS.verbosity >= 2: 224 print("reusing previous result for %s/%s with lower stddev " 225 "(%s%% instead of %s%%)." % 226 (result.config, result.bench, self.best_result.stddev, 227 result.stddev), file=sys.stderr) 228 if self.max_stddev and self.best_result.stddev > self.max_stddev: 229 raise StddevException() 230 231 def terminate(self): 232 if self._proc: 233 self._proc.terminate() 234 self._monitor.join() 235 self._proc.wait() 236 self._proc = None 237 238 def emit_result(line, resultsfile=None): 239 print(line) 240 sys.stdout.flush() 241 if resultsfile: 242 print(line, file=resultsfile) 243 resultsfile.flush() 244 245 def run_benchmarks(configs, skps, hardware, resultsfile=None): 246 hasheader = False 247 benches = collections.deque([(skp, config, FLAGS.max_stddev) 248 for skp in skps 249 for config in configs]) 250 while benches: 251 try: 252 with hardware: 253 SKPBench.run_warmup(hardware.warmup_time, configs[0]) 254 if not hasheader: 255 emit_result(SKPBench.get_header(), resultsfile) 256 hasheader = True 257 while benches: 258 benchargs = benches.popleft() 259 with SKPBench(*benchargs) as skpbench: 260 try: 261 skpbench.execute(hardware) 262 if skpbench.best_result: 263 emit_result(skpbench.best_result.format(FLAGS.suffix), 264 resultsfile) 265 else: 266 print("WARNING: no result for %s with config %s" % 267 (skpbench.skp, skpbench.config), file=sys.stderr) 268 269 except StddevException: 270 retry_max_stddev = skpbench.max_stddev * math.sqrt(2) 271 if FLAGS.verbosity >= 1: 272 print("stddev is too high for %s/%s (%s%%, max=%.2f%%), " 273 "re-queuing with max=%.2f%%." % 274 (skpbench.best_result.config, skpbench.best_result.bench, 275 skpbench.best_result.stddev, skpbench.max_stddev, 276 retry_max_stddev), 277 file=sys.stderr) 278 benches.append((skpbench.skp, skpbench.config, retry_max_stddev, 279 skpbench.best_result)) 280 281 except HardwareException as exception: 282 skpbench.terminate() 283 if FLAGS.verbosity >= 4: 284 hardware.print_debug_diagnostics() 285 if FLAGS.verbosity >= 1: 286 print("%s; rebooting and taking a %i second nap..." % 287 (exception.message, exception.sleeptime), file=sys.stderr) 288 benches.appendleft(benchargs) # retry the same bench next time. 289 raise # wake hw up from benchmarking mode before the nap. 290 291 except HardwareException as exception: 292 time.sleep(exception.sleeptime) 293 294 def main(): 295 # Delimiter is ',' or ' ', skip if nested inside parens (e.g. gpu(a=b,c=d)). 296 DELIMITER = r'[, ](?!(?:[^(]*\([^)]*\))*[^()]*\))' 297 configs = re.split(DELIMITER, FLAGS.config) 298 skps = _path.find_skps(FLAGS.skps) 299 300 if FLAGS.adb: 301 adb = Adb(FLAGS.device_serial, FLAGS.adb_binary, 302 echo=(FLAGS.verbosity >= 5)) 303 model = adb.check('getprop ro.product.model').strip() 304 if model == 'Pixel C': 305 from _hardware_pixel_c import HardwarePixelC 306 hardware = HardwarePixelC(adb) 307 elif model == 'Pixel': 308 from _hardware_pixel import HardwarePixel 309 hardware = HardwarePixel(adb) 310 elif model == 'Nexus 6P': 311 from _hardware_nexus_6p import HardwareNexus6P 312 hardware = HardwareNexus6P(adb) 313 else: 314 from _hardware_android import HardwareAndroid 315 print("WARNING: %s: don't know how to monitor this hardware; results " 316 "may be unreliable." % model, file=sys.stderr) 317 hardware = HardwareAndroid(adb) 318 else: 319 hardware = Hardware() 320 321 if FLAGS.resultsfile: 322 with open(FLAGS.resultsfile, mode='a+') as resultsfile: 323 run_benchmarks(configs, skps, hardware, resultsfile=resultsfile) 324 else: 325 run_benchmarks(configs, skps, hardware) 326 327 328 if __name__ == '__main__': 329 main() 330