blob: 275a862fa5a11eacb06b3d9f135fe20f0e0bd750 [file] [log] [blame]
# 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 Skottie-WASM and Lottie-Web perf.
import calendar
import json
import re
PYTHON_VERSION_COMPATIBILITY = "PY3"
# trim
DEPS = [
'flavor',
'checkout',
'env',
'infra',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/json',
'recipe_engine/path',
'recipe_engine/properties',
'recipe_engine/python',
'recipe_engine/step',
'recipe_engine/time',
'run',
'vars',
]
LOTTIE_WEB_EXCLUDE = [
# See https://bugs.chromium.org/p/skia/issues/detail?id=9187#c4
'lottiefiles.com - Progress Success.json',
# Fails with "val2 is not defined".
'lottiefiles.com - VR.json',
'vr_animation.json',
# Times out.
'lottiefiles.com - Nudge.json',
'lottiefiles.com - Retweet.json',
# Trace file has majority main_frame_aborted terminations in it and < 25
# occurrences of submitted_frame + missed_frame.
# Static scenes (nothing animating)
'mask1.json',
'mask2.json',
'stacking.json',
]
SKOTTIE_WASM_EXCLUDE = [
# Trace file has majority main_frame_aborted terminations in it and < 25
# occurrences of submitted_frame + missed_frame.
# Below descriptions are added from fmalita@'s comments in
# https://skia-review.googlesource.com/c/skia/+/229419
# Static scenes (nothing animating)
'mask1.json',
'mask2.json',
'stacking.json',
# Static in Skottie only due to unsupported feature (expressions).
'dna.json',
'elephant_trunk_swing.json',
# Looks all static in both skottie/lottie, not sure why lottie doesn't abort
# as many frames.
'hexadots.json',
# Very short transition, mostly static.
'screenhole.json',
# Broken in Skottie due to unidentified missing feature.
'interleague_golf_logo.json',
'loading.json',
'lottiefiles.com - Loading 2.json',
'streetby_loading.json',
'streetby_test_loading.json',
# Times out
'beetle.json',
]
# These files work in SVG but not in Canvas.
LOTTIE_WEB_CANVAS_EXCLUDE = LOTTIE_WEB_EXCLUDE + [
'Hello World.json',
'interactive_menu.json',
'Name.json',
]
def RunSteps(api):
api.vars.setup()
api.flavor.setup(None)
checkout_root = api.path['start_dir']
buildername = api.properties['buildername']
node_path = api.path['start_dir'].join('node', 'node', 'bin', 'node')
lottie_files = api.file.listdir(
'list lottie files', api.flavor.host_dirs.lotties_dir,
test_data=['lottie1.json', 'lottie2.json', 'lottie3.json', 'LICENSE'])
if 'SkottieWASM' in buildername:
source_type = 'skottie'
renderer = 'skottie-wasm'
perf_app_dir = checkout_root.join('skia', 'tools', 'skottie-wasm-perf')
canvaskit_js_path = api.vars.build_dir.join('canvaskit.js')
canvaskit_wasm_path = api.vars.build_dir.join('canvaskit.wasm')
skottie_wasm_js_path = perf_app_dir.join('skottie-wasm-perf.js')
perf_app_cmd = [
node_path, skottie_wasm_js_path,
'--canvaskit_js', canvaskit_js_path,
'--canvaskit_wasm', canvaskit_wasm_path,
]
lottie_files = [x for x in lottie_files
if api.path.basename(x) not in SKOTTIE_WASM_EXCLUDE]
elif 'LottieWeb' in buildername:
source_type = 'lottie-web'
renderer = 'lottie-web'
if 'Canvas' in buildername:
backend = 'canvas'
lottie_files = [
x for x in lottie_files
if api.path.basename(x) not in LOTTIE_WEB_CANVAS_EXCLUDE]
else:
backend = 'svg'
lottie_files = [x for x in lottie_files
if api.path.basename(x) not in LOTTIE_WEB_EXCLUDE]
perf_app_dir = checkout_root.join('skia', 'tools', 'lottie-web-perf')
lottie_web_js_path = perf_app_dir.join('lottie-web-perf.js')
perf_app_cmd = [
node_path, lottie_web_js_path,
'--backend', backend,
]
else:
raise Exception('Could not recognize the buildername %s' % buildername)
if api.vars.builder_cfg.get('cpu_or_gpu') == 'GPU':
perf_app_cmd.append('--use_gpu')
# Install prerequisites.
env_prefixes = {'PATH': [api.path['start_dir'].join('node', 'node', 'bin')]}
with api.context(cwd=perf_app_dir, env_prefixes=env_prefixes):
api.step('npm install', cmd=['npm', 'install'])
perf_results = {}
output_dir = api.path.mkdtemp('g3_try')
# Run the perf_app_cmd on each lottie file and parse the trace files.
for _, lottie_file in enumerate(lottie_files):
lottie_filename = api.path.basename(lottie_file)
if not lottie_filename.endswith('.json'):
continue
output_file = output_dir.join(lottie_filename)
with api.context(cwd=perf_app_dir, env={'DISPLAY': ':0'}):
# This is occasionally flaky due to skbug.com/9207, adding retries.
attempts = 3
# Add output and input arguments to the cmd.
api.run.with_retry(api.step, 'Run perf cmd line app', attempts,
cmd=perf_app_cmd + [
'--input', lottie_file,
'--output', output_file,
], infra_step=True)
perf_results[lottie_filename] = {
'gl': parse_trace(output_file, lottie_filename, api, renderer),
}
# Construct contents of the output 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': source_type,
},
'renderer': renderer,
'results': 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_]+)-'
'(?P<cpu_or_gpu>[A-Z]+)-'
'(?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', 'cpu_or_gpu_value', 'arch',
'configuration', 'extra_config']
for k in keys:
perf_json['key'][k] = m.group(k)
# Create the output 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)
now = api.time.utcnow()
ts = int(calendar.timegm(now.utctimetuple()))
json_path = api.flavor.host_dirs.perf_data_dir.join(
'perf_%s_%d.json' % (api.properties['revision'], ts))
json_contents = json.dumps(
perf_json, indent=4, sort_keys=True, separators=(',', ': '))
api.file.write_text('write output JSON', json_path, json_contents)
def parse_trace(trace_json, lottie_filename, api, renderer):
"""parse_trace parses the specified trace JSON.
Parses the trace JSON and calculates the time of a single frame.
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]
with open(trace_output, 'r') as f:
trace_json = json.load(f)
output_json_file = sys.argv[2]
renderer = sys.argv[3] # Unused for now but might be useful in the future.
# Output data about the GPU that was used.
print('GPU data:')
print(trace_json['metadata'].get('gpu-gl-renderer'))
print(trace_json['metadata'].get('gpu-driver'))
print(trace_json['metadata'].get('gpu-gl-vendor'))
erroneous_termination_statuses = [
'replaced_by_new_reporter_at_same_stage',
'did_not_produce_frame',
]
accepted_termination_statuses = [
'missed_frame',
'submitted_frame',
'main_frame_aborted'
]
current_frame_duration = 0
total_frames = 0
frame_id_to_start_ts = {}
# Will contain tuples of frame_ids and their duration and status.
completed_frame_id_and_duration_status = []
# Will contain tuples of drawn frame_ids and their duration.
drawn_frame_id_and_duration = []
for trace in trace_json['traceEvents']:
if 'PipelineReporter' in trace['name']:
frame_id = trace['id']
args = trace.get('args')
if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':
frame_id_to_start_ts[frame_id] = trace['ts']
elif args and (args.get('termination_status') in
accepted_termination_statuses):
if not frame_id_to_start_ts.get(frame_id):
print('[No start ts found for %s]' % frame_id)
continue
current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]
total_frames += 1
completed_frame_id_and_duration_status.append(
(frame_id, current_frame_duration, args['termination_status']))
if(args['termination_status'] == 'missed_frame' or
args['termination_status'] == 'submitted_frame'):
drawn_frame_id_and_duration.append((frame_id, current_frame_duration))
# We are done with this frame_id so remove it from the dict.
frame_id_to_start_ts.pop(frame_id)
print('%d (%s with %s): %d' % (
total_frames, frame_id, args['termination_status'],
current_frame_duration))
elif args and (args.get('termination_status') in
erroneous_termination_statuses):
# Invalidate previously collected results for this frame_id.
if frame_id_to_start_ts.get(frame_id):
print('[Invalidating %s due to %s]' % (
frame_id, args['termination_status']))
frame_id_to_start_ts.pop(frame_id)
# Calculate metrics for total completed frames.
total_completed_frames = len(completed_frame_id_and_duration_status)
if total_completed_frames < 25:
raise Exception('Even with 3 loops found only %d frames' %
total_completed_frames)
# Get frame avg/min/max for the middle 25 frames.
start = (total_completed_frames - 25)/2
print('Got %d total completed frames. Using indexes [%d, %d).' % (
total_completed_frames, start, start+25))
frame_max = 0
frame_min = 0
frame_cumulative = 0
aborted_frames = 0
for frame_id, duration, status in (
completed_frame_id_and_duration_status[start:start+25]):
frame_max = max(frame_max, duration)
frame_min = min(frame_min, duration) if frame_min else duration
frame_cumulative += duration
if status == 'main_frame_aborted':
aborted_frames += 1
perf_results = {}
perf_results['frame_max_us'] = frame_max
perf_results['frame_min_us'] = frame_min
perf_results['frame_avg_us'] = frame_cumulative/25
perf_results['aborted_frames'] = aborted_frames
# Now calculate metrics for only drawn frames.
drawn_frame_max = 0
drawn_frame_min = 0
drawn_frame_cumulative = 0
total_drawn_frames = len(drawn_frame_id_and_duration)
if total_drawn_frames < 25:
raise Exception('Even with 3 loops found only %d drawn frames' %
total_drawn_frames)
# Get drawn frame avg/min/max from the middle 25 frames.
start = (total_drawn_frames - 25)/2
print('Got %d total drawn frames. Using indexes [%d-%d).' % (
total_drawn_frames, start, start+25))
for frame_id, duration in drawn_frame_id_and_duration[start:start+25]:
drawn_frame_max = max(drawn_frame_max, duration)
drawn_frame_min = (min(drawn_frame_min, duration)
if drawn_frame_min else duration)
drawn_frame_cumulative += duration
# Add metrics to perf_results.
perf_results['drawn_frame_max_us'] = drawn_frame_max
perf_results['drawn_frame_min_us'] = drawn_frame_min
perf_results['drawn_frame_avg_us'] = drawn_frame_cumulative/25
print('Final perf_results dict: %s' % perf_results)
# Write perf_results to the output json.
with open(output_json_file, 'w') as f:
f.write(json.dumps(perf_results))
""", args=[trace_json, api.json.output(), renderer])
# 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 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}]
"""
parse_trace_json = {
'frame_avg_us': 179.71,
'frame_min_us': 141.17,
'frame_max_us': 218.25
}
skottie_cpu_buildername = ('Perf-Debian10-EMCC-GCE-CPU-AVX2-wasm-Release-All-'
'SkottieWASM')
yield (
api.test('skottie_wasm_perf') +
api.properties(buildername=skottie_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
yield (
api.test('skottie_wasm_perf_trybot') +
api.properties(buildername=skottie_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
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 lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
skottie_gpu_buildername = ('Perf-Debian10-EMCC-NUC7i5BNK-GPU-IntelIris640-'
'wasm-Release-All-SkottieWASM')
yield (
api.test('skottie_wasm_perf_gpu') +
api.properties(buildername=skottie_gpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
lottieweb_cpu_buildername = ('Perf-Debian10-none-GCE-CPU-AVX2-x86_64-Release-'
'All-LottieWeb')
yield (
api.test('lottie_web_perf') +
api.properties(buildername=lottieweb_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
yield (
api.test('lottie_web_perf_trybot') +
api.properties(buildername=lottieweb_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
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 lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
lottieweb_canvas_cpu_buildername = (
'Perf-Debian10-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas')
yield (
api.test('lottie_web_canvas_perf') +
api.properties(buildername=lottieweb_canvas_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
swarm_out_dir='[SWARM_OUT_DIR]') +
api.step_data('parse lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
yield (
api.test('lottie_web_canvas_perf_trybot') +
api.properties(buildername=lottieweb_canvas_cpu_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
trace_test_data=trace_output,
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 lottie1.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie2.json trace',
api.json.output(parse_trace_json)) +
api.step_data('parse lottie3.json trace',
api.json.output(parse_trace_json))
)
unrecognized_buildername = ('Perf-Debian10-none-GCE-CPU-AVX2-x86_64-Release-'
'All-Unrecognized')
yield (
api.test('unrecognized_builder') +
api.properties(buildername=unrecognized_buildername,
repository='https://skia.googlesource.com/skia.git',
revision='abc123',
path_config='kitchen',
swarm_out_dir='[SWARM_OUT_DIR]') +
api.expect_exception('Exception')
)