84fd1c2dec
Bug: skia:11760, skia:11787 Change-Id: Idfedb90576e0484bf32a9002081c0fcb888141eb Reviewed-on: https://skia-review.googlesource.com/c/skia/+/388216 Reviewed-by: Michael Ludwig <michaelludwig@google.com> Commit-Queue: Robert Phillips <robertphillips@google.com>
391 lines
14 KiB
Python
Executable File
391 lines
14 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright 2016 Google Inc.
|
|
#
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
from __future__ import print_function
|
|
from _adb import Adb
|
|
from _benchresult import BenchResult
|
|
from _hardware import HardwareException, Hardware
|
|
from argparse import ArgumentParser
|
|
from multiprocessing import Queue
|
|
from threading import Thread, Timer
|
|
import collections
|
|
import glob
|
|
import math
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
__argparse = ArgumentParser(description="""
|
|
|
|
Executes the skpbench binary with various configs and skps.
|
|
|
|
Also monitors the output in order to filter out and re-run results that have an
|
|
unacceptable stddev.
|
|
|
|
""")
|
|
|
|
__argparse.add_argument('skpbench',
|
|
help="path to the skpbench binary")
|
|
__argparse.add_argument('--adb',
|
|
action='store_true', help="execute skpbench over adb")
|
|
__argparse.add_argument('--adb_binary', default='adb',
|
|
help="The name of the adb binary to use.")
|
|
__argparse.add_argument('-s', '--device-serial',
|
|
help="if using adb, ID of the specific device to target "
|
|
"(only required if more than 1 device is attached)")
|
|
__argparse.add_argument('-m', '--max-stddev',
|
|
type=float, default=4,
|
|
help="initial max allowable relative standard deviation")
|
|
__argparse.add_argument('-x', '--suffix',
|
|
help="suffix to append on config (e.g. '_before', '_after')")
|
|
__argparse.add_argument('-w','--write-path',
|
|
help="directory to save .png proofs to disk.")
|
|
__argparse.add_argument('-v','--verbosity',
|
|
type=int, default=1, help="level of verbosity (0=none to 5=debug)")
|
|
__argparse.add_argument('-d', '--duration',
|
|
type=int, help="number of milliseconds to run each benchmark")
|
|
__argparse.add_argument('-l', '--sample-ms',
|
|
type=int, help="duration of a sample (minimum)")
|
|
__argparse.add_argument('--gpu',
|
|
action='store_true',
|
|
help="perform timing on the gpu clock instead of cpu (gpu work only)")
|
|
__argparse.add_argument('--fps',
|
|
action='store_true', help="use fps instead of ms")
|
|
__argparse.add_argument('--pr',
|
|
help="comma- or space-separated list of GPU path renderers, including: "
|
|
"[[~]all [~]default [~]dashline [~]msaa [~]aaconvex "
|
|
"[~]aalinearizing [~]small [~]tess]")
|
|
__argparse.add_argument('--cc',
|
|
action='store_true', help="allow coverage counting shortcuts to render paths")
|
|
__argparse.add_argument('--nocache',
|
|
action='store_true', help="disable caching of path mask textures")
|
|
__argparse.add_argument('--allPathsVolatile',
|
|
action='store_true',
|
|
help="Causes all GPU paths to be processed as if 'setIsVolatile' had been called.")
|
|
__argparse.add_argument('-c', '--config',
|
|
default='gl', help="comma- or space-separated list of GPU configs")
|
|
__argparse.add_argument('-a', '--resultsfile',
|
|
help="optional file to append results into")
|
|
__argparse.add_argument('--ddl',
|
|
action='store_true', help="record the skp into DDLs before rendering")
|
|
__argparse.add_argument('--lock-clocks',
|
|
action='store_true', help="Put device in benchmarking mode (locked clocks, no other processes)")
|
|
__argparse.add_argument('--clock-speed',
|
|
type=float, default=66.0, help="A number between 0 and 100 indicating how fast to lock the CPU and GPU clock."
|
|
"Valid speeds are chosen from their respective available frequencies list.")
|
|
__argparse.add_argument('--ddlNumRecordingThreads',
|
|
type=int, default=0,
|
|
help="number of DDL recording threads (0=num_cores)")
|
|
__argparse.add_argument('--ddlTilingWidthHeight',
|
|
type=int, default=0, help="number of tiles along one edge when in DDL mode")
|
|
__argparse.add_argument('--reduceOpsTaskSplitting',
|
|
action='store_true', help="reorder GPU tasks to reduce render target swaps")
|
|
__argparse.add_argument('--gpuThreads',
|
|
type=int, default=-1,
|
|
help="Create this many extra threads to assist with GPU work, including"
|
|
" software path rendering. Defaults to two.")
|
|
__argparse.add_argument('--internalSamples',
|
|
type=int, default=-1,
|
|
help="Number of samples for internal draws that use MSAA or mixed samples.")
|
|
__argparse.add_argument('srcs',
|
|
nargs='+',
|
|
help=".skp files or directories to expand for .skp files, and/or .svg files")
|
|
|
|
FLAGS = __argparse.parse_args()
|
|
if FLAGS.adb:
|
|
import _adb_path as _path
|
|
_path.init(FLAGS.device_serial, FLAGS.adb_binary)
|
|
else:
|
|
import _os_path as _path
|
|
|
|
def dump_commandline_if_verbose(commandline):
|
|
if FLAGS.verbosity >= 5:
|
|
quoted = ['\'%s\'' % re.sub(r'([\\\'])', r'\\\1', x) for x in commandline]
|
|
print(' '.join(quoted), file=sys.stderr)
|
|
|
|
|
|
class StddevException(Exception):
|
|
pass
|
|
|
|
class Message:
|
|
READLINE = 0,
|
|
POLL_HARDWARE = 1,
|
|
EXIT = 2
|
|
def __init__(self, message, value=None):
|
|
self.message = message
|
|
self.value = value
|
|
|
|
class SubprocessMonitor(Thread):
|
|
def __init__(self, queue, proc):
|
|
self._queue = queue
|
|
self._proc = proc
|
|
Thread.__init__(self)
|
|
|
|
def run(self):
|
|
"""Runs on the background thread."""
|
|
for line in iter(self._proc.stdout.readline, b''):
|
|
self._queue.put(Message(Message.READLINE, line.decode('utf-8').rstrip()))
|
|
self._queue.put(Message(Message.EXIT))
|
|
|
|
class SKPBench:
|
|
ARGV = [FLAGS.skpbench, '--verbosity', str(FLAGS.verbosity)]
|
|
if FLAGS.duration:
|
|
ARGV.extend(['--duration', str(FLAGS.duration)])
|
|
if FLAGS.sample_ms:
|
|
ARGV.extend(['--sampleMs', str(FLAGS.sample_ms)])
|
|
if FLAGS.gpu:
|
|
ARGV.extend(['--gpuClock', 'true'])
|
|
if FLAGS.fps:
|
|
ARGV.extend(['--fps', 'true'])
|
|
if FLAGS.pr:
|
|
ARGV.extend(['--pr'] + re.split(r'[ ,]', FLAGS.pr))
|
|
if FLAGS.cc:
|
|
ARGV.extend(['--cc', 'true'])
|
|
if FLAGS.nocache:
|
|
ARGV.extend(['--cachePathMasks', 'false'])
|
|
if FLAGS.allPathsVolatile:
|
|
ARGV.extend(['--allPathsVolatile', 'true'])
|
|
if FLAGS.gpuThreads != -1:
|
|
ARGV.extend(['--gpuThreads', str(FLAGS.gpuThreads)])
|
|
if FLAGS.internalSamples != -1:
|
|
ARGV.extend(['--internalSamples', str(FLAGS.internalSamples)])
|
|
|
|
# DDL parameters
|
|
if FLAGS.ddl:
|
|
ARGV.extend(['--ddl', 'true'])
|
|
if FLAGS.ddlNumRecordingThreads:
|
|
ARGV.extend(['--ddlNumRecordingThreads',
|
|
str(FLAGS.ddlNumRecordingThreads)])
|
|
if FLAGS.ddlTilingWidthHeight:
|
|
ARGV.extend(['--ddlTilingWidthHeight', str(FLAGS.ddlTilingWidthHeight)])
|
|
|
|
if FLAGS.reduceOpsTaskSplitting:
|
|
ARGV.extend(['--reduceOpsTaskSplitting'])
|
|
|
|
if FLAGS.adb:
|
|
if FLAGS.device_serial is None:
|
|
ARGV[:0] = [FLAGS.adb_binary, 'shell']
|
|
else:
|
|
ARGV[:0] = [FLAGS.adb_binary, '-s', FLAGS.device_serial, 'shell']
|
|
|
|
@classmethod
|
|
def get_header(cls, outfile=sys.stdout):
|
|
commandline = cls.ARGV + ['--duration', '0']
|
|
dump_commandline_if_verbose(commandline)
|
|
out = subprocess.check_output(commandline, stderr=subprocess.STDOUT)
|
|
return out.rstrip()
|
|
|
|
@classmethod
|
|
def run_warmup(cls, warmup_time, config):
|
|
if not warmup_time:
|
|
return
|
|
print('running %i second warmup...' % warmup_time, file=sys.stderr)
|
|
commandline = cls.ARGV + ['--duration', str(warmup_time * 1000),
|
|
'--config', config,
|
|
'--src', 'warmup']
|
|
dump_commandline_if_verbose(commandline)
|
|
output = subprocess.check_output(commandline, stderr=subprocess.STDOUT)
|
|
|
|
# validate the warmup run output.
|
|
for line in output.decode('utf-8').split('\n'):
|
|
match = BenchResult.match(line.rstrip())
|
|
if match and match.bench == 'warmup':
|
|
return
|
|
raise Exception('Invalid warmup output:\n%s' % output)
|
|
|
|
def __init__(self, src, config, max_stddev, best_result=None):
|
|
self.src = src
|
|
self.config = config
|
|
self.max_stddev = max_stddev
|
|
self.best_result = best_result
|
|
self._queue = Queue()
|
|
self._proc = None
|
|
self._monitor = None
|
|
self._hw_poll_timer = None
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exception_type, exception_value, traceback):
|
|
if self._proc:
|
|
self.terminate()
|
|
if self._hw_poll_timer:
|
|
self._hw_poll_timer.cancel()
|
|
|
|
def execute(self, hardware):
|
|
hardware.sanity_check()
|
|
self._schedule_hardware_poll()
|
|
|
|
commandline = self.ARGV + ['--config', self.config,
|
|
'--src', self.src,
|
|
'--suppressHeader', 'true']
|
|
if FLAGS.write_path:
|
|
pngfile = _path.join(FLAGS.write_path, self.config,
|
|
_path.basename(self.src) + '.png')
|
|
commandline.extend(['--png', pngfile])
|
|
dump_commandline_if_verbose(commandline)
|
|
self._proc = subprocess.Popen(commandline, stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
self._monitor = SubprocessMonitor(self._queue, self._proc)
|
|
self._monitor.start()
|
|
|
|
while True:
|
|
message = self._queue.get()
|
|
if message.message == Message.READLINE:
|
|
result = BenchResult.match(message.value)
|
|
if result:
|
|
hardware.sanity_check()
|
|
self._process_result(result)
|
|
elif hardware.filter_line(message.value):
|
|
print(message.value, file=sys.stderr)
|
|
continue
|
|
if message.message == Message.POLL_HARDWARE:
|
|
hardware.sanity_check()
|
|
self._schedule_hardware_poll()
|
|
continue
|
|
if message.message == Message.EXIT:
|
|
self._monitor.join()
|
|
self._proc.wait()
|
|
if self._proc.returncode != 0:
|
|
raise Exception("skpbench exited with nonzero exit code %i" %
|
|
self._proc.returncode)
|
|
self._proc = None
|
|
break
|
|
|
|
def _schedule_hardware_poll(self):
|
|
if self._hw_poll_timer:
|
|
self._hw_poll_timer.cancel()
|
|
self._hw_poll_timer = \
|
|
Timer(1, lambda: self._queue.put(Message(Message.POLL_HARDWARE)))
|
|
self._hw_poll_timer.start()
|
|
|
|
def _process_result(self, result):
|
|
if not self.best_result or result.stddev <= self.best_result.stddev:
|
|
self.best_result = result
|
|
elif FLAGS.verbosity >= 2:
|
|
print("reusing previous result for %s/%s with lower stddev "
|
|
"(%s%% instead of %s%%)." %
|
|
(result.config, result.bench, self.best_result.stddev,
|
|
result.stddev), file=sys.stderr)
|
|
if self.max_stddev and self.best_result.stddev > self.max_stddev:
|
|
raise StddevException()
|
|
|
|
def terminate(self):
|
|
if self._proc:
|
|
self._proc.terminate()
|
|
self._monitor.join()
|
|
self._proc.wait()
|
|
self._proc = None
|
|
|
|
def emit_result(line, resultsfile=None):
|
|
print(line)
|
|
sys.stdout.flush()
|
|
if resultsfile:
|
|
print(line, file=resultsfile)
|
|
resultsfile.flush()
|
|
|
|
def run_benchmarks(configs, srcs, hardware, resultsfile=None):
|
|
hasheader = False
|
|
benches = collections.deque([(src, config, FLAGS.max_stddev)
|
|
for src in srcs
|
|
for config in configs])
|
|
while benches:
|
|
try:
|
|
with hardware:
|
|
SKPBench.run_warmup(hardware.warmup_time, configs[0])
|
|
if not hasheader:
|
|
emit_result(SKPBench.get_header(), resultsfile)
|
|
hasheader = True
|
|
while benches:
|
|
benchargs = benches.popleft()
|
|
with SKPBench(*benchargs) as skpbench:
|
|
try:
|
|
skpbench.execute(hardware)
|
|
if skpbench.best_result:
|
|
emit_result(skpbench.best_result.format(FLAGS.suffix),
|
|
resultsfile)
|
|
else:
|
|
print("WARNING: no result for %s with config %s" %
|
|
(skpbench.src, skpbench.config), file=sys.stderr)
|
|
|
|
except StddevException:
|
|
retry_max_stddev = skpbench.max_stddev * math.sqrt(2)
|
|
if FLAGS.verbosity >= 1:
|
|
print("stddev is too high for %s/%s (%s%%, max=%.2f%%), "
|
|
"re-queuing with max=%.2f%%." %
|
|
(skpbench.best_result.config, skpbench.best_result.bench,
|
|
skpbench.best_result.stddev, skpbench.max_stddev,
|
|
retry_max_stddev),
|
|
file=sys.stderr)
|
|
benches.append((skpbench.src, skpbench.config, retry_max_stddev,
|
|
skpbench.best_result))
|
|
|
|
except HardwareException as exception:
|
|
skpbench.terminate()
|
|
if FLAGS.verbosity >= 4:
|
|
hardware.print_debug_diagnostics()
|
|
if FLAGS.verbosity >= 1:
|
|
print("%s; rebooting and taking a %i second nap..." %
|
|
(exception.message, exception.sleeptime), file=sys.stderr)
|
|
benches.appendleft(benchargs) # retry the same bench next time.
|
|
raise # wake hw up from benchmarking mode before the nap.
|
|
|
|
except HardwareException as exception:
|
|
time.sleep(exception.sleeptime)
|
|
|
|
def main():
|
|
# Delimiter is ',' or ' ', skip if nested inside parens (e.g. gpu(a=b,c=d)).
|
|
DELIMITER = r'[, ](?!(?:[^(]*\([^)]*\))*[^()]*\))'
|
|
configs = re.split(DELIMITER, FLAGS.config)
|
|
srcs = _path.find_skps(FLAGS.srcs)
|
|
assert srcs
|
|
|
|
|
|
if FLAGS.adb:
|
|
adb = Adb(FLAGS.device_serial, FLAGS.adb_binary,
|
|
echo=(FLAGS.verbosity >= 5))
|
|
from _hardware_android import HardwareAndroid
|
|
|
|
model = adb.check('getprop ro.product.model').strip()
|
|
if model == 'Pixel C':
|
|
from _hardware_pixel_c import HardwarePixelC
|
|
hardware = HardwarePixelC(adb)
|
|
elif model == 'Pixel' or model == "Pixel XL":
|
|
from _hardware_pixel import HardwarePixel
|
|
hardware = HardwarePixel(adb)
|
|
elif model == 'Pixel 2':
|
|
from _hardware_pixel2 import HardwarePixel2
|
|
hardware = HardwarePixel2(adb)
|
|
elif model == 'Nexus 6P':
|
|
from _hardware_nexus_6p import HardwareNexus6P
|
|
hardware = HardwareNexus6P(adb)
|
|
else:
|
|
print("WARNING: %s: don't know how to monitor this hardware; results "
|
|
"may be unreliable." % model, file=sys.stderr)
|
|
hardware = HardwareAndroid(adb)
|
|
|
|
if FLAGS.lock_clocks:
|
|
hardware.__enter__()
|
|
print("Entered benchmarking mode, not running benchmarks. Reboot to restore.");
|
|
return;
|
|
|
|
if FLAGS.clock_speed:
|
|
hardware.setDesiredClock(FLAGS.clock_speed)
|
|
else:
|
|
hardware = Hardware()
|
|
|
|
if FLAGS.resultsfile:
|
|
with open(FLAGS.resultsfile, mode='a+') as resultsfile:
|
|
run_benchmarks(configs, srcs, hardware, resultsfile=resultsfile)
|
|
else:
|
|
run_benchmarks(configs, srcs, hardware)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|