diff --git a/infra/bots/jobs.json b/infra/bots/jobs.json
index fc4fe3fc68..989b4efac6 100644
--- a/infra/bots/jobs.json
+++ b/infra/bots/jobs.json
@@ -229,7 +229,9 @@
   "Perf-Debian9-EMCC-GCE-GPU-AVX2-wasm-Release-All-CanvasKit",
   "Perf-Debian9-EMCC-NUC7i5BNK-GPU-IntelIris640-wasm-Release-All-SkottieWASM",
   "Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb",
+  "Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas",
   "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb",
+  "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas",
   "Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All",
   "Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All-CommandBuffer",
   "Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All-Metal",
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf.json b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf.json
new file mode 100644
index 0000000000..d680edef95
--- /dev/null
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf.json
@@ -0,0 +1,591 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/cache/work"
+    ],
+    "infra_step": true,
+    "name": "makedirs checkout_path"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "remove",
+      "[START_DIR]/cache/work/.gclient_entries"
+    ],
+    "infra_step": true,
+    "name": "remove [START_DIR]/cache/work/.gclient_entries"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[depot_tools::bot_update]/resources/bot_update.py",
+      "--spec-path",
+      "cache_dir = '[START_DIR]/cache/git'\nsolutions = [{'deps_file': '.DEPS.git', 'managed': False, 'name': 'skia', 'url': 'https://skia.googlesource.com/skia.git'}]",
+      "--patch_root",
+      "skia",
+      "--revision_mapping_file",
+      "{\"got_revision\": \"skia\"}",
+      "--git-cache-dir",
+      "[START_DIR]/cache/git",
+      "--cleanup-dir",
+      "[CLEANUP]/bot_update",
+      "--output_json",
+      "/path/to/tmp/json",
+      "--revision",
+      "skia@abc123"
+    ],
+    "cwd": "[START_DIR]/cache/work",
+    "env_prefixes": {
+      "PATH": [
+        "RECIPE_REPO[depot_tools]"
+      ]
+    },
+    "infra_step": true,
+    "name": "bot_update",
+    "~followup_annotations": [
+      "@@@STEP_TEXT@Some step text@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"did_run\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"fixed_revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": \"abc123\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"repository\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_failure\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"properties\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision_cp\": \"refs/heads/master@{#164710}\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"source_manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"directories\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"git_checkout\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"repo_url\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"version\": 0@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"step_text\": \"Some step text\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision_cp@\"refs/heads/master@{#164710}\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "listdir",
+      "[START_DIR]/lottie-samples"
+    ],
+    "infra_step": true,
+    "name": "list lottie files",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/LICENSE@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie1.json@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie2.json@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie3.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "npm",
+      "install"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env_prefixes": {
+      "PATH": [
+        "[START_DIR]/node/node/bin"
+      ]
+    },
+    "name": "npm install"
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie1.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie1.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie1.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie1.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie2.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie2.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app (2)"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie2.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie2.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie3.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie3.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app (3)"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie3.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie3.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/g3_try_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "rmtree [CLEANUP]/g3_try_tmp_1"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_BOT_ID', '')\n"
+    ],
+    "name": "get swarming bot id",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_BOT_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_TASK_ID', '')\n"
+    ],
+    "name": "get swarming task id",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_TASK_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "makedirs perf_dir"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import json\nwith open('[START_DIR]/[SWARM_OUT_DIR]/perf_abc123_1337000001.json', 'w') as outfile:\n  json.dump(obj={'gitHash': 'abc123', 'results': {'lottie3.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie1.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie2.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}}, 'swarming_task_id': '', 'renderer': 'lottie-web', 'key': {'extra_config': 'LottieWeb_Canvas', 'bench_type': 'tracing', 'cpu_or_gpu_value': 'AVX2', 'arch': 'x86_64', 'source_type': 'lottie-web', 'cpu_or_gpu': 'CPU', 'model': 'GCE', 'configuration': 'Release', 'os': 'Debian9', 'compiler': 'none'}, 'swarming_bot_id': ''}, fp=outfile, indent=4)\n"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "write output JSON",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open('[START_DIR]/[SWARM_OUT_DIR]/perf_abc123_1337000001.json', 'w') as outfile:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  json.dump(obj={'gitHash': 'abc123', 'results': {'lottie3.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie1.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie2.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}}, 'swarming_task_id': '', 'renderer': 'lottie-web', 'key': {'extra_config': 'LottieWeb_Canvas', 'bench_type': 'tracing', 'cpu_or_gpu_value': 'AVX2', 'arch': 'x86_64', 'source_type': 'lottie-web', 'cpu_or_gpu': 'CPU', 'model': 'GCE', 'configuration': 'Release', 'os': 'Debian9', 'compiler': 'none'}, 'swarming_bot_id': ''}, fp=outfile, indent=4)@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf_trybot.json b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf_trybot.json
new file mode 100644
index 0000000000..924060a408
--- /dev/null
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf_trybot.json
@@ -0,0 +1,593 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/cache/work"
+    ],
+    "infra_step": true,
+    "name": "makedirs checkout_path"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "remove",
+      "[START_DIR]/cache/work/.gclient_entries"
+    ],
+    "infra_step": true,
+    "name": "remove [START_DIR]/cache/work/.gclient_entries"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[depot_tools::bot_update]/resources/bot_update.py",
+      "--spec-path",
+      "cache_dir = '[START_DIR]/cache/git'\nsolutions = [{'deps_file': '.DEPS.git', 'managed': False, 'name': 'skia', 'url': 'https://skia.googlesource.com/skia.git'}]",
+      "--patch_root",
+      "skia",
+      "--revision_mapping_file",
+      "{\"got_revision\": \"skia\"}",
+      "--git-cache-dir",
+      "[START_DIR]/cache/git",
+      "--cleanup-dir",
+      "[CLEANUP]/bot_update",
+      "--output_json",
+      "/path/to/tmp/json",
+      "--patch_ref",
+      "https://skia.googlesource.com/skia.git@abc123:89/456789/12",
+      "--revision",
+      "skia@abc123"
+    ],
+    "cwd": "[START_DIR]/cache/work",
+    "env_prefixes": {
+      "PATH": [
+        "RECIPE_REPO[depot_tools]"
+      ]
+    },
+    "infra_step": true,
+    "name": "bot_update",
+    "~followup_annotations": [
+      "@@@STEP_TEXT@Some step text@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"did_run\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"fixed_revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": \"abc123\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"repository\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_failure\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"properties\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision_cp\": \"refs/heads/master@{#164710}\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"source_manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"directories\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"git_checkout\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"repo_url\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"version\": 0@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"step_text\": \"Some step text\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision_cp@\"refs/heads/master@{#164710}\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "listdir",
+      "[START_DIR]/lottie-samples"
+    ],
+    "infra_step": true,
+    "name": "list lottie files",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/LICENSE@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie1.json@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie2.json@@@",
+      "@@@STEP_LOG_LINE@listdir@[START_DIR]/lottie-samples/lottie3.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "npm",
+      "install"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env_prefixes": {
+      "PATH": [
+        "[START_DIR]/node/node/bin"
+      ]
+    },
+    "name": "npm install"
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie1.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie1.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie1.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie1.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie2.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie2.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app (2)"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie2.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie2.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/node/node/bin/node",
+      "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "canvas",
+      "--input",
+      "[START_DIR]/lottie-samples/lottie3.json",
+      "--output",
+      "[CLEANUP]/g3_try_tmp_1/lottie3.json"
+    ],
+    "cwd": "[START_DIR]/cache/work/skia/tools/lottie-web-perf",
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "DISPLAY": ":0",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "Run perf cmd line app (3)"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport json\nimport sys\n\ntrace_output = sys.argv[1]\nwith open(trace_output, 'r') as f:\n  trace_json = json.load(f)\noutput_json_file = sys.argv[2]\nrenderer = sys.argv[3]  # Unused for now but might be useful in the future.\n\n# Output data about the GPU that was used.\nprint 'GPU data:'\nprint trace_json['metadata'].get('gpu-gl-renderer')\nprint trace_json['metadata'].get('gpu-driver')\nprint trace_json['metadata'].get('gpu-gl-vendor')\n\nerroneous_termination_statuses = [\n    'replaced_by_new_reporter_at_same_stage',\n    'did_not_produce_frame',\n]\naccepted_termination_statuses = [\n    'missed_frame',\n    'submitted_frame',\n    'main_frame_aborted'\n]\n\ncurrent_frame_duration = 0\ntotal_frames = 0\nframe_id_to_start_ts = {}\n# Will contain tuples of frame_ids and their duration.\ncompleted_frame_id_and_duration = []\nfor trace in trace_json['traceEvents']:\n  if 'PipelineReporter' in trace['name']:\n    frame_id = trace['id']\n    args = trace.get('args')\n    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':\n      frame_id_to_start_ts[frame_id] = trace['ts']\n    elif args and (args.get('termination_status') in\n                   accepted_termination_statuses):\n      if not frame_id_to_start_ts.get(frame_id):\n        print '[No start ts found for %s]' % frame_id\n        continue\n      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]\n      total_frames += 1\n      completed_frame_id_and_duration.append(\n          (frame_id, current_frame_duration))\n      # We are done with this frame_id so remove it from the dict.\n      frame_id_to_start_ts.pop(frame_id)\n      print '%d (%s with %s): %d' % (\n          total_frames, frame_id, args['termination_status'],\n          current_frame_duration)\n    elif args and (args.get('termination_status') in\n                   erroneous_termination_statuses):\n      # Invalidate previously collected results for this frame_id.\n      if frame_id_to_start_ts.get(frame_id):\n        print '[Invalidating %s due to %s]' % (\n            frame_id, args['termination_status'])\n        frame_id_to_start_ts.pop(frame_id)\n\ntotal_completed_frames = len(completed_frame_id_and_duration)\nif total_completed_frames < 25:\n  raise Exception('Even with 2 loops found only %d frames' %\n                  total_completed_frames)\n\n# Get frame avg/min/max for the middle 25 frames.\nstart = (total_completed_frames - 25)/2\nprint 'Got %d total completed frames. Using start_index of %d.' % (\n    total_completed_frames, start)\nframe_max = 0\nframe_min = 0\nframe_cumulative = 0\nfor frame_id, duration in completed_frame_id_and_duration[start:start+25]:\n  frame_max = max(frame_max, duration)\n  frame_min = min(frame_min, duration) if frame_min else duration\n  frame_cumulative += duration\n\nperf_results = {}\nperf_results['frame_max_us'] = frame_max\nperf_results['frame_min_us'] = frame_min\nperf_results['frame_avg_us'] = frame_cumulative/25\nprint 'For 25 frames got: %s' % perf_results\n\n# Write perf_results to the output json.\nwith open(output_json_file, 'w') as f:\n  f.write(json.dumps(perf_results))\n",
+      "[CLEANUP]/g3_try_tmp_1/lottie3.json",
+      "/path/to/tmp/json",
+      "lottie-web"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "parse lottie3.json trace",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_avg_us\": 179.71, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_max_us\": 218.25, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"frame_min_us\": 141.17@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@trace_output = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(trace_output, 'r') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  trace_json = json.load(f)@@@",
+      "@@@STEP_LOG_LINE@python.inline@output_json_file = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@renderer = sys.argv[3]  # Unused for now but might be useful in the future.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Output data about the GPU that was used.@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'GPU data:'@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-renderer')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-driver')@@@",
+      "@@@STEP_LOG_LINE@python.inline@print trace_json['metadata'].get('gpu-gl-vendor')@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@erroneous_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'replaced_by_new_reporter_at_same_stage',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'did_not_produce_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@accepted_termination_statuses = [@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'missed_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'submitted_frame',@@@",
+      "@@@STEP_LOG_LINE@python.inline@    'main_frame_aborted'@@@",
+      "@@@STEP_LOG_LINE@python.inline@]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@current_frame_duration = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_frames = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_id_to_start_ts = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Will contain tuples of frame_ids and their duration.@@@",
+      "@@@STEP_LOG_LINE@python.inline@completed_frame_id_and_duration = []@@@",
+      "@@@STEP_LOG_LINE@python.inline@for trace in trace_json['traceEvents']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if 'PipelineReporter' in trace['name']:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    frame_id = trace['id']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    args = trace.get('args')@@@",
+      "@@@STEP_LOG_LINE@python.inline@    if args and args.get('step') == 'BeginImplFrameToSendBeginMainFrame':@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts[frame_id] = trace['ts']@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   accepted_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if not frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[No start ts found for %s]' % frame_id@@@",
+      "@@@STEP_LOG_LINE@python.inline@        continue@@@",
+      "@@@STEP_LOG_LINE@python.inline@      current_frame_duration = trace['ts'] - frame_id_to_start_ts[frame_id]@@@",
+      "@@@STEP_LOG_LINE@python.inline@      total_frames += 1@@@",
+      "@@@STEP_LOG_LINE@python.inline@      completed_frame_id_and_duration.append(@@@",
+      "@@@STEP_LOG_LINE@python.inline@          (frame_id, current_frame_duration))@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # We are done with this frame_id so remove it from the dict.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@      print '%d (%s with %s): %d' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@          total_frames, frame_id, args['termination_status'],@@@",
+      "@@@STEP_LOG_LINE@python.inline@          current_frame_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@    elif args and (args.get('termination_status') in@@@",
+      "@@@STEP_LOG_LINE@python.inline@                   erroneous_termination_statuses):@@@",
+      "@@@STEP_LOG_LINE@python.inline@      # Invalidate previously collected results for this frame_id.@@@",
+      "@@@STEP_LOG_LINE@python.inline@      if frame_id_to_start_ts.get(frame_id):@@@",
+      "@@@STEP_LOG_LINE@python.inline@        print '[Invalidating %s due to %s]' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@            frame_id, args['termination_status'])@@@",
+      "@@@STEP_LOG_LINE@python.inline@        frame_id_to_start_ts.pop(frame_id)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@total_completed_frames = len(completed_frame_id_and_duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@if total_completed_frames < 25:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  raise Exception('Even with 2 loops found only %d frames' %@@@",
+      "@@@STEP_LOG_LINE@python.inline@                  total_completed_frames)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Get frame avg/min/max for the middle 25 frames.@@@",
+      "@@@STEP_LOG_LINE@python.inline@start = (total_completed_frames - 25)/2@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'Got %d total completed frames. Using start_index of %d.' % (@@@",
+      "@@@STEP_LOG_LINE@python.inline@    total_completed_frames, start)@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_max = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_min = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@frame_cumulative = 0@@@",
+      "@@@STEP_LOG_LINE@python.inline@for frame_id, duration in completed_frame_id_and_duration[start:start+25]:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_max = max(frame_max, duration)@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_min = min(frame_min, duration) if frame_min else duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@  frame_cumulative += duration@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results = {}@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_max_us'] = frame_max@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_min_us'] = frame_min@@@",
+      "@@@STEP_LOG_LINE@python.inline@perf_results['frame_avg_us'] = frame_cumulative/25@@@",
+      "@@@STEP_LOG_LINE@python.inline@print 'For 25 frames got: %s' % perf_results@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Write perf_results to the output json.@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open(output_json_file, 'w') as f:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  f.write(json.dumps(perf_results))@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "[CLEANUP]/g3_try_tmp_1"
+    ],
+    "infra_step": true,
+    "name": "rmtree [CLEANUP]/g3_try_tmp_1"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_BOT_ID', '')\n"
+    ],
+    "name": "get swarming bot id",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_BOT_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_TASK_ID', '')\n"
+    ],
+    "name": "get swarming task id",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_TASK_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "makedirs perf_dir"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import json\nwith open('[START_DIR]/[SWARM_OUT_DIR]/perf_abc123_1337000001.json', 'w') as outfile:\n  json.dump(obj={'gitHash': 'abc123', 'results': {'lottie3.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie1.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie2.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}}, 'patch_storage': 'gerrit', 'swarming_task_id': '', 'renderer': 'lottie-web', 'key': {'extra_config': 'LottieWeb_Canvas', 'bench_type': 'tracing', 'cpu_or_gpu_value': 'AVX2', 'arch': 'x86_64', 'source_type': 'lottie-web', 'cpu_or_gpu': 'CPU', 'model': 'GCE', 'configuration': 'Release', 'os': 'Debian9', 'compiler': 'none'}, 'swarming_bot_id': '', 'patchset': 7, 'issue': 1234}, fp=outfile, indent=4)\n"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "name": "write output JSON",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import json@@@",
+      "@@@STEP_LOG_LINE@python.inline@with open('[START_DIR]/[SWARM_OUT_DIR]/perf_abc123_1337000001.json', 'w') as outfile:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  json.dump(obj={'gitHash': 'abc123', 'results': {'lottie3.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie1.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}, 'lottie2.json': {'gl': {'frame_avg_us': 179.71, 'frame_max_us': 218.25, 'frame_min_us': 141.17}}}, 'patch_storage': 'gerrit', 'swarming_task_id': '', 'renderer': 'lottie-web', 'key': {'extra_config': 'LottieWeb_Canvas', 'bench_type': 'tracing', 'cpu_or_gpu_value': 'AVX2', 'arch': 'x86_64', 'source_type': 'lottie-web', 'cpu_or_gpu': 'CPU', 'model': 'GCE', 'configuration': 'Release', 'os': 'Debian9', 'compiler': 'none'}, 'swarming_bot_id': '', 'patchset': 7, 'issue': 1234}, fp=outfile, indent=4)@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf.json b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf.json
index 6ac4bfc4d9..e0f3b96dd1 100644
--- a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf.json
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf.json
@@ -130,6 +130,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie1.json",
       "--output",
@@ -257,6 +259,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie2.json",
       "--output",
@@ -384,6 +388,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie3.json",
       "--output",
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf_trybot.json b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf_trybot.json
index 6eaaabdf51..2ff7488a8f 100644
--- a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf_trybot.json
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_perf_trybot.json
@@ -132,6 +132,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie1.json",
       "--output",
@@ -259,6 +261,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie2.json",
       "--output",
@@ -386,6 +390,8 @@
     "cmd": [
       "[START_DIR]/node/node/bin/node",
       "[START_DIR]/cache/work/skia/tools/lottie-web-perf/lottie-web-perf.js",
+      "--backend",
+      "svg",
       "--input",
       "[START_DIR]/lottie-samples/lottie3.json",
       "--output",
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/unrecognized_builder.json b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/unrecognized_builder.json
index 3c3b5f1a41..a76d5a6219 100644
--- a/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/unrecognized_builder.json
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/unrecognized_builder.json
@@ -129,7 +129,7 @@
       "    arg_names, **additional_args)",
       "  File \"RECIPE_REPO[recipe_engine]/recipe_engine/internal/property_invoker.py\", in _invoke_with_properties",
       "    return callable_obj(*props, **additional_args)",
-      "  File \"RECIPE_REPO[skia]/infra/bots/recipes/perf_skottiewasm_lottieweb.py\", line 77, in RunSteps",
+      "  File \"RECIPE_REPO[skia]/infra/bots/recipes/perf_skottiewasm_lottieweb.py\", line 94, in RunSteps",
       "    raise Exception('Could not recognize the buildername %s' % buildername)",
       "Exception: Could not recognize the buildername Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-Unrecognized"
     ]
diff --git a/infra/bots/recipes/perf_skottiewasm_lottieweb.py b/infra/bots/recipes/perf_skottiewasm_lottieweb.py
index cad9620561..989d60abe3 100644
--- a/infra/bots/recipes/perf_skottiewasm_lottieweb.py
+++ b/infra/bots/recipes/perf_skottiewasm_lottieweb.py
@@ -39,6 +39,13 @@ LOTTIE_WEB_BLACKLIST = [
   'lottiefiles.com - Retweet.json',
 ]
 
+# These files work in SVG but not in Canvas.
+LOTTIE_WEB_CANVAS_BLACKLIST = LOTTIE_WEB_BLACKLIST + [
+  'Hello World.json',
+  'interactive_menu.json',
+  'Name.json',
+]
+
 
 def RunSteps(api):
   api.vars.setup()
@@ -67,12 +74,22 @@ def RunSteps(api):
   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_BLACKLIST]
+    else:
+      backend = 'svg'
+      lottie_files = [x for x in lottie_files
+                      if api.path.basename(x) not in LOTTIE_WEB_BLACKLIST]
 
     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]
-    lottie_files = [x for x in lottie_files
-                    if api.path.basename(x) not in LOTTIE_WEB_BLACKLIST]
+    perf_app_cmd = [
+        node_path, lottie_web_js_path,
+        '--backend', backend,
+    ]
   else:
     raise Exception('Could not recognize the buildername %s' % buildername)
 
@@ -372,6 +389,46 @@ def GenTests(api):
                     api.json.output(parse_trace_json))
   )
 
