skia2/infra/bots/recipes/perf_skottietrace.py
Ravi Mistry 1b53106907 New perf-skottietrace recipe
The recipe runs DM on lottie files with tracing enabled.
DM will grab 25 samples evenly distributed across the timeline and the trace
will be outputted with Animation entry points.
The output of the trace is then parsed and written into perf.json for
perf.skia.org ingestion.

Example resultant perf.json:
https://isolateserver.appspot.com/browse?namespace=default-gzip&digest=31f605ad3c9047f2889893654d57344502794371&as=perf.json

Recipe would run on android devices.

Bug: skia:8884
Change-Id: I70715febf2bfbd7b1f8fcbd872cb4709638eabd7
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/201472
Commit-Queue: Ravi Mistry <rmistry@google.com>
Reviewed-by: Eric Boren <borenet@google.com>
2019-03-21 16:46:03 +00:00

315 lines
11 KiB
Python

# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Recipe which runs DM with trace flag on lottie files and then parses the
# trace output into perf.json files to ingest to perf.skia.org.
# Design doc: go/skottie-tracing
import re
import string
DEPS = [
'flavor',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/json',
'recipe_engine/path',
'recipe_engine/step',
'recipe_engine/properties',
'recipe_engine/python',
'recipe_engine/raw_io',
'run',
'vars',
]
SEEK_TRACE_NAME = 'skottie::Animation::seek'
RENDER_TRACE_NAME = 'skottie::Animation::render'
EXPECTED_DM_FRAMES = 25
def perf_steps(api):
"""Run DM on lottie files with tracing turned on and then parse the output."""
api.flavor.create_clean_device_dir(
api.flavor.device_dirs.dm_dir)
lottie_files = api.file.listdir(
'list lottie files', api.flavor.host_dirs.lotties_dir,
test_data=['lottie1.json', 'lottie(test)\'!2.json', 'lottie 3!.json',
'LICENSE'])
perf_results = {}
push_dm = True
# Run DM on each lottie file and parse the trace files.
for idx, lottie_file in enumerate(lottie_files):
lottie_filename = api.path.basename(lottie_file)
if not lottie_filename.endswith('.json'):
continue
trace_output_path = api.flavor.device_dirs.dm_dir + '/%s.json' % (idx + 1)
# See go/skottie-tracing for how these flags were selected.
dm_args = [
'dm',
'--resourcePath', api.flavor.device_dirs.resource_dir,
'--lotties', api.flavor.device_dirs.lotties_dir,
'--nocpu',
'--config', 'gles',
'--src', 'lottie',
'--nonativeFonts',
'--verbose',
'--traceMatch', 'skottie', # recipe can OOM without this.
'--trace', trace_output_path,
'--match', get_trace_match(lottie_filename),
]
api.run(api.flavor.step, 'dm', cmd=dm_args, abort_on_failure=False,
skip_binary_push=not push_dm)
# We already pushed the binary once. No need to waste time by pushing
# the same binary for future runs.
push_dm = False
trace_test_data = api.properties.get('trace_test_data', '{}')
trace_file_content = api.flavor.read_file_on_device(trace_output_path)
if not trace_file_content and trace_test_data:
trace_file_content = trace_test_data
perf_results[lottie_filename] = parse_trace(
trace_file_content, lottie_filename, api)
# Construct contents of perf.json
perf_json = {
'gitHash': api.properties['revision'],
'swarming_bot_id': api.vars.swarming_bot_id,
'swarming_task_id': api.vars.swarming_task_id,
'key': {
'bench_type': 'tracing',
'source_type': 'skottie',
},
'results': {
'gles': perf_results,
},
}
if api.vars.is_trybot:
perf_json['issue'] = api.vars.issue
perf_json['patchset'] = api.vars.patchset
perf_json['patch_storage'] = api.vars.patch_storage
# Add tokens from the builder name to the key.
reg = re.compile('Perf-(?P<os>[A-Za-z0-9_]+)-'
'(?P<compiler>[A-Za-z0-9_]+)-'
'(?P<model>[A-Za-z0-9_]+)-GPU-'
'(?P<cpu_or_gpu_value>[A-Za-z0-9_]+)-'
'(?P<arch>[A-Za-z0-9_]+)-'
'(?P<configuration>[A-Za-z0-9_]+)-'
'All(-(?P<extra_config>[A-Za-z0-9_]+)|)')
m = reg.match(api.properties['buildername'])
keys = ['os', 'compiler', 'model', 'cpu_or_gpu_value', 'arch',
'configuration', 'extra_config']
for k in keys:
perf_json['key'][k] = m.group(k)
# Create the perf.json file in perf_data_dir for the Upload task to upload.
api.file.ensure_directory(
'makedirs perf_dir',
api.flavor.host_dirs.perf_data_dir)
api.run(
api.python.inline,
'write perf.json',
program="""import json
with open('%s', 'w') as outfile:
json.dump(obj=%s, fp=outfile, indent=4)
""" % (api.flavor.host_dirs.perf_data_dir.join('perf.json'), perf_json))
def get_trace_match(lottie_filename):
"""Returns the DM regex to match the specified lottie file name."""
trace_match = '^%s$' % lottie_filename
if ' ' not in trace_match:
# Punctuation characters confuse DM so escape them. Do not need to do this
# when there is a space in the match because subprocess.list2cmdline
# automatically adds quotes in that case.
for sp_char in string.punctuation:
if sp_char == '\\':
# No need to escape the escape char.
continue
trace_match = trace_match.replace(sp_char, '\%s' % sp_char)
return trace_match
def parse_trace(trace_json, lottie_filename, api):
"""parse_trace parses the specified trace JSON.
Parses the trace JSON and calculates the time of a single frame. Frame time is
considered the same as seek time + render time.
Note: The first seek is ignored because it is a constructor call.
A dictionary is returned that has the following structure:
{
'frame_max_us': 100,
'frame_min_us': 90,
'frame_avg_us': 95,
}
"""
step_result = api.run(
api.python.inline,
'parse %s trace' % lottie_filename,
program="""
import json
import sys
trace_output = sys.argv[1]
trace_json = json.loads(trace_output)
lottie_filename = sys.argv[2]
output_json_file = sys.argv[3]
perf_results = {}
frame_max = 0
frame_min = 0
frame_cumulative = 0
current_frame_duration = 0
total_frames = 0
frame_start = False
skipped_first_seek = False # Skip the first seek constructor call.
for trace in trace_json:
if '%s' in trace['name']:
if not skipped_first_seek:
skipped_first_seek = True
continue
if frame_start:
raise Exception('We got consecutive Animation::seek without a ' +
'render. Something is wrong.')
frame_start = True
current_frame_duration = trace['dur']
elif '%s' in trace['name']:
if not frame_start:
raise Exception('We got an Animation::render without a seek first. ' +
'Something is wrong.')
current_frame_duration += trace['dur']
frame_start = False
total_frames += 1
frame_max = max(frame_max, current_frame_duration)
frame_min = (min(frame_min, current_frame_duration)
if frame_min else current_frame_duration)
frame_cumulative += current_frame_duration
expected_dm_frames = %d
if total_frames != expected_dm_frames:
raise Exception(
'Got ' + str(total_frames) + ' frames instead of ' +
str(expected_dm_frames))
perf_results['frame_max_us'] = frame_max
perf_results['frame_min_us'] = frame_min
perf_results['frame_avg_us'] = frame_cumulative/total_frames
# Write perf_results to the output json.
with open(output_json_file, 'w') as f:
f.write(json.dumps(perf_results))
""" % (SEEK_TRACE_NAME, RENDER_TRACE_NAME, EXPECTED_DM_FRAMES),
args=[trace_json, lottie_filename, api.json.output()])
# Sanitize float outputs to 2 precision points.
output = dict(step_result.json.output)
output['frame_max_us'] = float("%.2f" % output['frame_max_us'])
output['frame_min_us'] = float("%.2f" % output['frame_min_us'])
output['frame_avg_us'] = float("%.2f" % output['frame_avg_us'])
return output
def RunSteps(api):
api.vars.setup()
api.file.ensure_directory('makedirs tmp_dir', api.vars.tmp_dir)
api.flavor.setup()
with api.context():
try:
api.flavor.install(resources=True, lotties=True)
perf_steps(api)
finally:
api.flavor.cleanup_steps()
api.run.check_failure()
def GenTests(api):
trace_output = """
[{"ph":"X","name":"void skottie::Animation::seek(SkScalar)","ts":452,"dur":2.57,"tid":1,"pid":0},{"ph":"X","name":"void SkCanvas::drawPaint(const SkPaint &)","ts":473,"dur":2.67e+03,"tid":1,"pid":0},{"ph":"X","name":"void skottie::Animation::seek(SkScalar)","ts":3.15e+03,"dur":2.25,"tid":1,"pid":0},{"ph":"X","name":"void skottie::Animation::render(SkCanvas *, const SkRect *, RenderFlags) const","ts":3.15e+03,"dur":216,"tid":1,"pid":0},{"ph":"X","name":"void SkCanvas::drawPath(const SkPath &, const SkPaint &)","ts":3.35e+03,"dur":15.1,"tid":1,"pid":0},{"ph":"X","name":"void skottie::Animation::seek(SkScalar)","ts":3.37e+03,"dur":1.17,"tid":1,"pid":0},{"ph":"X","name":"void skottie::Animation::render(SkCanvas *, const SkRect *, RenderFlags) const","ts":3.37e+03,"dur":140,"tid":1,"pid":0}]
"""
dm_json_test_data = """
{
"gitHash": "bac53f089dbc473862bc5a2e328ba7600e0ed9c4",
"swarming_bot_id": "skia-rpi-094",
"swarming_task_id": "438f11c0e19eab11",
"key": {
"arch": "arm",
"compiler": "Clang",
"cpu_or_gpu": "GPU",
"cpu_or_gpu_value": "Mali400MP2",
"extra_config": "Android",
"model": "AndroidOne",
"os": "Android"
},
"results": {
}
}
"""
parse_trace_json = {
'frame_avg_us': 179.71,
'frame_min_us': 141.17,
'frame_max_us': 218.25
}
buildername = ('Perf-Android-Clang-AndroidOne-GPU-Mali400MP2-arm-Release-'
'All-Android_SkottieTracing')
yield (
api.test(buildername) +
api.properties(buildername=buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
task_id='abc123',
trace_test_data=trace_output,
dm_json_test_data=dm_json_test_data,
path_config='kitchen',
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie(test)\'!2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie 3!.json trace',
api.json.output(parse_trace_json))
)
yield (
api.test('skottietracing_parse_trace_error') +
api.properties(buildername=buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
task_id='abc123',
trace_test_data=trace_output,
dm_json_test_data=dm_json_test_data,
path_config='kitchen',
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie 3!.json trace',
api.json.output(parse_trace_json), retcode=1)
)
yield (
api.test('skottietracing_trybot') +
api.properties(buildername=buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
task_id='abc123',
trace_test_data=trace_output,
dm_json_test_data=dm_json_test_data,
path_config='kitchen',
swarm_out_dir='[SWARM_OUT_DIR]',
patch_ref='89/456789/12',
patch_repo='https://skia.googlesource.com/skia.git',
patch_storage='gerrit',
patch_set=7,
patch_issue=1234,
gerrit_project='skia',
gerrit_url='https://skia-review.googlesource.com/') +
api.step_data('parse lottie(test)\'!2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie 3!.json trace',
api.json.output(parse_trace_json))
)