1b53106907
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>
315 lines
11 KiB
Python
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))
|
|
)
|