+  lottieweb_canvas_cpu_buildername = (
+      'Perf-Debian9-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-Debian9-none-GCE-CPU-AVX2-x86_64-Release-'
                               'All-Unrecognized')
   yield (
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index b9216b9da6..6ac4d08eb4 100755
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -1162,11 +1162,21 @@
         "Upload-Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb"
       ]
     },
+    "Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas": {
+      "tasks": [
+        "Upload-Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas"
+      ]
+    },
     "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb": {
       "tasks": [
         "Upload-Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb"
       ]
     },
+    "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas": {
+      "tasks": [
+        "Upload-Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas"
+      ]
+    },
     "Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All": {
       "tasks": [
         "Upload-Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All"
@@ -19495,6 +19505,108 @@
         "perf"
       ]
     },
+    "Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:d8f38ca9494b5af249942631f9cee45927f6b4bc"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:f96db4b66034c859090be3c47eb38227277f228b"
+        },
+        {
+          "name": "skia/bots/skimage",
+          "path": "skimage",
+          "version": "version:39"
+        },
+        {
+          "name": "skia/bots/skp",
+          "path": "skp",
+          "version": "version:196"
+        },
+        {
+          "name": "skia/bots/svg",
+          "path": "svg",
+          "version": "version:9"
+        },
+        {
+          "name": "skia/bots/node",
+          "path": "node",
+          "version": "version:1"
+        },
+        {
+          "name": "skia/bots/lottie-samples",
+          "path": "lottie-samples",
+          "version": "version:1"
+        },
+        {
+          "name": "infra/git/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "version:2.17.1.chromium15"
+        },
+        {
+          "name": "infra/tools/git/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:c9c8a52bfeaf8bc00ece22fdfd447822c8fcad77"
+        },
+        {
+          "name": "infra/tools/luci/git-credential-luci/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        }
+      ],
+      "command": [
+        "cipd_bin_packages/vpython${EXECUTABLE_SUFFIX}",
+        "skia/infra/bots/run_recipe.py",
+        "${ISOLATED_OUTDIR}",
+        "perf_skottiewasm_lottieweb",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"perf\",\"task_id\":\"<(TASK_ID)\"}",
+        "skia"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "machine_type:n1-standard-16",
+        "os:Debian-9.8",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "cache/vpython"
+        ]
+      },
+      "execution_timeout_ns": 14400000000000,
+      "expiration_ns": 72000000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/${SWARMING_TASK_ID}/+/annotations"
+      },
+      "io_timeout_ns": 14400000000000,
+      "isolate": "perf_skia_bundled.isolate",
+      "max_attempts": 2,
+      "outputs": [
+        "perf"
+      ]
+    },
     "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb": {
       "caches": [
         {
@@ -19601,6 +19713,112 @@
         "perf"
       ]
     },
+    "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:d8f38ca9494b5af249942631f9cee45927f6b4bc"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:f96db4b66034c859090be3c47eb38227277f228b"
+        },
+        {
+          "name": "skia/bots/skimage",
+          "path": "skimage",
+          "version": "version:39"
+        },
+        {
+          "name": "skia/bots/skp",
+          "path": "skp",
+          "version": "version:196"
+        },
+        {
+          "name": "skia/bots/svg",
+          "path": "svg",
+          "version": "version:9"
+        },
+        {
+          "name": "skia/bots/mesa_intel_driver_linux",
+          "path": "mesa_intel_driver_linux",
+          "version": "version:5"
+        },
+        {
+          "name": "skia/bots/node",
+          "path": "node",
+          "version": "version:1"
+        },
+        {
+          "name": "skia/bots/lottie-samples",
+          "path": "lottie-samples",
+          "version": "version:1"
+        },
+        {
+          "name": "infra/git/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "version:2.17.1.chromium15"
+        },
+        {
+          "name": "infra/tools/git/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:c9c8a52bfeaf8bc00ece22fdfd447822c8fcad77"
+        },
+        {
+          "name": "infra/tools/luci/git-credential-luci/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        }
+      ],
+      "command": [
+        "cipd_bin_packages/vpython${EXECUTABLE_SUFFIX}",
+        "skia/infra/bots/run_recipe.py",
+        "${ISOLATED_OUTDIR}",
+        "perf_skottiewasm_lottieweb",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"perf\",\"task_id\":\"<(TASK_ID)\"}",
+        "skia"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes"
+      ],
+      "dimensions": [
+        "gpu:8086:5926",
+        "os:Debian-9.4",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "cache/vpython"
+        ]
+      },
+      "execution_timeout_ns": 14400000000000,
+      "expiration_ns": 72000000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/${SWARMING_TASK_ID}/+/annotations"
+      },
+      "io_timeout_ns": 14400000000000,
+      "isolate": "perf_skia_bundled.isolate",
+      "max_attempts": 2,
+      "outputs": [
+        "perf"
+      ]
+    },
     "Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All": {
       "caches": [
         {
@@ -52632,6 +52850,72 @@
       "max_attempts": 2,
       "service_account": "skia-external-nano-uploader@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "Upload-Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:d8f38ca9494b5af249942631f9cee45927f6b4bc"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:f96db4b66034c859090be3c47eb38227277f228b"
+        },
+        {
+          "name": "infra/gsutil",
+          "path": "cipd_bin_packages",
+          "version": "version:4.28"
+        }
+      ],
+      "command": [
+        "cipd_bin_packages/vpython${EXECUTABLE_SUFFIX}",
+        "skia/infra/bots/run_recipe.py",
+        "${ISOLATED_OUTDIR}",
+        "upload_nano_results",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas\",\"gs_bucket\":\"skia-perf\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"output_ignored\",\"task_id\":\"<(TASK_ID)\"}",
+        "skia"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes",
+        "Perf-Debian9-none-GCE-CPU-AVX2-x86_64-Release-All-LottieWeb_Canvas"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-highmem-2",
+        "os:Debian-9.8",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "cache/vpython"
+        ]
+      },
+      "execution_timeout_ns": 3600000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/${SWARMING_TASK_ID}/+/annotations"
+      },
+      "io_timeout_ns": 3600000000000,
+      "isolate": "swarm_recipe.isolate",
+      "max_attempts": 2,
+      "service_account": "skia-external-nano-uploader@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "Upload-Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb": {
       "caches": [
         {
@@ -52698,6 +52982,72 @@
       "max_attempts": 2,
       "service_account": "skia-external-nano-uploader@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "Upload-Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:d8f38ca9494b5af249942631f9cee45927f6b4bc"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:2c805f1c716f6c5ad2126b27ec88b8585a09481e"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:f96db4b66034c859090be3c47eb38227277f228b"
+        },
+        {
+          "name": "infra/gsutil",
+          "path": "cipd_bin_packages",
+          "version": "version:4.28"
+        }
+      ],
+      "command": [
+        "cipd_bin_packages/vpython${EXECUTABLE_SUFFIX}",
+        "skia/infra/bots/run_recipe.py",
+        "${ISOLATED_OUTDIR}",
+        "upload_nano_results",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas\",\"gs_bucket\":\"skia-perf\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"output_ignored\",\"task_id\":\"<(TASK_ID)\"}",
+        "skia"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes",
+        "Perf-Debian9-none-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-LottieWeb_Canvas"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-highmem-2",
+        "os:Debian-9.8",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "cache/vpython"
+        ]
+      },
+      "execution_timeout_ns": 3600000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/${SWARMING_TASK_ID}/+/annotations"
+      },
+      "io_timeout_ns": 3600000000000,
+      "isolate": "swarm_recipe.isolate",
+      "max_attempts": 2,
+      "service_account": "skia-external-nano-uploader@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "Upload-Perf-Mac10.13-Clang-MacBook10.1-GPU-IntelHD615-x86_64-Release-All": {
       "caches": [
         {
diff --git a/tools/lottie-web-perf/lottie-web-canvas-perf.html b/tools/lottie-web-perf/lottie-web-canvas-perf.html
new file mode 100644
index 0000000000..746c0bcfed
--- /dev/null
+++ b/tools/lottie-web-perf/lottie-web-canvas-perf.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Lottie-Web Perf</title>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=egde,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <script src="/res/lottie.js" type="text/javascript" charset="utf-8"></script>
+    <style type="text/css" media="screen">
+      body {
+        margin: 0;
+        padding: 0;
+      }
+    </style>
+</head>
+<body>
+  <main>
+    <canvas id=canvas width=1000 height=1000 style="height: 1000px; width: 1000px;"></canvas>
+  </main>
+  <script type="text/javascript" charset="utf-8">
+    (function () {
+      const PATH = '/res/lottie.json';
+      const RENDERER = 'canvas';
+      const MAX_FRAMES = 25;
+      const MAX_LOOPS = 3;
+
+      const cvs = document.getElementById("canvas");
+      const canvasContext = cvs.getContext('2d');
+
+      // Get total number of frames of the animation from the hash.
+      const hash = window.location.hash;
+      const totalFrames = hash.slice(1);
+      console.log("Lottie has " + totalFrames + "total frames");
+
+      // Load the animation with autoplay false. We will control which
+      // frame to seek to and then will measure performance.
+      let anim = lottie.loadAnimation({
+        container: document.querySelector('.anim'),
+        renderer: RENDERER,
+        loop: false,
+        autoplay: false,
+        path: PATH,
+        rendererSettings: {
+          context: canvasContext,
+          scaleMode: 'noScale',
+          clearCanvas: true,
+          preserveAspectRatio:'xMidYMid meet',
+        },
+      });
+
+      const t_rate = 1.0 / (MAX_FRAMES - 1);
+      let frame = 0;
+      let loop = 0;
+      const drawFrame = () => {
+        if (frame >= MAX_FRAMES) {
+          // Reached the end of one loop.
+          loop++;
+          if (loop == MAX_LOOPS) {
+            // These are global variables to talk with puppeteer.
+            window._lottieWebDone = true;
+            return;
+          }
+          // Reset frame to restart the loop.
+          frame = 0;
+        }
+
+        let t1 = Math.max(Math.min(t_rate * frame, 1.0), 0.0);
+        let seekToFrame = totalFrames * t1;
+        if (seekToFrame >= totalFrames-1) {
+          // bodymovin player sometimes draws blank when requesting
+          // to draw the very last frame.  Subtracting a small value
+          // seems to fix this and make it draw the last frame.
+          seekToFrame -= .001;
+        }
+
+        anim.goToAndStop(seekToFrame, true /* isFrame */);
+        console.log("Used seek: " + (seekToFrame/totalFrames));
+        frame++;
+        window.requestAnimationFrame(drawFrame);
+      };
+      window.requestAnimationFrame(drawFrame);
+    })();
+  </script>
+</body>
+</html>
diff --git a/tools/lottie-web-perf/lottie-web-perf.js b/tools/lottie-web-perf/lottie-web-perf.js
index 298116580c..bc55092b33 100644
--- a/tools/lottie-web-perf/lottie-web-perf.js
+++ b/tools/lottie-web-perf/lottie-web-perf.js
@@ -36,6 +36,11 @@ const opts = [
     description: 'The path to lottie.min.js, defaults to a local npm install location.',
     type: String,
   },
+  {
+    name: 'backend',
+    description: 'Which lottie-web backend to use. Options: canvas or svg.',
+    type: String,
+  },
   {
     name: 'help',
     alias: 'h',
@@ -58,6 +63,12 @@ const usage = [
 // Parse and validate flags.
 const options = commandLineArgs(opts);
 
+if (options.backend != 'canvas' && options.backend != 'svg') {
+  console.error('You must supply a lottie-web backend (canvas, svg).');
+  console.log(commandLineUsage(usage));
+  process.exit(1);
+}
+
 if (!options.output) {
   options.output = 'perf.json';
 }
@@ -81,8 +92,15 @@ if (!options.input) {
 
 // Start up a web server to serve the three files we need.
 let lottieJS = fs.readFileSync(options.lottie_player, 'utf8');
-let driverHTML = fs.readFileSync('lottie-web-perf.html', 'utf8');
 let lottieJSON = fs.readFileSync(options.input, 'utf8');
+let driverHTML;
+if (options.backend == 'svg') {
+  console.log('Using lottie-web-perf.html');
+  driverHTML = fs.readFileSync('lottie-web-perf.html', 'utf8');
+} else {
+  console.log('Using lottie-web-canvas-perf.html');
+  driverHTML = fs.readFileSync('lottie-web-canvas-perf.html', 'utf8');
+}
 
 // Find number of frames from the lottie JSON.
 let lottieJSONContent = JSON.parse(lottieJSON);