Merge "Update catapult to latest version (76a241669)" am: 6e5717f80b am: ee01823b5b am: 3328f1104e
am: 8e2ec7bab5

Change-Id: Ia1645886356db3f147ffeef4fac2aecf5d05c232
diff --git a/UPSTREAM_REVISION b/UPSTREAM_REVISION
index 60f3d3a..2856b7a 100644
--- a/UPSTREAM_REVISION
+++ b/UPSTREAM_REVISION
@@ -1 +1 @@
-5e1c1c293b07ef04a247dd8dff50972d207663a4
+cad35e22dcad126c6a20663ded101565e6326d82
diff --git a/catapult/common/bin/run_tests b/catapult/common/bin/run_tests
index 3021755..632cdbf 100755
--- a/catapult/common/bin/run_tests
+++ b/catapult/common/bin/run_tests
@@ -6,33 +6,23 @@
 import os
 import sys
 
+
 _CATAPULT_PATH = os.path.abspath(
     os.path.join(os.path.dirname(__file__), '..', '..'))
-
-_PY_UTILS_PATH = os.path.abspath(
-    os.path.join(_CATAPULT_PATH, 'common', 'py_utils'))
-
-
-def _RunTestsOrDie(top_level_dir):
-  exit_code = run_with_typ.Run(top_level_dir, path=[_PY_UTILS_PATH])
-  if exit_code:
-    sys.exit(exit_code)
-
-
-def _AddToPathIfNeeded(path):
-  if path not in sys.path:
-    sys.path.insert(0, path)
+_TESTS = [
+    {'path': os.path.join(
+        _CATAPULT_PATH, 'common', 'eslint', 'bin', 'run_tests')},
+    {'path': os.path.join(
+        _CATAPULT_PATH, 'common', 'py_trace_event', 'bin', 'run_tests')},
+    {'path': os.path.join(
+        _CATAPULT_PATH, 'common', 'py_utils', 'bin', 'run_tests')},
+    {'path': os.path.join(
+        _CATAPULT_PATH, 'common', 'py_vulcanize', 'bin', 'run_py_tests')},
+]
 
 
 if __name__ == '__main__':
-  _AddToPathIfNeeded(_CATAPULT_PATH)
+  sys.path.append(_CATAPULT_PATH)
+  from catapult_build import test_runner
+  sys.exit(test_runner.Main('project', _TESTS, sys.argv))
 
-  from hooks import install
-  if '--no-install-hooks' in sys.argv:
-    sys.argv.remove('--no-install-hooks')
-  else:
-    install.InstallHooks()
-
-  from catapult_build import run_with_typ
-  _RunTestsOrDie(_PY_UTILS_PATH)
-  sys.exit(0)
diff --git a/catapult/common/bin/update_chrome_reference_binaries b/catapult/common/bin/update_chrome_reference_binaries
index 02070f0..e148c74 100755
--- a/catapult/common/bin/update_chrome_reference_binaries
+++ b/catapult/common/bin/update_chrome_reference_binaries
@@ -18,6 +18,7 @@
 import shutil
 import subprocess
 import sys
+import tempfile
 import urllib2
 import zipfile
 
@@ -42,9 +43,10 @@
 # Add one to enable updating it. (Must also update _PLATFORM_MAP.)
 _PLATFORMS_TO_UPDATE = ['mac_x86_64', 'win_x86', 'win_AMD64', 'linux_x86_64',
                         'android_k_armeabi-v7a', 'android_l_arm64-v8a',
-                        'android_l_armeabi-v7a', 'android_n_armeabi-v7a']
+                        'android_l_armeabi-v7a', 'android_n_armeabi-v7a',
+                        'android_n_arm64-v8a']
 
-# Remove a channal name from this list to disable updating it.
+# Remove a channel name from this list to disable updating it.
 # Add one to enable updating it.
 _CHANNELS_TO_UPDATE = ['stable', 'canary', 'dev']
 
@@ -56,7 +58,7 @@
 
 
 # All of the information we need to update each platform.
-#   omaha: name omaha uses for the plaftorms.
+#   omaha: name omaha uses for the platforms.
 #   zip_name: name of the zip file to be retrieved from cloud storage.
 #   gs_build: name of the Chrome build platform used in cloud storage.
 #   destination: Name of the folder to download the reference build to.
@@ -94,6 +96,10 @@
                                                      gs_folder='android-*',
                                                      gs_build='arm',
                                                      zip_name='Monochrome.apk'),
+                 'android_n_arm64-v8a': UpdateInfo(omaha='android',
+                                                   gs_folder='android-*',
+                                                   gs_build='arm_64',
+                                                   zip_name='Monochrome.apk'),
 
 }
 
@@ -155,11 +161,49 @@
     os.makedirs(reference_builds_folder)
   local_dest_path = os.path.join(reference_builds_folder, filename)
   cloud_storage.Get(CHROME_GS_BUCKET, remote_path, local_dest_path)
+  _ModifyBuildIfNeeded(local_dest_path, platform)
   config.AddCloudStorageDependencyUpdateJob(
       'chrome_%s' % channel, platform, local_dest_path, version=version,
       execute_job=False)
 
 
+def _ModifyBuildIfNeeded(location, platform):
+  """Hook to modify the build before saving it for Telemetry to use.
+
+  This can be used to remove various utilities that cause noise in a
+  test environment. Right now, it is just used to remove Keystone,
+  which is a tool used to autoupdate Chrome.
+  """
+  if platform == 'mac_x86_64':
+    _RemoveKeystoneFromBuild(location)
+    return
+
+  if 'mac' in platform:
+    raise NotImplementedError(
+        'Platform <%s> sounds like it is an OSX version. If so, we may need to '
+        'remove Keystone from it per crbug.com/932615. Please edit this script'
+        ' and teach it what needs to be done :).')
+
+
+def _RemoveKeystoneFromBuild(location):
+  """Removes the Keystone autoupdate binary from the chrome mac zipfile."""
+  logging.info('Removing keystone from mac build at %s' % location)
+  temp_folder = tempfile.mkdtemp(prefix='RemoveKeystoneFromBuild')
+  try:
+    subprocess.check_call(['unzip', '-q', location, '-d', temp_folder])
+    keystone_folder = os.path.join(
+        temp_folder, 'chrome-mac', 'Google Chrome.app', 'Contents',
+        'Frameworks', 'Google Chrome Framework.framework', 'Frameworks',
+        'KeystoneRegistration.framework')
+    shutil.rmtree(keystone_folder)
+    os.remove(location)
+    subprocess.check_call(['zip', '--quiet', '--recurse-paths', '--symlinks',
+                           location, 'chrome-mac'],
+                           cwd=temp_folder)
+  finally:
+    shutil.rmtree(temp_folder)
+
+
 def UpdateBuilds():
   config = base_config.BaseConfig(_CHROME_BINARIES_CONFIG, writable=True)
   for channel in _CHANNELS_TO_UPDATE:
@@ -172,7 +216,6 @@
       if current_version and current_version == channel_version:
         continue
       _QueuePlatformUpdate(platform, channel_version, config, channel)
-    # TODO: move execute update jobs here, and add committing/uploading the cl.
 
   print 'Updating chrome builds with downloaded binaries'
   config.ExecuteUpdateJobs(force=True)
@@ -180,10 +223,7 @@
 
 def main():
   logging.getLogger().setLevel(logging.DEBUG)
-  #TODO(aiolos): alert sheriffs via email when an error is seen.
-  #This should be added when alerts are added when updating the build.
   UpdateBuilds()
-  # TODO(aiolos): Add --commit flag. crbug.com/547229
 
 if __name__ == '__main__':
   main()
diff --git a/catapult/common/node_runner/node_runner/README.md b/catapult/common/node_runner/node_runner/README.md
new file mode 100644
index 0000000..47c85ba
--- /dev/null
+++ b/catapult/common/node_runner/node_runner/README.md
@@ -0,0 +1,11 @@
+Update binaries:
+
+1. Download archives pre-compiled binaries.
+2. Unzip archives.
+3. Re-zip just the binary:
+   `zip new.zip node-v10.14.1-linux-x64/bin/node`
+4. Use the update script:
+   `./dependency_manager/bin/update --config
+   common/node_runner/node_runner/node_binaries.json --dependency node --path
+   new.zip --platform linux_x86_64`
+5. Mail out the automated change to `node_binaries.json` for review and CQ.
diff --git a/catapult/common/node_runner/node_runner/minifyjs b/catapult/common/node_runner/node_runner/minifyjs
new file mode 100755
index 0000000..e594169
--- /dev/null
+++ b/catapult/common/node_runner/node_runner/minifyjs
@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+'use strict';
+/*
+Copyright 2019 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+
+This script strips whitespace and comments from Javascript.
+*/
+const escodegen = require('escodegen');
+const espree = require('espree');
+const fs = require('fs');
+const nopt = require('nopt');
+
+const args = nopt();
+const filename = args.argv.remain[0];
+
+let text = fs.readFileSync(filename).toString('utf8');
+const ast = espree.parse(text, {ecmaVersion: 2018});
+text = escodegen.generate(ast, {format: {indent: {style: ''}}});
+fs.writeFileSync(filename, text);
diff --git a/catapult/common/node_runner/node_runner/node_binaries.json b/catapult/common/node_runner/node_runner/node_binaries.json
index 4245249..3a17db0 100644
--- a/catapult/common/node_runner/node_runner/node_binaries.json
+++ b/catapult/common/node_runner/node_runner/node_binaries.json
@@ -6,9 +6,9 @@
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
         "linux_x86_64": {
-          "cloud_storage_hash": "5750e968975e7f5ab8cb694f5e92a34a890e129d",
+          "cloud_storage_hash": "27ad092b0ce59d2da32090a00f717f0c31e65240",
           "download_path": "bin/node/node-linux64.zip",
-          "path_within_archive": "node-v6.7.0-linux-x64/bin/node",
+          "path_within_archive": "node-v10.14.1-linux-x64/bin/node",
           "version_in_cs": "6.7.0"
         },
         "mac_x86_64": {
diff --git a/catapult/common/node_runner/node_runner/package-lock.json b/catapult/common/node_runner/node_runner/package-lock.json
new file mode 100644
index 0000000..683cae9
--- /dev/null
+++ b/catapult/common/node_runner/node_runner/package-lock.json
@@ -0,0 +1,7189 @@
+{
+  "name": "catapult_base",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@chopsui/batch-iterator": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@chopsui/batch-iterator/-/batch-iterator-0.1.0.tgz",
+      "integrity": "sha512-rKXkaIe3H6sQ5bQ798Qdim3v5Lb1WD881daiiMgTsnWvHmFftiytsC0yPespE20vxlllDea2CZpzfOxTY6/Wsg=="
+    },
+    "@chopsui/chops-button": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-button/-/chops-button-0.1.11.tgz",
+      "integrity": "sha512-Mf2t8W629ABg+CKmI6friQGAE7C9bed/Q2GF4Bb8QLKKHcYM73XtWDNcivr4h7ej6YeuGf1KzGMWsApk3m/zww==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-checkbox": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-checkbox/-/chops-checkbox-0.1.11.tgz",
+      "integrity": "sha512-nJOXWP04kIw9eZio1yye0wJEwWR5ZWZUBk2XP+//Fuu+RHMafZdkGfG4DNdrHh9VYprdRcZNM4R+LS5Zh9l6JQ==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-header": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-header/-/chops-header-0.1.5.tgz",
+      "integrity": "sha512-AVbOU1IjOsKxO7j3B0TWXLSzWcaznmxAJFCh9Hq0GZUeBF/d+UBzlwoVZ6fXwzZXZ4A54QVbFbeD+bNQJ55piQ==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-input": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-input/-/chops-input-0.1.11.tgz",
+      "integrity": "sha512-B4dE2IoyilBpQAt1ERH3Q4PmpgRNogo2xlFNhag9FedBKXZmYa+o2ygl25IuAMaUa30mWBz1kOKYN8Lsovxv+w==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-loading": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-loading/-/chops-loading-0.1.11.tgz",
+      "integrity": "sha512-IkLWkiQXsJHd76MPN4pfoeAcX+4Ap9g6WSh1j7oFMJd2rzHQZpPfkLlMcAI99nUymmZrLbRjZ3qO48FbViK+kg==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-radio": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-radio/-/chops-radio-0.1.11.tgz",
+      "integrity": "sha512-ZFtS+CtyGg34ezzTod20zLOYPgsHSmpyZ4zmkDdY1fatBdskG3ojSp4u0p/fd9kTKSykG94h0Gtj02GijCCRRg==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-radio-group": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-radio-group/-/chops-radio-group-0.1.11.tgz",
+      "integrity": "sha512-Fq5/RaTI1kpdxOenFKp9P/0fDQXzQYhU7+v1/W+7NgB6SlOtJ6EmsVsotEI/woPuRcOdt7dcrzATj4IQwapKxA==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-signin": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-signin/-/chops-signin-0.1.5.tgz",
+      "integrity": "sha512-4dLoxnc+W6CmErR8iUfFh01da8AUndnbTSjCRnklYMCMhq3oCCgHKF709ISzEjuChsbwKLe6Y0EjEScLeMiVeg==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-switch": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-switch/-/chops-switch-0.1.11.tgz",
+      "integrity": "sha512-ie+7x3xoZA8ADnr6+2HJox6xycCEvZb1Qhhu3lWuXi7TINFFTry0C7vU9W8EoBu31JVM+g47Y+9+HI6jQfaUbA==",
+      "requires": {
+        "@chopsui/chops-checkbox": "^0.1.6",
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-tab": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-tab/-/chops-tab-0.1.11.tgz",
+      "integrity": "sha512-9YUcBNUSaW7Cyk5MNQSZpR4fDhwJul8na7/MwEpgdRVdndbVl7a4juTI4oTftEeoqjirPn/ZEo7+VwlJp0kR7A==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-tab-bar": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-tab-bar/-/chops-tab-bar-0.1.11.tgz",
+      "integrity": "sha512-BeClVVCpYN/h7nKGaAIT9hJS3tLhzam4coIK0t/egImJNPGHj3+Mu07MzjUYZb2dA/rcKjpAdA9cIQFfEzXthA==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/chops-textarea": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/@chopsui/chops-textarea/-/chops-textarea-0.1.11.tgz",
+      "integrity": "sha512-lJDC6OeTpKQV5JYED6Ev5Rkm3oMw/UcOWXyLh6n1/BnlCweg8n1CGqqUQvxtxTG7hc4fhIkiok84zcSnwBcwIg==",
+      "requires": {
+        "lit-element": "^2.0.0"
+      }
+    },
+    "@chopsui/result-channel": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@chopsui/result-channel/-/result-channel-0.1.0.tgz",
+      "integrity": "sha512-9gocIAIwaX74Yj+wnkzlebfgTsvnZed8h+Yc71KDGO/A9rmgMNvl1kC1DoXgMMCUvELM0LybGHfZvzfkM8HKlw=="
+    },
+    "@chopsui/tsmon-client": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/@chopsui/tsmon-client/-/tsmon-client-0.0.1.tgz",
+      "integrity": "sha1-QoowBjL2RNLWDxU9WBj2fWTugF0="
+    },
+    "@polymer/app-route": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@polymer/app-route/-/app-route-3.0.2.tgz",
+      "integrity": "sha1-dJCW+2EPsV0nx7aERkBvMHhs+T0=",
+      "requires": {
+        "@polymer/iron-location": "^3.0.0-pre.26",
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-collapse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-collapse/-/iron-collapse-3.0.1.tgz",
+      "integrity": "sha1-ZBfIT1QF7ZCRh3ZdkkLjuHukYm8=",
+      "requires": {
+        "@polymer/iron-resizable-behavior": "^3.0.0-pre.26",
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-flex-layout": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz",
+      "integrity": "sha1-NvnhqOt5LSebK8ddNiYochrTfww=",
+      "requires": {
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-icon": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-icon/-/iron-icon-3.0.1.tgz",
+      "integrity": "sha1-kyEcOdiCX+SWWmhBlWYDbB3ykes=",
+      "requires": {
+        "@polymer/iron-flex-layout": "^3.0.0-pre.26",
+        "@polymer/iron-meta": "^3.0.0-pre.26",
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-iconset-svg": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz",
+      "integrity": "sha1-Vo1ufbwSApna5jvjYArroNMN2+o=",
+      "requires": {
+        "@polymer/iron-meta": "^3.0.0-pre.26",
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-location": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-location/-/iron-location-3.0.1.tgz",
+      "integrity": "sha1-Q6WfztJI6nHbWDMRb83voYa3lSc=",
+      "requires": {
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-meta": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz",
+      "integrity": "sha1-fxQGKNEnsKKE+ILxuzI6JhvBJfU=",
+      "requires": {
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/iron-resizable-behavior": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz",
+      "integrity": "sha1-4oQ0jtfBx+Jj9wOSl1MvqVQCXqI=",
+      "requires": {
+        "@polymer/polymer": "^3.0.0"
+      }
+    },
+    "@polymer/polymer": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.2.0.tgz",
+      "integrity": "sha1-tB/d7E7KxjsSk2uTcmZ40jrdev0=",
+      "requires": {
+        "@webcomponents/shadycss": "^1.8.0"
+      }
+    },
+    "@sinonjs/commons": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
+      "integrity": "sha1-ez7C2Wr0gdegMhJS57HJRyTsWng=",
+      "requires": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "@sinonjs/formatio": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
+      "integrity": "sha1-UjEPL5vLxnvawYyUrUkBuV/eJn4=",
+      "requires": {
+        "@sinonjs/commons": "^1",
+        "@sinonjs/samsam": "^3.1.0"
+      }
+    },
+    "@sinonjs/samsam": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz",
+      "integrity": "sha1-6IxT+9nZGtnw8rAUDBbHwQf+DQc=",
+      "requires": {
+        "@sinonjs/commons": "^1.0.2",
+        "array-from": "^2.1.1",
+        "lodash": "^4.17.11"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.11",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+          "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+        }
+      }
+    },
+    "@sinonjs/text-encoding": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+      "integrity": "sha1-jaXGUwkVZT86Hzj9XxAdjD+AecU="
+    },
+    "@types/clone": {
+      "version": "0.1.30",
+      "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz",
+      "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ="
+    },
+    "@types/node": {
+      "version": "4.2.23",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-4.2.23.tgz",
+      "integrity": "sha1-kkHwDWTrkQhPaDZ3Ru8Q1fsvL8Q="
+    },
+    "@types/parse5": {
+      "version": "0.0.31",
+      "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-0.0.31.tgz",
+      "integrity": "sha1-6Cekk6RDsVbhtYKi5MO9wAQPLuc=",
+      "requires": {
+        "@types/node": "6.0.*"
+      },
+      "dependencies": {
+        "@types/node": {
+          "version": "6.0.116",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.116.tgz",
+          "integrity": "sha1-L5zWK07MSSfjlC4mVcGC7s9bRfE="
+        }
+      }
+    },
+    "@webassemblyjs/ast": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.10.tgz",
+      "integrity": "sha1-DPxh1hKGJAty/FIst1VhNpnupAo=",
+      "requires": {
+        "@webassemblyjs/helper-module-context": "1.7.10",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.10",
+        "@webassemblyjs/wast-parser": "1.7.10"
+      }
+    },
+    "@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.10.tgz",
+      "integrity": "sha1-7mPXKcYxGoWGPjaaRz+Zg/mE5Nk="
+    },
+    "@webassemblyjs/helper-api-error": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.10.tgz",
+      "integrity": "sha1-v8s7vll3U1dHV5CirXsonwmy8Zg="
+    },
+    "@webassemblyjs/helper-buffer": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.10.tgz",
+      "integrity": "sha1-CoxiTGetCyFNLgA4WZIaGYjLFRs="
+    },
+    "@webassemblyjs/helper-code-frame": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.10.tgz",
+      "integrity": "sha1-CrfiL60CQaFzF4xzl2/A7fUIMs4=",
+      "requires": {
+        "@webassemblyjs/wast-printer": "1.7.10"
+      }
+    },
+    "@webassemblyjs/helper-fsm": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.10.tgz",
+      "integrity": "sha1-CRXncT+7tzViCp0+T6PXlR+XrGQ="
+    },
+    "@webassemblyjs/helper-module-context": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.10.tgz",
+      "integrity": "sha1-m+uD9ydA9ayAdTE7XKxeeWUQ91U="
+    },
+    "@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.10.tgz",
+      "integrity": "sha1-eXsec0u8/eqDmWac3FgwjvHH/8A="
+    },
+    "@webassemblyjs/helper-wasm-section": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.10.tgz",
+      "integrity": "sha1-wOo3A8YV17w+NQfDt5kch2ey8g4=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-buffer": "1.7.10",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.10",
+        "@webassemblyjs/wasm-gen": "1.7.10"
+      }
+    },
+    "@webassemblyjs/ieee754": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.10.tgz",
+      "integrity": "sha1-YsFyi37w9m74Ih4pZqCv1120MN8=",
+      "requires": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "@webassemblyjs/leb128": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.10.tgz",
+      "integrity": "sha1-Fn4LtLBtdwFYV3KnP7qfTfhUOfY=",
+      "requires": {
+        "@xtuc/long": "4.2.1"
+      }
+    },
+    "@webassemblyjs/utf8": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.10.tgz",
+      "integrity": "sha1-tnKPW29QNkq8FVvgKflnDmaFYFo="
+    },
+    "@webassemblyjs/wasm-edit": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.10.tgz",
+      "integrity": "sha1-g/4xQPWlj1owuRRwK+nw5Zo5kJI=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-buffer": "1.7.10",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.10",
+        "@webassemblyjs/helper-wasm-section": "1.7.10",
+        "@webassemblyjs/wasm-gen": "1.7.10",
+        "@webassemblyjs/wasm-opt": "1.7.10",
+        "@webassemblyjs/wasm-parser": "1.7.10",
+        "@webassemblyjs/wast-printer": "1.7.10"
+      }
+    },
+    "@webassemblyjs/wasm-gen": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.10.tgz",
+      "integrity": "sha1-TeADgGrinJerNwd4JGm1MplXAXQ=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.10",
+        "@webassemblyjs/ieee754": "1.7.10",
+        "@webassemblyjs/leb128": "1.7.10",
+        "@webassemblyjs/utf8": "1.7.10"
+      }
+    },
+    "@webassemblyjs/wasm-opt": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.10.tgz",
+      "integrity": "sha1-0VHjFhGTSlVsgnif3uxBqBSZPCo=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-buffer": "1.7.10",
+        "@webassemblyjs/wasm-gen": "1.7.10",
+        "@webassemblyjs/wasm-parser": "1.7.10"
+      }
+    },
+    "@webassemblyjs/wasm-parser": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.10.tgz",
+      "integrity": "sha1-A2e+e/jwnj5qvJX45IO5IGSH7GU=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-api-error": "1.7.10",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.10",
+        "@webassemblyjs/ieee754": "1.7.10",
+        "@webassemblyjs/leb128": "1.7.10",
+        "@webassemblyjs/utf8": "1.7.10"
+      }
+    },
+    "@webassemblyjs/wast-parser": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.10.tgz",
+      "integrity": "sha1-BY9Zi1L3MLI/yHTUd1tihrYkcmQ=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/floating-point-hex-parser": "1.7.10",
+        "@webassemblyjs/helper-api-error": "1.7.10",
+        "@webassemblyjs/helper-code-frame": "1.7.10",
+        "@webassemblyjs/helper-fsm": "1.7.10",
+        "@xtuc/long": "4.2.1"
+      }
+    },
+    "@webassemblyjs/wast-printer": {
+      "version": "1.7.10",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.10.tgz",
+      "integrity": "sha1-2BeQnSRQrpbGa3YHYk2YozuEIjs=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/wast-parser": "1.7.10",
+        "@xtuc/long": "4.2.1"
+      }
+    },
+    "@webcomponents/shadycss": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.9.1.tgz",
+      "integrity": "sha1-12n7rfpQTxG4TK7vJnAfiQcOxJo="
+    },
+    "@webpack-contrib/config-loader": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@webpack-contrib/config-loader/-/config-loader-1.2.1.tgz",
+      "integrity": "sha1-Wz3UdOIHQ3k50pTSAMaLewAAjgQ=",
+      "requires": {
+        "@webpack-contrib/schema-utils": "^1.0.0-beta.0",
+        "chalk": "^2.1.0",
+        "cosmiconfig": "^5.0.2",
+        "is-plain-obj": "^1.1.0",
+        "loud-rejection": "^1.6.0",
+        "merge-options": "^1.0.1",
+        "minimist": "^1.2.0",
+        "resolve": "^1.6.0",
+        "webpack-log": "^1.1.2"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "@webpack-contrib/schema-utils": {
+      "version": "1.0.0-beta.0",
+      "resolved": "https://registry.npmjs.org/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz",
+      "integrity": "sha1-v5Y4yUZNF3tIIJ6EIJ4jvuLrT2U=",
+      "requires": {
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0",
+        "chalk": "^2.3.2",
+        "strip-ansi": "^4.0.0",
+        "text-table": "^0.2.0",
+        "webpack-log": "^1.1.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.5.4",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
+          "integrity": "sha1-JH1SdBENtlNwa1UPzCt5fKKM/Fk=",
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ajv-keywords": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+          "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo="
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+        }
+      }
+    },
+    "@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha1-7vAUoxRa5Hehy8AM0eVSM23Ot5A="
+    },
+    "@xtuc/long": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz",
+      "integrity": "sha1-XIXWYvdvodNFdXZsXc1mFavNMNg="
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg="
+    },
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=",
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      }
+    },
+    "acorn": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz",
+      "integrity": "sha1-8JWCkpdwanyXdpWMCvyJMKm52dg="
+    },
+    "acorn-dynamic-import": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
+      "integrity": "sha1-kBzu5Mf6rvfgetKkfokGddpQong=",
+      "requires": {
+        "acorn": "^5.0.0"
+      }
+    },
+    "acorn-jsx": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+      "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+      "requires": {
+        "acorn": "^3.0.4"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
+        }
+      }
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+    },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha1-2J5ZmfeXh1Z0wH2H8mD8Qeg+jKk=",
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+      "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+      "requires": {
+        "co": "^4.6.0",
+        "fast-deep-equal": "^1.0.0",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.3.0"
+      }
+    },
+    "ajv-keywords": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+      "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I="
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "optional": true
+    },
+    "ansi-align": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz",
+      "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=",
+      "requires": {
+        "string-width": "^2.0.0"
+      }
+    },
+    "ansi-colors": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
+      "integrity": "sha1-46PaS/uubIapwoViXeEkojQCb78="
+    },
+    "ansi-escapes": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+      "integrity": "sha1-9zIHu4EgfXX9bIPxJa8m7qN4yjA="
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+    },
+    "anymatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=",
+      "requires": {
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      }
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=",
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE="
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ="
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
+    },
+    "array-from": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
+      "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU="
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU="
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
+    },
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha1-ucK/WAXx5kqt7tbfOiv6+1pz9aA=",
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "requires": {
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs="
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
+    },
+    "async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+      "integrity": "sha1-GDMOp+bjE4h/XS8qkEusb+TdU4E=",
+      "requires": {
+        "lodash": "^4.17.11"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.11",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+          "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+        }
+      }
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0="
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg="
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k="
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha1-GERAjTuPDTWkBOp6wYDwh6YBvZA=",
+      "requires": {
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "detect-indent": "^4.0.0",
+        "jsesc": "^1.3.0",
+        "lodash": "^4.17.4",
+        "source-map": "^0.5.7",
+        "trim-right": "^1.0.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+        }
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-polyfill": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+      "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "regenerator-runtime": "^0.10.5"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+          "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk="
+        }
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "debug": "^2.6.8",
+        "globals": "^9.18.0",
+        "invariant": "^2.2.2",
+        "lodash": "^4.17.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "globals": {
+          "version": "9.18.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+          "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo="
+        }
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM="
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=",
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha1-yrHmEY8FEJXli1KBrqjBzSK/wOM="
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "big.js": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+      "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4="
+    },
+    "binary-extensions": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
+      "integrity": "sha1-wteA9T1Fu6gxeokC1M7q86Y4WxQ="
+    },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha1-1oDu7yX4zZGtUz9bAe7UjmTK9oM="
+    },
+    "bluebird": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz",
+      "integrity": "sha1-G+CQjgVKdRdUVJwnBInBUF1KsVo="
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8="
+    },
+    "body-parser": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha1-lrJwnlfJxOCab9Zqj9l5hE9p8Io=",
+      "requires": {
+        "bytes": "3.1.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "http-errors": {
+          "version": "1.7.2",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+          "integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.1.1",
+            "statuses": ">= 1.5.0 < 2",
+            "toidentifier": "1.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+          "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM="
+        },
+        "statuses": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+          "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+        }
+      }
+    },
+    "boxen": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
+      "integrity": "sha1-VcbDmouljZxhrSLNh3Uy3rZlogs=",
+      "requires": {
+        "ansi-align": "^2.0.0",
+        "camelcase": "^4.0.0",
+        "chalk": "^2.0.1",
+        "cli-boxes": "^1.0.0",
+        "string-width": "^2.0.0",
+        "term-size": "^1.2.0",
+        "widest-line": "^2.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
+        }
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=",
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA="
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha1-Mmc0ZC9APavDADIJhTu3CtQo70g=",
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha1-jWR0wbhwv9q807z8wZNKEOlPFfA=",
+      "requires": {
+        "browserify-aes": "^1.0.4",
+        "browserify-des": "^1.0.0",
+        "evp_bytestokey": "^1.0.0"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+      "integrity": "sha1-OvTx9Zg5QDVy8cZiBDdfen9wPpw=",
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "des.js": "^1.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "requires": {
+        "bn.js": "^4.1.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "requires": {
+        "bn.js": "^4.1.1",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.2",
+        "elliptic": "^6.0.0",
+        "inherits": "^2.0.1",
+        "parse-asn1": "^5.0.0"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha1-KGlFnZqjviRf6P4sofRuLn9U1z8=",
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
+    "buffer": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+      "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
+      }
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=",
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA="
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8="
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
+    },
+    "bytes": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+      "integrity": "sha1-9s95M6Ng4FiPqf3oVlHNx/gF0fY="
+    },
+    "cacache": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+      "integrity": "sha1-ZFI2eZnv+dQYiu/ZoU6dfGomNGA=",
+      "requires": {
+        "bluebird": "^3.5.1",
+        "chownr": "^1.0.1",
+        "glob": "^7.1.2",
+        "graceful-fs": "^4.1.11",
+        "lru-cache": "^4.1.1",
+        "mississippi": "^2.0.0",
+        "mkdirp": "^0.5.1",
+        "move-concurrently": "^1.0.1",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^2.6.2",
+        "ssri": "^5.2.4",
+        "unique-filename": "^1.1.0",
+        "y18n": "^4.0.0"
+      }
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=",
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
+    "caller-path": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+      "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+      "requires": {
+        "callsites": "^0.2.0"
+      }
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+    },
+    "callsites": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+      "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo="
+    },
+    "camelcase": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+      "integrity": "sha1-AylVJ9WL081Kp1Nj81sujZe+L0I="
+    },
+    "camelcase-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz",
+      "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=",
+      "requires": {
+        "camelcase": "^4.1.0",
+        "map-obj": "^2.0.0",
+        "quick-lru": "^1.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
+        }
+      }
+    },
+    "capture-stack-trace": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz",
+      "integrity": "sha1-psC74fOPOqC5Ijjstv9Cw0TUE10="
+    },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha1-dgqnLPION5XoSxKHfODoNzeqKeU=",
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha1-GMSasWoDe26wFSzIPjRxM4IVtm4=",
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "chardet": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+      "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII="
+    },
+    "chokidar": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
+      "integrity": "sha1-NW/04rDo5D4yLRijckYLvPOszSY=",
+      "requires": {
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.0",
+        "braces": "^2.3.0",
+        "fsevents": "^1.2.2",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.1",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "lodash.debounce": "^4.0.8",
+        "normalize-path": "^2.1.1",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.0.0",
+        "upath": "^1.0.5"
+      }
+    },
+    "chownr": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+      "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ="
+    },
+    "chrome-trace-event": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
+      "integrity": "sha1-Rakb0sIMlBHwljtarrmhuV4JzEg=",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "ci-info": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
+      "integrity": "sha1-LKINu5zrMtRSSmgzAzE/AwSx5Jc="
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "circular-json": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+      "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY="
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "cli-boxes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
+      "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM="
+    },
+    "cli-cursor": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+      "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+      "requires": {
+        "restore-cursor": "^2.0.0"
+      }
+    },
+    "cli-spinners": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz",
+      "integrity": "sha1-ACwZkJEtDVlYDJO9NsBW3pnkJZo="
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
+    },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
+      "integrity": "sha1-SYgbj7pn3xKpa98/VsCqueeRMUc=",
+      "requires": {
+        "color-name": "1.1.1"
+      }
+    },
+    "color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok="
+    },
+    "colors": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz",
+      "integrity": "sha1-OeAF1Uav4B4B+cTKj6UPaGoBIF0="
+    },
+    "commander": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+      "integrity": "sha1-aWS8pnaF33wfFDDFhPB9dZeIW5w="
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=",
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "configstore": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz",
+      "integrity": "sha1-xvJd767vJt8S3TNBSwAf6BpUP48=",
+      "requires": {
+        "dot-prop": "^4.1.0",
+        "graceful-fs": "^4.1.2",
+        "make-dir": "^1.0.0",
+        "unique-string": "^1.0.0",
+        "write-file-atomic": "^2.0.0",
+        "xdg-basedir": "^3.0.0"
+      }
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "~1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "requires": {
+        "date-now": "^0.1.4"
+      }
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js="
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha1-UbU3qMQ+DwTewZk7/83VBOdYrCA=",
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "copy-concurrently": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+      "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=",
+      "requires": {
+        "aproba": "^1.1.1",
+        "fs-write-stream-atomic": "^1.0.8",
+        "iferr": "^0.1.5",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.0"
+      }
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
+    },
+    "core-js": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+      "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cosmiconfig": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
+      "integrity": "sha1-3KbPaAoL0DWJr/aEcAhYyBq+6zk=",
+      "requires": {
+        "is-directory": "^0.3.1",
+        "js-yaml": "^3.9.0",
+        "parse-json": "^4.0.0"
+      }
+    },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha1-yREbbzMEXEaX8UR4f5JUzcd8Rf8=",
+      "requires": {
+        "bn.js": "^4.1.0",
+        "elliptic": "^6.0.0"
+      }
+    },
+    "create-error-class": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
+      "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
+      "requires": {
+        "capture-stack-trace": "^1.0.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha1-iJB4rxGmN1a8+1m9IhmWvjqe8ZY=",
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha1-aRcMeLOrlXFHsriwRXLkfq0iQ/8=",
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cross-spawn": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+      "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+      "requires": {
+        "lru-cache": "^4.0.1",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=",
+      "requires": {
+        "browserify-cipher": "^1.0.0",
+        "browserify-sign": "^4.0.0",
+        "create-ecdh": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.0",
+        "diffie-hellman": "^5.0.0",
+        "inherits": "^2.0.1",
+        "pbkdf2": "^3.0.3",
+        "public-encrypt": "^4.0.0",
+        "randombytes": "^2.0.0",
+        "randomfill": "^1.0.3"
+      }
+    },
+    "crypto-random-string": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
+      "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4="
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "requires": {
+        "array-find-index": "^1.0.1"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU="
+    },
+    "cyclist": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
+      "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA="
+    },
+    "d": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
+      "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+      "requires": {
+        "es5-ext": "^0.10.9"
+      }
+    },
+    "date-format": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz",
+      "integrity": "sha1-fPexcvHsVk8AA7OeowLFSY+5jI8="
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
+    },
+    "dateformat": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+      "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+      "requires": {
+        "get-stdin": "^4.0.1",
+        "meow": "^3.3.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+        },
+        "camelcase-keys": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+          "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+          "requires": {
+            "camelcase": "^2.0.0",
+            "map-obj": "^1.0.0"
+          }
+        },
+        "decamelize": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+          "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+        },
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "requires": {
+            "path-exists": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "indent-string": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+          "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+          "requires": {
+            "repeating": "^2.0.0"
+          }
+        },
+        "load-json-file": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+          "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "parse-json": "^2.2.0",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0",
+            "strip-bom": "^2.0.0"
+          }
+        },
+        "map-obj": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+          "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
+        },
+        "meow": {
+          "version": "3.7.0",
+          "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+          "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+          "requires": {
+            "camelcase-keys": "^2.0.0",
+            "decamelize": "^1.1.2",
+            "loud-rejection": "^1.0.0",
+            "map-obj": "^1.0.1",
+            "minimist": "^1.1.3",
+            "normalize-package-data": "^2.3.4",
+            "object-assign": "^4.0.1",
+            "read-pkg-up": "^1.0.1",
+            "redent": "^1.0.0",
+            "trim-newlines": "^1.0.0"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        },
+        "parse-json": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+          "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+          "requires": {
+            "error-ex": "^1.2.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "requires": {
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "read-pkg": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+          "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+          "requires": {
+            "load-json-file": "^1.0.0",
+            "normalize-package-data": "^2.3.2",
+            "path-type": "^1.0.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+          "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+          "requires": {
+            "find-up": "^1.0.0",
+            "read-pkg": "^1.0.0"
+          }
+        },
+        "redent": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+          "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+          "requires": {
+            "indent-string": "^2.1.0",
+            "strip-indent": "^1.0.1"
+          }
+        },
+        "strip-bom": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+          "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+          "requires": {
+            "is-utf8": "^0.2.0"
+          }
+        },
+        "strip-indent": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+          "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+          "requires": {
+            "get-stdin": "^4.0.1"
+          }
+        },
+        "trim-newlines": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+          "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
+        }
+      }
+    },
+    "debug": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+      "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz",
+      "integrity": "sha1-ZW17vICUxMeI6lPFhAkIycfQY8c=",
+      "requires": {
+        "xregexp": "4.0.0"
+      }
+    },
+    "decamelize-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
+      "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
+      "requires": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "dependencies": {
+        "decamelize": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+          "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+        },
+        "map-obj": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+          "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
+        }
+      }
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+    },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=",
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
+    "deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw="
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
+    },
+    "defaults": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+      "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+      "requires": {
+        "clone": "^1.0.2"
+      }
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=",
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=",
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "del": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+      "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+      "requires": {
+        "globby": "^5.0.0",
+        "is-path-cwd": "^1.0.0",
+        "is-path-in-cwd": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "rimraf": "^2.2.8"
+      }
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw="
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI="
+    },
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha1-QOjumPVaIUlgcUaSHGPhrl89KHU=",
+      "requires": {
+        "bn.js": "^4.1.0",
+        "miller-rabin": "^4.0.0",
+        "randombytes": "^2.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha1-XNAfwQFiG0LEzX9dGmYkNxbT850=",
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "requires": {
+        "custom-event": "~1.0.0",
+        "ent": "~2.2.0",
+        "extend": "^3.0.0",
+        "void-elements": "^2.0.0"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "requires": {
+        "domelementtype": "~1.1.1",
+        "entities": "~1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
+        }
+      }
+    },
+    "dom5": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/dom5/-/dom5-1.3.6.tgz",
+      "integrity": "sha1-pwiKn8XzsI3J9u2kx6uuskGUXg0=",
+      "requires": {
+        "@types/clone": "^0.1.29",
+        "@types/node": "^4.0.30",
+        "@types/parse5": "^0.0.31",
+        "clone": "^1.0.2",
+        "parse5": "^1.4.1"
+      }
+    },
+    "domain-browser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+      "integrity": "sha1-PTH1AZGmdJ3RN1p/Ui6CPULlTto="
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=",
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=",
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "dot-prop": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
+      "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=",
+      "requires": {
+        "is-obj": "^1.0.0"
+      }
+    },
+    "dot-prop-immutable": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/dot-prop-immutable/-/dot-prop-immutable-1.5.0.tgz",
+      "integrity": "sha512-YcnAEqxtJSect/W3taJeMkKhDrL7NzzvgKlJ515m5aGxJBJpzetXf0wZbGapdrBNwAItWvb4sOn+jX0RBYYM1g=="
+    },
+    "duplexer3": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+      "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+    },
+    "duplexify": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
+      "integrity": "sha1-saeinEq/1jlYXvrszoDWZrHjQSU=",
+      "requires": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "elliptic": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+      "integrity": "sha1-wtC3d2kRuGcixjLDwGxg8vgZk5o=",
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      }
+    },
+    "emojis-list": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+      "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+      "integrity": "sha1-tgKBw1SEpw7gNR6g6/+D7IyVIqI=",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.0",
+        "ws": "~3.3.1"
+      },
+      "dependencies": {
+        "ws": {
+          "version": "3.3.3",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+          "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=",
+          "requires": {
+            "async-limiter": "~1.0.0",
+            "safe-buffer": "~5.1.0",
+            "ultron": "~1.1.0"
+          }
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+      "integrity": "sha1-b1TAR13khxWKGnx30QF4cItq3TY=",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "~3.3.1",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "ws": {
+          "version": "3.3.3",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+          "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=",
+          "requires": {
+            "async-limiter": "~1.0.0",
+            "safe-buffer": "~5.1.0",
+            "ultron": "~1.1.0"
+          }
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha1-dXq5cPvy37Mse3SwMyFtVznveaY=",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      }
+    },
+    "enhanced-resolve": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
+      "integrity": "sha1-Qcfgv9/nSsH/4eV61qXGyfN0Kn8=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "memory-fs": "^0.4.0",
+        "tapable": "^1.0.0"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
+    },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+    },
+    "errno": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+      "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=",
+      "requires": {
+        "prr": "~1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=",
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
+      "integrity": "sha1-nbvdJ8aFbwABQhyhh4LXhr+KYWU=",
+      "requires": {
+        "es-to-primitive": "^1.1.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.1",
+        "is-callable": "^1.1.3",
+        "is-regex": "^1.0.4"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+      "integrity": "sha1-7fckeAM0VujdqO8J4ArZZQcH83c=",
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.46",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz",
+      "integrity": "sha1-79mfZ8Wn7Hibqj2qf3mHA4j39XI=",
+      "requires": {
+        "es6-iterator": "~2.0.3",
+        "es6-symbol": "~3.1.1",
+        "next-tick": "1"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-promise": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz",
+      "integrity": "sha1-lu258v2wGZWCKyY92KratnSBgbw="
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "requires": {
+        "es6-promise": "^4.0.3"
+      },
+      "dependencies": {
+        "es6-promise": {
+          "version": "4.2.6",
+          "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
+          "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q=="
+        }
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+      "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "escodegen": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz",
+      "integrity": "sha1-snqTiUgdW/1b7Hb3ux6z+PRVZYk=",
+      "requires": {
+        "esprima": "^3.1.3",
+        "estraverse": "^4.2.0",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.6.1"
+      }
+    },
+    "eslint": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+      "integrity": "sha1-MtHWU+HZBAiFS/spbwdux+GGowA=",
+      "requires": {
+        "ajv": "^5.3.0",
+        "babel-code-frame": "^6.22.0",
+        "chalk": "^2.1.0",
+        "concat-stream": "^1.6.0",
+        "cross-spawn": "^5.1.0",
+        "debug": "^3.1.0",
+        "doctrine": "^2.1.0",
+        "eslint-scope": "^3.7.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^3.5.4",
+        "esquery": "^1.0.0",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^2.0.0",
+        "functional-red-black-tree": "^1.0.1",
+        "glob": "^7.1.2",
+        "globals": "^11.0.1",
+        "ignore": "^3.3.3",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^3.0.6",
+        "is-resolvable": "^1.0.0",
+        "js-yaml": "^3.9.1",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.4",
+        "minimatch": "^3.0.2",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.2",
+        "pluralize": "^7.0.0",
+        "progress": "^2.0.0",
+        "regexpp": "^1.0.1",
+        "require-uncached": "^1.0.3",
+        "semver": "^5.3.0",
+        "strip-ansi": "^4.0.0",
+        "strip-json-comments": "~2.0.1",
+        "table": "4.0.2",
+        "text-table": "~0.2.0"
+      }
+    },
+    "eslint-config-google": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.6.0.tgz",
+      "integrity": "sha1-xULsGPsyR5g6wWu6MWYtAWJbdj8=",
+      "requires": {
+        "eslint-config-xo": "^0.13.0"
+      }
+    },
+    "eslint-config-xo": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-xo/-/eslint-config-xo-0.13.0.tgz",
+      "integrity": "sha1-+RZ2VDK6Z9L8enF3uLz+8/brBWQ="
+    },
+    "eslint-plugin-html": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-4.0.5.tgz",
+      "integrity": "sha1-6Ox+FkhRJEYPO/8xIBb+sKVNllk=",
+      "requires": {
+        "htmlparser2": "^3.8.2"
+      }
+    },
+    "eslint-scope": {
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+      "integrity": "sha1-u1ByANPRf2AkdjYWC0gmKEsQhTU=",
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha1-PzGA+y4pEBdxastMnW1bXDSmqB0="
+    },
+    "espree": {
+      "version": "3.5.4",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+      "integrity": "sha1-sPRHGHyKi+2US4FaZgvd9d610ac=",
+      "requires": {
+        "acorn": "^5.5.0",
+        "acorn-jsx": "^3.0.0"
+      }
+    },
+    "esprima": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+      "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
+    },
+    "esquery": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+      "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=",
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=",
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+    },
+    "eventemitter3": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+      "integrity": "sha1-LT1I+cNGaY/Og6hdfWZOmFNd9uc="
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=",
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "execa": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+      "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+      "requires": {
+        "cross-spawn": "^5.0.1",
+        "get-stream": "^3.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo="
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "external-editor": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+      "integrity": "sha1-BFURz9jRM/OEZnPRBHwVTiFK09U=",
+      "requires": {
+        "chardet": "^0.4.0",
+        "iconv-lite": "^0.4.17",
+        "tmp": "^0.0.33"
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=",
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "extract-zip": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
+      "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
+      "requires": {
+        "concat-stream": "1.6.2",
+        "debug": "2.6.9",
+        "mkdirp": "0.5.1",
+        "yauzl": "2.4.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "fast-deep-equal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
+    },
+    "fd-slicer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "figures": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+      "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+      "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+      "requires": {
+        "flat-cache": "^1.2.1",
+        "object-assign": "^4.0.1"
+      }
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.1",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.3.1",
+        "unpipe": "~1.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "find-cache-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
+      "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^1.0.0",
+        "pkg-dir": "^2.0.0"
+      }
+    },
+    "find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "requires": {
+        "locate-path": "^2.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz",
+      "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=",
+      "requires": {
+        "circular-json": "^0.3.1",
+        "del": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "write": "^0.2.1"
+      }
+    },
+    "flatted": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
+      "integrity": "sha1-VRIrZTbqSWtLRIk+4mCBQdENmRY="
+    },
+    "flush-write-stream": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
+      "integrity": "sha1-xdWG7zivYJdlC0m8QbVfq7GfNb0=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.4"
+      }
+    },
+    "follow-redirects": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
+      "integrity": "sha1-SJ68GY3A5/ZBZ70jsDxMGbV4THY=",
+      "requires": {
+        "debug": "^3.2.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo="
+        }
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "requires": {
+        "null-check": "^1.0.0"
+      }
+    },
+    "fs-extra": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+      "integrity": "sha1-TxicRKoSO4lfcigE9V6iPq3DSOk=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      }
+    },
+    "fs-write-stream-atomic": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+      "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "iferr": "^0.1.5",
+        "imurmurhash": "^0.1.4",
+        "readable-stream": "1 || 2"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "fsevents": {
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+      "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+      "optional": true,
+      "requires": {
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "optional": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "optional": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "4.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "deep-extend": {
+          "version": "0.6.0",
+          "bundled": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "optional": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "optional": true
+        },
+        "minipass": {
+          "version": "2.3.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.2.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.3.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "debug": "^4.1.0",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.12.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.1",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.2.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.6",
+          "bundled": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.4.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.6.0",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "bundled": true,
+          "optional": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.7.0",
+          "bundled": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.1.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2 || 2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "yallist": {
+          "version": "3.0.3",
+          "bundled": true,
+          "optional": true
+        }
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0="
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE="
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
+    },
+    "get-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
+    },
+    "glob": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+      "requires": {
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "global-dirs": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz",
+      "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
+      "requires": {
+        "ini": "^1.3.4"
+      }
+    },
+    "globals": {
+      "version": "11.7.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+      "integrity": "sha1-pYP6pDBVsayncZFL9oJY4vwSVnM="
+    },
+    "globby": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+      "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+      "requires": {
+        "array-union": "^1.0.1",
+        "arrify": "^1.0.0",
+        "glob": "^7.0.3",
+        "object-assign": "^4.0.1",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "got": {
+      "version": "6.7.1",
+      "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
+      "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
+      "requires": {
+        "create-error-class": "^3.0.0",
+        "duplexer3": "^0.1.4",
+        "get-stream": "^3.0.0",
+        "is-redirect": "^1.0.0",
+        "is-retry-allowed": "^1.0.0",
+        "is-stream": "^1.0.0",
+        "lowercase-keys": "^1.0.0",
+        "safe-buffer": "^5.0.1",
+        "timed-out": "^4.0.0",
+        "unzip-response": "^2.0.1",
+        "url-parse-lax": "^1.0.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha1-8nNdwig2dPpnR4sQGBBZNVw2nl4="
+    },
+    "handlebars": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
+      "integrity": "sha1-trN8HO0DBrIh4JT8eso+wjsTG2c=",
+      "requires": {
+        "neo-async": "^2.6.0",
+        "optimist": "^0.6.1",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.20.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+          "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
+          "optional": true
+        },
+        "uglify-js": {
+          "version": "3.5.9",
+          "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.9.tgz",
+          "integrity": "sha512-WpT0RqsDtAWPNJK955DEnb6xjymR8Fn0OlK4TT4pS0ASYsVPqr5ELhgwOwLCP5J5vHeJ4xmMmz3DEgdqC10JeQ==",
+          "optional": true,
+          "requires": {
+            "commander": "~2.20.0",
+            "source-map": "~0.6.1"
+          }
+        }
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=",
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
+      "integrity": "sha1-44q0uF37HgxA/pJlwOm1SFTCOBI=",
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha1-l/I2l3vW4SVAiTD/bePuxigewEc="
+    },
+    "htmlparser2": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+      "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+      "requires": {
+        "domelementtype": "^1.3.0",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "http-proxy": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
+      "integrity": "sha1-etOElGWPhGBeL220Q230EPTlvpo=",
+      "requires": {
+        "eventemitter3": "^3.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
+    },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=",
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      }
+    },
+    "hydrolysis": {
+      "version": "1.25.0",
+      "resolved": "https://registry.npmjs.org/hydrolysis/-/hydrolysis-1.25.0.tgz",
+      "integrity": "sha1-pPsUo3oeA7DbUtiqpXxoInKhTYQ=",
+      "requires": {
+        "acorn": "^3.2.0",
+        "babel-polyfill": "^6.2.0",
+        "doctrine": "^0.7.0",
+        "dom5": "1.1.0",
+        "escodegen": "^1.7.0",
+        "espree": "^3.1.3",
+        "estraverse": "^3.1.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
+        },
+        "doctrine": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz",
+          "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=",
+          "requires": {
+            "esutils": "^1.1.6",
+            "isarray": "0.0.1"
+          }
+        },
+        "dom5": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/dom5/-/dom5-1.1.0.tgz",
+          "integrity": "sha1-Ogx3AMCDq0xNJpOKeLDwxtzDd5Q=",
+          "requires": {
+            "parse5": "^1.4.1"
+          }
+        },
+        "estraverse": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-3.1.0.tgz",
+          "integrity": "sha1-FeKKRGuLgrxwDMyLlseK9NoNbLo="
+        },
+        "esutils": {
+          "version": "1.1.6",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
+          "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U="
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        }
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.23",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+      "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ieee754": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+      "integrity": "sha1-UL8k5bnIu5ivSWTJQc2wkY2ntgs="
+    },
+    "iferr": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
+      "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE="
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha1-Cpf7h2mG6AgcYxFg+PnziRV/AEM="
+    },
+    "import-lazy": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+      "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM="
+    },
+    "import-local": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
+      "integrity": "sha1-Xk/9wD9P5sAJxnKb6yljHC+CJ7w=",
+      "requires": {
+        "pkg-dir": "^2.0.0",
+        "resolve-cwd": "^2.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
+    },
+    "indent-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+      "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok="
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc="
+    },
+    "inquirer": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+      "integrity": "sha1-ndLyrXZdyrH/BEO0kUQqILoifck=",
+      "requires": {
+        "ansi-escapes": "^3.0.0",
+        "chalk": "^2.0.0",
+        "cli-cursor": "^2.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^2.0.4",
+        "figures": "^2.0.0",
+        "lodash": "^4.3.0",
+        "mute-stream": "0.0.7",
+        "run-async": "^2.2.0",
+        "rx-lite": "^4.0.8",
+        "rx-lite-aggregates": "^4.0.8",
+        "string-width": "^2.1.0",
+        "strip-ansi": "^4.0.0",
+        "through": "^2.3.6"
+      }
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "irregular-plurals": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz",
+      "integrity": "sha1-OdQPBbAPZW0Lf6RxIw3TtxSvKHI="
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4="
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "requires": {
+        "builtin-modules": "^1.0.0"
+      }
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha1-HhrfIZ4e62hNaR+dagX/DTCiTXU="
+    },
+    "is-ci": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
+      "integrity": "sha1-43ecjuF/zPQoSI9uKBGH8uYyhBw=",
+      "requires": {
+        "ci-info": "^1.5.0"
+      }
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=",
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0="
+        }
+      }
+    },
+    "is-directory": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+    },
+    "is-glob": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+      "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-installed-globally": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz",
+      "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
+      "requires": {
+        "global-dirs": "^0.1.0",
+        "is-path-inside": "^1.0.0"
+      }
+    },
+    "is-npm": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz",
+      "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ="
+    },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+      "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+      "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0="
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+      "integrity": "sha1-WsSLNF72dTOb1sekipEhELJBz1I=",
+      "requires": {
+        "is-path-inside": "^1.0.0"
+      }
+    },
+    "is-path-inside": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "requires": {
+        "path-is-inside": "^1.0.1"
+      }
+    },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=",
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
+    },
+    "is-redirect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
+      "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-resolvable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+      "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg="
+    },
+    "is-retry-allowed": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
+      "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-symbol": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+      "integrity": "sha1-oFX2rlcZLK7jKeeoYBGLSXqVDzg=",
+      "requires": {
+        "has-symbols": "^1.0.0"
+      }
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0="
+    },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isbinaryfile": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
+      "integrity": "sha1-XW3vPt6/boyoyunDAYOoBLX4voA=",
+      "requires": {
+        "buffer-alloc": "^1.2.0"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+    },
+    "istanbul": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+      "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=",
+      "requires": {
+        "abbrev": "1.0.x",
+        "async": "1.x",
+        "escodegen": "1.8.x",
+        "esprima": "2.7.x",
+        "glob": "^5.0.15",
+        "handlebars": "^4.0.1",
+        "js-yaml": "3.x",
+        "mkdirp": "0.5.x",
+        "nopt": "3.x",
+        "once": "1.x",
+        "resolve": "1.1.x",
+        "supports-color": "^3.1.0",
+        "which": "^1.1.1",
+        "wordwrap": "^1.0.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.0.9",
+          "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+          "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU="
+        },
+        "async": {
+          "version": "1.5.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
+        },
+        "escodegen": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+          "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=",
+          "requires": {
+            "esprima": "^2.7.1",
+            "estraverse": "^1.9.1",
+            "esutils": "^2.0.2",
+            "optionator": "^0.8.1",
+            "source-map": "~0.2.0"
+          }
+        },
+        "esprima": {
+          "version": "2.7.3",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+          "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE="
+        },
+        "estraverse": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+          "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q="
+        },
+        "glob": {
+          "version": "5.0.15",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+          "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+          "requires": {
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "2 || 3",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo="
+        },
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+        },
+        "source-map": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+          "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-instrumenter-loader": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz",
+      "integrity": "sha1-mVe9WSUrNz+uXFK3tRiOb94qCUk=",
+      "requires": {
+        "convert-source-map": "^1.5.0",
+        "istanbul-lib-instrument": "^1.7.3",
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.3.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
+          "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
+          "requires": {
+            "ajv": "^5.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-lib-coverage": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz",
+      "integrity": "sha1-zPftzQoLubj3Kf7rCTBHD5r2ZPA="
+    },
+    "istanbul-lib-instrument": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz",
+      "integrity": "sha1-H1XtEKw8R/K93dUweTUSZ1TQqco=",
+      "requires": {
+        "babel-generator": "^6.18.0",
+        "babel-template": "^6.16.0",
+        "babel-traverse": "^6.18.0",
+        "babel-types": "^6.18.0",
+        "babylon": "^6.18.0",
+        "istanbul-lib-coverage": "^1.2.1",
+        "semver": "^5.3.0"
+      }
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE="
+        }
+      }
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk="
+    },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
+    },
+    "jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "just-extend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
+      "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw="
+    },
+    "karma": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-4.1.0.tgz",
+      "integrity": "sha1-0HOHyXQ6V1tA+vc+ij61Qhwhk+E=",
+      "requires": {
+        "bluebird": "^3.3.0",
+        "body-parser": "^1.16.1",
+        "braces": "^2.3.2",
+        "chokidar": "^2.0.3",
+        "colors": "^1.1.0",
+        "connect": "^3.6.0",
+        "core-js": "^2.2.0",
+        "di": "^0.0.1",
+        "dom-serialize": "^2.2.0",
+        "flatted": "^2.0.0",
+        "glob": "^7.1.1",
+        "graceful-fs": "^4.1.2",
+        "http-proxy": "^1.13.0",
+        "isbinaryfile": "^3.0.0",
+        "lodash": "^4.17.11",
+        "log4js": "^4.0.0",
+        "mime": "^2.3.1",
+        "minimatch": "^3.0.2",
+        "optimist": "^0.6.1",
+        "qjobs": "^1.1.4",
+        "range-parser": "^1.2.0",
+        "rimraf": "^2.6.0",
+        "safe-buffer": "^5.0.1",
+        "socket.io": "2.1.1",
+        "source-map": "^0.6.1",
+        "tmp": "0.0.33",
+        "useragent": "2.3.0"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.11",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+          "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+        }
+      }
+    },
+    "karma-chrome-launcher": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
+      "integrity": "sha1-zxudBxNswY/iOTJ9JGVMPbw2is8=",
+      "requires": {
+        "fs-access": "^1.0.0",
+        "which": "^1.2.1"
+      }
+    },
+    "karma-coverage": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.2.tgz",
+      "integrity": "sha1-zAnc61iagxAayl/nDCh2Re84dok=",
+      "requires": {
+        "dateformat": "^1.0.6",
+        "istanbul": "^0.4.0",
+        "lodash": "^4.17.0",
+        "minimatch": "^3.0.0",
+        "source-map": "^0.5.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+        }
+      }
+    },
+    "karma-mocha": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-1.3.0.tgz",
+      "integrity": "sha1-7qrH/8DiAetjxGdEDStpx883eL8=",
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "karma-sinon": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz",
+      "integrity": "sha1-TjRD8oMP3s/2JNN0cWPxIX2qKpo="
+    },
+    "karma-sourcemap-loader": {
+      "version": "0.3.7",
+      "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz",
+      "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=",
+      "requires": {
+        "graceful-fs": "^4.1.2"
+      }
+    },
+    "karma-webpack": {
+      "version": "4.0.0-rc.6",
+      "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.0-rc.6.tgz",
+      "integrity": "sha1-AqxqR8f8FmyLIIRGBppCRpgIJAU=",
+      "requires": {
+        "async": "^2.0.0",
+        "loader-utils": "^1.1.0",
+        "source-map": "^0.5.6",
+        "webpack-dev-middleware": "^3.2.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+        }
+      }
+    },
+    "kind-of": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+      "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE="
+    },
+    "latest-version": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz",
+      "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=",
+      "requires": {
+        "package-json": "^4.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "lit-element": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.1.0.tgz",
+      "integrity": "sha1-hbw/HaAif0sT3oob6Xgim5+jJ+k=",
+      "requires": {
+        "lit-html": "^1.0.0"
+      }
+    },
+    "lit-html": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0.tgz",
+      "integrity": "sha1-PcN4GoymiptcL/KmHiY2YrmyJns="
+    },
+    "load-json-file": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+      "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^4.0.0",
+        "pify": "^3.0.0",
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "loader-runner": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz",
+      "integrity": "sha1-Am8S/nwxFZkolqwCugIrqSlxuXk="
+    },
+    "loader-utils": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
+      "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
+      "requires": {
+        "big.js": "^3.1.3",
+        "emojis-list": "^2.0.0",
+        "json5": "^0.5.0"
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40="
+    },
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+    },
+    "log-symbols": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+      "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=",
+      "requires": {
+        "chalk": "^2.0.1"
+      }
+    },
+    "log4js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.1.0.tgz",
+      "integrity": "sha1-V5g8akQ1RqjIYH6csEXSoRfCdkQ=",
+      "requires": {
+        "date-format": "^2.0.0",
+        "debug": "^4.1.1",
+        "flatted": "^2.0.0",
+        "rfdc": "^1.1.2",
+        "streamroller": "^1.0.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo="
+        }
+      }
+    },
+    "loglevelnext": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz",
+      "integrity": "sha1-NvxPWZbWZA9Tn/IDuoGWQWgNdaI=",
+      "requires": {
+        "es6-symbol": "^3.1.1",
+        "object.assign": "^4.1.0"
+      }
+    },
+    "lolex": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz",
+      "integrity": "sha1-SpnCJRV51pPGoINEba4OXDhE0/o="
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=",
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "requires": {
+        "currently-unhandled": "^0.4.1",
+        "signal-exit": "^3.0.0"
+      }
+    },
+    "lowercase-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+      "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8="
+    },
+    "lru-cache": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+      "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=",
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "make-dir": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+      "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=",
+      "requires": {
+        "pify": "^3.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8="
+    },
+    "map-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz",
+      "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk="
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "md5.js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+      "integrity": "sha1-tdB7jjIW4+J81yjXL3DR5qNCAF8=",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "meant": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.1.tgz",
+      "integrity": "sha1-ZgRP6i8jIw7IBvtRXv6inETSEV0="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "memory-fs": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+      "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "requires": {
+        "errno": "^0.1.3",
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "meow": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz",
+      "integrity": "sha1-38c9Y6mvxxSl43F2DrXIi5EHiqQ=",
+      "requires": {
+        "camelcase-keys": "^4.0.0",
+        "decamelize-keys": "^1.0.0",
+        "loud-rejection": "^1.0.0",
+        "minimist-options": "^3.0.1",
+        "normalize-package-data": "^2.3.4",
+        "read-pkg-up": "^3.0.0",
+        "redent": "^2.0.0",
+        "trim-newlines": "^2.0.0",
+        "yargs-parser": "^10.0.0"
+      }
+    },
+    "merge-options": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz",
+      "integrity": "sha1-KmSyRFe+zU5NxggoMkfpTOWJqjI=",
+      "requires": {
+        "is-plain-obj": "^1.1"
+      }
+    },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=",
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=",
+      "requires": {
+        "bn.js": "^4.0.0",
+        "brorand": "^1.0.1"
+      }
+    },
+    "mime": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz",
+      "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg=="
+    },
+    "mime-db": {
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+      "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI="
+    },
+    "mime-types": {
+      "version": "2.1.24",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+      "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=",
+      "requires": {
+        "mime-db": "1.40.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI="
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha1-LhlN4ERibUoQ5/f7wAznPoPk1cc="
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "minimist-options": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz",
+      "integrity": "sha1-+6TIGRM54T7PTWG+sD8HAQPz2VQ=",
+      "requires": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0"
+      }
+    },
+    "mississippi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz",
+      "integrity": "sha1-NEKlCPr8KFAEhv7qmUCWduTuWm8=",
+      "requires": {
+        "concat-stream": "^1.5.0",
+        "duplexify": "^3.4.2",
+        "end-of-stream": "^1.1.0",
+        "flush-write-stream": "^1.0.0",
+        "from2": "^2.1.0",
+        "parallel-transform": "^1.1.0",
+        "pump": "^2.0.1",
+        "pumpify": "^1.3.3",
+        "stream-each": "^1.1.0",
+        "through2": "^2.0.0"
+      }
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=",
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha1-bYrlCPWRZ/lA8rWzxKYSrlDJCuY=",
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha1-30boZ9D8Kuxmo0ZitAapzK//Ww8="
+        },
+        "he": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+          "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
+        },
+        "supports-color": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+          "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "move-concurrently": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+      "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
+      "requires": {
+        "aproba": "^1.1.1",
+        "copy-concurrently": "^1.0.0",
+        "fs-write-stream-atomic": "^1.0.8",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.3"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "mute-stream": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+      "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
+    },
+    "nan": {
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+      "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=",
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs="
+    },
+    "neo-async": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
+      "integrity": "sha1-udFeTXHGdikIZUtRg+04t1M0CDU="
+    },
+    "next-tick": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+      "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
+    },
+    "nise": {
+      "version": "1.4.10",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz",
+      "integrity": "sha1-rkagmiZDb66Ro4pgkZNWrm2xQ7Y=",
+      "requires": {
+        "@sinonjs/formatio": "^3.1.0",
+        "@sinonjs/text-encoding": "^0.7.1",
+        "just-extend": "^4.0.2",
+        "lolex": "^2.3.2",
+        "path-to-regexp": "^1.7.0"
+      },
+      "dependencies": {
+        "lolex": {
+          "version": "2.7.5",
+          "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
+          "integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM="
+        }
+      }
+    },
+    "node-libs-browser": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
+      "integrity": "sha1-X5QmPUBPbkR2fXJpAf/wVHjWAN8=",
+      "requires": {
+        "assert": "^1.1.1",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^4.3.0",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "crypto-browserify": "^3.11.0",
+        "domain-browser": "^1.1.1",
+        "events": "^1.0.0",
+        "https-browserify": "^1.0.0",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "0.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.2.4",
+        "querystring-es3": "^0.2.0",
+        "readable-stream": "^2.3.3",
+        "stream-browserify": "^2.0.1",
+        "stream-http": "^2.7.2",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "^0.11.0",
+        "util": "^0.10.3",
+        "vm-browserify": "0.0.4"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+        }
+      }
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "is-builtin-module": "^1.0.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "null-check": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
+      "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0="
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-keys": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+      "integrity": "sha1-CcU4VTd1dTEMymL1W7M0q/97PtI="
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=",
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "object.values": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz",
+      "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=",
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.6.1",
+        "function-bind": "^1.1.0",
+        "has": "^1.0.1"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+      "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+      "requires": {
+        "mimic-fn": "^1.0.0"
+      }
+    },
+    "opn": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz",
+      "integrity": "sha1-y1Reeqt4VivrEao7+rxwQuF2EDU=",
+      "requires": {
+        "is-wsl": "^1.1.0"
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "ora": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-2.1.0.tgz",
+      "integrity": "sha1-bK8oMOuSSUGGHsU6FzeZ4Ai1Hls=",
+      "requires": {
+        "chalk": "^2.3.1",
+        "cli-cursor": "^2.1.0",
+        "cli-spinners": "^1.1.0",
+        "log-symbols": "^2.2.0",
+        "strip-ansi": "^4.0.0",
+        "wcwidth": "^1.0.1"
+      }
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha1-uGvV8MJWkJEcdZD8v8IBDVSzzLg=",
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
+    },
+    "package-json": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz",
+      "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=",
+      "requires": {
+        "got": "^6.7.1",
+        "registry-auth-token": "^3.0.1",
+        "registry-url": "^3.0.3",
+        "semver": "^5.1.0"
+      }
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg="
+    },
+    "parallel-transform": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
+      "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+      "requires": {
+        "cyclist": "~0.2.2",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.1.5"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+      "integrity": "sha1-9r8pOBgzK9DatU77Fgh3JHRebKg=",
+      "requires": {
+        "asn1.js": "^4.0.0",
+        "browserify-aes": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.0",
+        "pbkdf2": "^3.0.3"
+      }
+    },
+    "parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+      "requires": {
+        "error-ex": "^1.3.1",
+        "json-parse-better-errors": "^1.0.1"
+      }
+    },
+    "parse5": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz",
+      "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ="
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
+    },
+    "path": {
+      "version": "0.12.7",
+      "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "requires": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo="
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw="
+    },
+    "path-posix": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
+      "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8="
+    },
+    "path-to-regexp": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+      "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+      "requires": {
+        "isarray": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        }
+      }
+    },
+    "path-type": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+      "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=",
+      "requires": {
+        "pify": "^3.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA="
+    },
+    "pbkdf2": {
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+      "integrity": "sha1-l2wgZTBhexTrsyEUI597CTNuk6Y=",
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "plur": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/plur/-/plur-3.0.1.tgz",
+      "integrity": "sha1-JoZS1gX4FmmbQrhiSN5zyazQanw=",
+      "requires": {
+        "irregular-plurals": "^2.0.0"
+      }
+    },
+    "pluralize": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+      "integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c="
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
+    },
+    "prepend-http": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+      "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
+    },
+    "pretty-bytes": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.1.0.tgz",
+      "integrity": "sha1-Yjfs+9xlJb6u9N5yLMYKWK4ObG0="
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o="
+    },
+    "progress": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+      "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8="
+    },
+    "promise-inflight": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+      "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM="
+    },
+    "proxy-from-env": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
+    },
+    "public-encrypt": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+      "integrity": "sha1-T8ydd6B+SLp1J+fL4N4z0HATMeA=",
+      "requires": {
+        "bn.js": "^4.1.0",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "parse-asn1": "^5.0.0",
+        "randombytes": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=",
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "pumpify": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+      "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=",
+      "requires": {
+        "duplexify": "^3.6.0",
+        "inherits": "^2.0.3",
+        "pump": "^2.0.0"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew="
+    },
+    "puppeteer": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.15.0.tgz",
+      "integrity": "sha512-D2y5kwA9SsYkNUmcBzu9WZ4V1SGHiQTmgvDZSx6sRYFsgV25IebL4V6FaHjF6MbwLK9C6f3G3pmck9qmwM8H3w==",
+      "requires": {
+        "debug": "^4.1.0",
+        "extract-zip": "^1.6.6",
+        "https-proxy-agent": "^2.2.1",
+        "mime": "^2.0.3",
+        "progress": "^2.0.1",
+        "proxy-from-env": "^1.0.0",
+        "rimraf": "^2.6.1",
+        "ws": "^6.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo="
+        },
+        "progress": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+          "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
+        }
+      }
+    },
+    "qjobs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+      "integrity": "sha1-xF6cYYAL0IfviNfiVkI73Unl0HE="
+    },
+    "qs": {
+      "version": "6.7.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+      "integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw="
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
+    },
+    "quick-lru": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
+      "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g="
+    },
+    "randombytes": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+      "integrity": "sha1-0wLFIpSFiISKjTAMkytEwkIx2oA=",
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+      "integrity": "sha1-ySGW/IarQr6YPxvzF3giSTHWFFg=",
+      "requires": {
+        "randombytes": "^2.0.5",
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+    },
+    "raw-body": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+      "integrity": "sha1-oc5vucm8NWylLoklarWQWeE9AzI=",
+      "requires": {
+        "bytes": "3.1.0",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "http-errors": {
+          "version": "1.7.2",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+          "integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.1.1",
+            "statuses": ">= 1.5.0 < 2",
+            "toidentifier": "1.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+          "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM="
+        },
+        "statuses": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+          "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+        }
+      }
+    },
+    "rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha1-zZJL9SAKB1uDwYjNa54hG3/A0+0=",
+      "requires": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "read-pkg": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+      "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+      "requires": {
+        "load-json-file": "^4.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^3.0.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz",
+      "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=",
+      "requires": {
+        "find-up": "^2.0.0",
+        "read-pkg": "^3.0.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha1-DodiKjMlqjPokihcr4tOhGUppSU=",
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "redent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz",
+      "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=",
+      "requires": {
+        "indent-string": "^3.0.0",
+        "strip-indent": "^2.0.0"
+      }
+    },
+    "redux": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
+      "integrity": "sha1-Q2yubMQPvkcnaJ18j65EgI8b/vU=",
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "symbol-observable": "^1.2.0"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.10.5",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+      "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=",
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexpp": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+      "integrity": "sha1-DjUW3Qt5BPQT0tQZPc5GGMOmias="
+    },
+    "registry-auth-token": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
+      "integrity": "sha1-hR/UkDjuy1hpERFa+EUmDuyYPyA=",
+      "requires": {
+        "rc": "^1.1.6",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "registry-url": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
+      "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=",
+      "requires": {
+        "rc": "^1.0.1"
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4="
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "require-uncached": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+      "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+      "requires": {
+        "caller-path": "^0.1.0",
+        "resolve-from": "^1.0.0"
+      }
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
+    },
+    "resolve": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+      "integrity": "sha1-gvHsGaQjrB+9CAsLqwa6NuhKeiY=",
+      "requires": {
+        "path-parse": "^1.0.5"
+      }
+    },
+    "resolve-cwd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+      "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
+      "requires": {
+        "resolve-from": "^3.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+          "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
+        }
+      }
+    },
+    "resolve-from": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+      "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY="
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
+    },
+    "restore-cursor": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+      "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+      "requires": {
+        "onetime": "^2.0.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w="
+    },
+    "rfdc": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
+      "integrity": "sha1-5uctdPXcOd6PU49l4Aw2wYAY40k="
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+      "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
+      "requires": {
+        "glob": "^7.0.5"
+      }
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha1-ocGm9iR1FXe6XQeRTLyShQWFiQw=",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "run-async": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "run-queue": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
+      "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
+      "requires": {
+        "aproba": "^1.1.1"
+      }
+    },
+    "rx-lite": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+      "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ="
+    },
+    "rx-lite-aggregates": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+      "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+      "requires": {
+        "rx-lite": "*"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo="
+    },
+    "schema-utils": {
+      "version": "0.4.7",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
+      "integrity": "sha1-unT1l9K+LqiAExdG7hfQoJPGgYc=",
+      "requires": {
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.5.4",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
+          "integrity": "sha1-JH1SdBENtlNwa1UPzCt5fKKM/Fk=",
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ajv-keywords": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+          "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo="
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+        }
+      }
+    },
+    "semver": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+      "integrity": "sha1-ff3YgUvbfKvHvg+x1zTPtmyUBHc="
+    },
+    "semver-diff": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz",
+      "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=",
+      "requires": {
+        "semver": "^5.0.3"
+      }
+    },
+    "serialize-javascript": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
+      "integrity": "sha1-GqM2FiyIqJDdrVOEuuvJOmVRYf4="
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=",
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha1-N6XPC4HsvGlD3hCbopYNGyZYSuc=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+    },
+    "sinon": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz",
+      "integrity": "sha1-gtujpthfbSGB4eyiwQ2GV8IWHyg=",
+      "requires": {
+        "@sinonjs/commons": "^1.4.0",
+        "@sinonjs/formatio": "^3.2.1",
+        "@sinonjs/samsam": "^3.3.1",
+        "diff": "^3.5.0",
+        "lolex": "^4.0.1",
+        "nise": "^1.4.10",
+        "supports-color": "^5.5.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "slice-ansi": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+      "integrity": "sha1-BE8aSdiEL/MHqta1Be0Xi9lQE00=",
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0"
+      }
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=",
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=",
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=",
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "socket.io": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+      "integrity": "sha1-oGnF/qvuPmshSnW0DOBlLhz7mYA=",
+      "requires": {
+        "debug": "~3.1.0",
+        "engine.io": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.1.1",
+        "socket.io-parser": "~3.2.0"
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
+    },
+    "socket.io-client": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+      "integrity": "sha1-3LOBA0NqtFeN2wJmOK4vIbYjZx8=",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.2.0",
+        "to-array": "0.1.4"
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+      "integrity": "sha1-58Yii2qh+BTmFIrqMltRqpSZ4Hc=",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "source-list-map": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+      "integrity": "sha1-OZO9hzv8SEecyp6jpUeDXHwVSzQ="
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM="
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=",
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM="
+    },
+    "spdx-correct": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz",
+      "integrity": "sha1-GbtAnpG0exrVQVkkP3MSqFjbPC4=",
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha1-LqRQrudPKom/uUUZwH/Nb0EyKXc="
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=",
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz",
+      "integrity": "sha1-4qMDI2ysVLBAMfp6WnnH5wHfhS8="
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=",
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "ssri": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
+      "integrity": "sha1-ujhyycbTOgcEp9cf8EXl7EiZnQY=",
+      "requires": {
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "statuses": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+      "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "requires": {
+        "inherits": "~2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "stream-each": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+      "integrity": "sha1-6+J6DDibBPvMIzZClS4Qcxr6m64=",
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "stream-http": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+      "integrity": "sha1-stJCRpKIpaJ+xP6JM6z2I95lFPw=",
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.6",
+        "to-arraybuffer": "^1.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "stream-shift": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
+    },
+    "streamroller": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.4.tgz",
+      "integrity": "sha1-1IXHYkeW1eLrNBkMea/L8AavteY=",
+      "requires": {
+        "async": "^2.6.1",
+        "date-format": "^2.0.0",
+        "debug": "^3.1.0",
+        "fs-extra": "^7.0.0",
+        "lodash": "^4.17.10"
+      }
+    },
+    "string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+      "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+      "requires": {
+        "ansi-regex": "^3.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+        }
+      }
+    },
+    "strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
+    },
+    "strip-indent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
+      "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g="
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+    },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ="
+    },
+    "table": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
+      "integrity": "sha1-ozRHN1OR52atNNNIbm4q7chNLjY=",
+      "requires": {
+        "ajv": "^5.2.3",
+        "ajv-keywords": "^2.1.0",
+        "chalk": "^2.1.0",
+        "lodash": "^4.17.4",
+        "slice-ansi": "1.0.0",
+        "string-width": "^2.1.1"
+      }
+    },
+    "tapable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz",
+      "integrity": "sha1-DQdqFy49m6CI/SJysmaPuNGUt4w="
+    },
+    "term-size": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
+      "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=",
+      "requires": {
+        "execa": "^0.7.0"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ="
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "through2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+      "requires": {
+        "readable-stream": "^2.1.5",
+        "xtend": "~4.0.1"
+      }
+    },
+    "timed-out": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
+      "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
+    },
+    "timers-browserify": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
+      "integrity": "sha1-HSjj0qrfHVpZlsTp+VYBzQU0gK4=",
+      "requires": {
+        "setimmediate": "^1.0.4"
+      }
+    },
+    "titleize": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.1.tgz",
+      "integrity": "sha1-Ibwk/Mpljq3G0708OPK9FzdptMU="
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=",
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc="
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=",
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM="
+    },
+    "trim-newlines": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz",
+      "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA="
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY="
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw="
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
+    "uglify-es": {
+      "version": "3.3.9",
+      "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+      "integrity": "sha1-DBxPBwC+2NvBJM2zBNJZLKID5nc=",
+      "requires": {
+        "commander": "~2.13.0",
+        "source-map": "~0.6.1"
+      }
+    },
+    "uglifyjs-webpack-plugin": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
+      "integrity": "sha1-dfVIFghYFjoIZD4IbV/v4YpdZ94=",
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "schema-utils": "^0.4.5",
+        "serialize-javascript": "^1.4.0",
+        "source-map": "^0.6.1",
+        "uglify-es": "^3.3.4",
+        "webpack-sources": "^1.1.0",
+        "worker-farm": "^1.5.2"
+      }
+    },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw="
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "unique-filename": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
+      "integrity": "sha1-HWl2k2mtoFgxA6HmrodoG1ZXMjA=",
+      "requires": {
+        "unique-slug": "^2.0.0"
+      }
+    },
+    "unique-slug": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz",
+      "integrity": "sha1-Xp7cbRzo+yZNsYpQfvm9hURFHKY=",
+      "requires": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
+    "unique-string": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
+      "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=",
+      "requires": {
+        "crypto-random-string": "^1.0.0"
+      }
+    },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY="
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E="
+        }
+      }
+    },
+    "unzip-response": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
+      "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
+    },
+    "upath": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
+      "integrity": "sha1-NSVll+RqWB20eT0M5H+prr/J+r0="
+    },
+    "update-notifier": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz",
+      "integrity": "sha1-0HRFk+E/Fh5AassdlAi3LK0Ir/Y=",
+      "requires": {
+        "boxen": "^1.2.1",
+        "chalk": "^2.0.1",
+        "configstore": "^3.0.0",
+        "import-lazy": "^2.1.0",
+        "is-ci": "^1.0.10",
+        "is-installed-globally": "^0.1.0",
+        "is-npm": "^1.0.0",
+        "latest-version": "^3.0.0",
+        "semver-diff": "^2.0.0",
+        "xdg-basedir": "^3.0.0"
+      }
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+        }
+      }
+    },
+    "url-parse-lax": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
+      "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
+      "requires": {
+        "prepend-http": "^1.0.1"
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8="
+    },
+    "useragent": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+      "integrity": "sha1-IX+UOtVAyyEoZYqyP8lg9qiMmXI=",
+      "requires": {
+        "lru-cache": "4.1.x",
+        "tmp": "0.0.x"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=",
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE="
+    },
+    "v8-compile-cache": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz",
+      "integrity": "sha1-pCiyi7JnkHNMT8i8n6EG/M6/amw="
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha1-/JH2uce6FchX9MssXe/uw51PQQo=",
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+      "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
+    },
+    "vulcanize": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/vulcanize/-/vulcanize-1.16.0.tgz",
+      "integrity": "sha1-sM47AETRlK1JCK5PGmxhEKbk1eY=",
+      "requires": {
+        "dom5": "^1.3.1",
+        "es6-promise": "^2.1.0",
+        "hydrolysis": "^1.19.1",
+        "nopt": "^3.0.1",
+        "path-posix": "^1.0.0"
+      }
+    },
+    "watchpack": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+      "integrity": "sha1-S8EsLr6KonenHx0/FNaFx7RGzQA=",
+      "requires": {
+        "chokidar": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "neo-async": "^2.5.0"
+      }
+    },
+    "wcwidth": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+      "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
+      "requires": {
+        "defaults": "^1.0.3"
+      }
+    },
+    "webpack": {
+      "version": "4.23.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.23.1.tgz",
+      "integrity": "sha1-23RnsRZ3GuAgxYvf4qCCJ4W7gjk=",
+      "requires": {
+        "@webassemblyjs/ast": "1.7.10",
+        "@webassemblyjs/helper-module-context": "1.7.10",
+        "@webassemblyjs/wasm-edit": "1.7.10",
+        "@webassemblyjs/wasm-parser": "1.7.10",
+        "acorn": "^5.6.2",
+        "acorn-dynamic-import": "^3.0.0",
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0",
+        "chrome-trace-event": "^1.0.0",
+        "enhanced-resolve": "^4.1.0",
+        "eslint-scope": "^4.0.0",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^2.3.0",
+        "loader-utils": "^1.1.0",
+        "memory-fs": "~0.4.1",
+        "micromatch": "^3.1.8",
+        "mkdirp": "~0.5.0",
+        "neo-async": "^2.5.0",
+        "node-libs-browser": "^2.0.0",
+        "schema-utils": "^0.4.4",
+        "tapable": "^1.1.0",
+        "uglifyjs-webpack-plugin": "^1.2.4",
+        "watchpack": "^1.5.0",
+        "webpack-sources": "^1.3.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.5.4",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
+          "integrity": "sha1-JH1SdBENtlNwa1UPzCt5fKKM/Fk=",
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ajv-keywords": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+          "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo="
+        },
+        "eslint-scope": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz",
+          "integrity": "sha1-UL8wcekzi83EMzF5Sgy1M/ATYXI=",
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+        }
+      }
+    },
+    "webpack-command": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/webpack-command/-/webpack-command-0.4.1.tgz",
+      "integrity": "sha1-P4iq6HwoKS7QqXKTYVouliocZvQ=",
+      "requires": {
+        "@webpack-contrib/config-loader": "^1.2.0",
+        "@webpack-contrib/schema-utils": "^1.0.0-beta.0",
+        "camelcase": "^5.0.0",
+        "chalk": "^2.3.2",
+        "debug": "^3.1.0",
+        "decamelize": "^2.0.0",
+        "enhanced-resolve": "^4.0.0",
+        "import-local": "^1.0.0",
+        "isobject": "^3.0.1",
+        "loader-utils": "^1.1.0",
+        "log-symbols": "^2.2.0",
+        "loud-rejection": "^1.6.0",
+        "meant": "^1.0.1",
+        "meow": "^5.0.0",
+        "merge-options": "^1.0.0",
+        "object.values": "^1.0.4",
+        "opn": "^5.3.0",
+        "ora": "^2.1.0",
+        "plur": "^3.0.0",
+        "pretty-bytes": "^5.0.0",
+        "strip-ansi": "^4.0.0",
+        "text-table": "^0.2.0",
+        "titleize": "^1.0.1",
+        "update-notifier": "^2.3.0",
+        "v8-compile-cache": "^2.0.0",
+        "webpack-log": "^1.1.2",
+        "wordwrap": "^1.0.0"
+      }
+    },
+    "webpack-dev-middleware": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.6.2.tgz",
+      "integrity": "sha1-83onrXwJzX3GfNl2VUE6uqH1WUI=",
+      "requires": {
+        "memory-fs": "^0.4.1",
+        "mime": "^2.3.1",
+        "range-parser": "^1.0.3",
+        "webpack-log": "^2.0.0"
+      },
+      "dependencies": {
+        "webpack-log": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
+          "integrity": "sha1-W3ko4GN1k/EZ0y9iJ8HgrDHhtH8=",
+          "requires": {
+            "ansi-colors": "^3.0.0",
+            "uuid": "^3.3.2"
+          }
+        }
+      }
+    },
+    "webpack-log": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz",
+      "integrity": "sha1-pLNM2msitRjbsKsy5WeWLVxypD0=",
+      "requires": {
+        "chalk": "^2.1.0",
+        "log-symbols": "^2.1.0",
+        "loglevelnext": "^1.0.1",
+        "uuid": "^3.1.0"
+      }
+    },
+    "webpack-sources": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
+      "integrity": "sha1-KijcufH0X+lg2PFJMlK17mUw+oU=",
+      "requires": {
+        "source-list-map": "^2.0.0",
+        "source-map": "~0.6.1"
+      }
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "widest-line": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
+      "integrity": "sha1-dDh2RzDsfvQ4HOTfgvuYpTFCo/w=",
+      "requires": {
+        "string-width": "^2.1.1"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+    },
+    "worker-farm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
+      "integrity": "sha1-rsxAWXb6talVJhgIRvDboojzpKA=",
+      "requires": {
+        "errno": "~0.1.7"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "write": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+      "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "write-file-atomic": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
+      "integrity": "sha1-H/YVdcLipOjlENb6TiQ8zhg5mas=",
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "imurmurhash": "^0.1.4",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "ws": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+      "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+      "requires": {
+        "async-limiter": "~1.0.0"
+      }
+    },
+    "xdg-basedir": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz",
+      "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ="
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    },
+    "xregexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz",
+      "integrity": "sha1-5pgYneSd0qGMxWh7BeF8jkOUMCA="
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms="
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+    },
+    "yargs-parser": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+      "integrity": "sha1-cgImW4n36eny5XZeD+c1qQXtuqg=",
+      "requires": {
+        "camelcase": "^4.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
+        }
+      }
+    },
+    "yauzl": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+      "requires": {
+        "fd-slicer": "~1.0.1"
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    }
+  }
+}
diff --git a/catapult/common/node_runner/node_runner/package.json b/catapult/common/node_runner/node_runner/package.json
index d743e00..526650d 100644
--- a/catapult/common/node_runner/node_runner/package.json
+++ b/catapult/common/node_runner/node_runner/package.json
@@ -8,19 +8,55 @@
   },
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "cd ../../../dashboard/dashboard/spa && karma start --coverage --no-colors"
   },
   "author": "The Chromium Authors",
   "license": "BSD-2-Clause",
   "gypfile": false,
   "private": true,
   "dependencies": {
+    "dot-prop-immutable": "1.5.0",
+    "@chopsui/result-channel": "0.1.0",
+    "@chopsui/batch-iterator": "0.1.0",
+    "@chopsui/chops-button": "0.1.11",
+    "@chopsui/chops-checkbox": "0.1.11",
+    "@chopsui/chops-input": "0.1.11",
+    "@chopsui/chops-loading": "0.1.11",
+    "@chopsui/chops-radio": "0.1.11",
+    "@chopsui/chops-radio-group": "0.1.11",
+    "@chopsui/chops-switch": "0.1.11",
+    "@chopsui/chops-tab": "0.1.11",
+    "@chopsui/chops-tab-bar": "0.1.11",
+    "@chopsui/chops-textarea": "0.1.11",
+    "@chopsui/tsmon-client": "0.0.1",
+    "@chopsui/chops-header": "0.1.5",
+    "@chopsui/chops-signin": "0.1.5",
+    "@polymer/app-route": "^3.0.0",
+    "@polymer/iron-collapse": "^3.0.0",
+    "@polymer/iron-icon": "^3.0.0",
+    "@polymer/iron-iconset-svg": "^3.0.0",
+    "@polymer/polymer": "^3.0.0",
+    "chai": "^4.0.2",
     "dom5": "^1.0.0",
     "escodegen": "^1.11.0",
     "eslint": "^4.0.0",
     "eslint-config-google": "^0.6.0",
     "eslint-plugin-html": "^4.0.0",
     "espree": "^3.0.0",
+    "istanbul-instrumenter-loader": "^3.0.1",
+    "lit-element": "^2.0.0",
+    "karma": "^4.0.0",
+    "karma-chrome-launcher": "^2.2.0",
+    "karma-coverage": "^1.1.2",
+    "karma-mocha": "^1.3.0",
+    "karma-sinon": "^1.0.5",
+    "karma-sourcemap-loader": "^0.3.7",
+    "karma-webpack": "4.0.0-rc.6",
+    "mocha": "^5.2.0",
+    "path": "^0.12.7",
+    "puppeteer": "^1.10.0",
+    "redux": "^4.0.0",
+    "sinon": "^7.2.3",
     "vulcanize": "^1.16.0",
     "webpack": "^4.16.1",
     "webpack-command": "^0.4.1"
diff --git a/catapult/common/py_trace_event/bin/run_tests b/catapult/common/py_trace_event/bin/run_tests
new file mode 100755
index 0000000..b9e1cbe
--- /dev/null
+++ b/catapult/common/py_trace_event/bin/run_tests
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+# Copyright (c) 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+_CATAPULT_PATH = os.path.abspath(
+    os.path.join(os.path.dirname(__file__), '..', '..', '..'))
+
+_PY_TRACE_EVENT_PATH = os.path.abspath(
+    os.path.join(os.path.dirname(__file__), '..'))
+
+
+def _RunTestsOrDie(top_level_dir):
+  # Need everything in one process for tracing to work.
+  exit_code = run_with_typ.Run(
+      top_level_dir, path=[_PY_TRACE_EVENT_PATH], jobs=1)
+  if exit_code:
+    sys.exit(exit_code)
+
+
+def _AddToPathIfNeeded(path):
+  if path not in sys.path:
+    sys.path.insert(0, path)
+
+
+if __name__ == '__main__':
+  _AddToPathIfNeeded(_CATAPULT_PATH)
+
+  from catapult_build import run_with_typ
+
+  _RunTestsOrDie(_PY_TRACE_EVENT_PATH)
+
diff --git a/catapult/common/py_trace_event/py_trace_event/__init__.py b/catapult/common/py_trace_event/py_trace_event/__init__.py
index b8b6630..2cd8dd1 100644
--- a/catapult/common/py_trace_event/py_trace_event/__init__.py
+++ b/catapult/common/py_trace_event/py_trace_event/__init__.py
@@ -6,4 +6,7 @@
 
 SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
 PY_UTILS = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', 'py_utils'))
+PROTOBUF = os.path.abspath(os.path.join(
+    SCRIPT_DIR, '..', 'third_party', 'protobuf'))
 sys.path.append(PY_UTILS)
+sys.path.append(PROTOBUF)
diff --git a/catapult/common/py_trace_event/py_trace_event/run_tests b/catapult/common/py_trace_event/py_trace_event/run_tests
deleted file mode 100755
index 7f9673d..0000000
--- a/catapult/common/py_trace_event/py_trace_event/run_tests
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-import logging
-import optparse
-import os
-import platform
-import re
-import sys
-import types
-import traceback
-import unittest
-
-
-def discover(dir, filters):
-  if hasattr(unittest.TestLoader, 'discover'):
-    return unittest.TestLoader().discover(dir, '*')
-
-  # poor mans unittest.discover
-  loader = unittest.TestLoader()
-  subsuites = []
-
-  for (dirpath, dirnames, filenames) in os.walk(dir):
-    for filename in [x for x in filenames if re.match('.*_test\.py$', x)]:
-      if filename.startswith('.') or filename.startswith('_'):
-        continue
-      fqn = dirpath.replace(
-          '/', '.') + '.' + re.match('(.+)\.py$', filename).group(1)
-
-      # load the test
-      try:
-        module = __import__(fqn,fromlist=[True])
-      except:
-        print "While importing [%s]\n" % fqn
-        traceback.print_exc()
-        continue
-
-      def test_is_selected(name):
-        for f in filters:
-          if re.search(f,name):
-            return True
-        return False
-
-      if hasattr(module, 'suite'):
-        base_suite = module.suite()
-      else:
-        base_suite = loader.loadTestsFromModule(module)
-      new_suite = unittest.TestSuite()
-      for t in base_suite:
-        if isinstance(t, unittest.TestSuite):
-          for i in t:
-            if test_is_selected(i.id()):
-              new_suite.addTest(i)
-        elif isinstance(t, unittest.TestCase):
-          if test_is_selected(t.id()):
-            new_suite.addTest(t)
-        else:
-          raise Exception("Wtf, expected TestSuite or TestCase, got %s" % t)
-
-      if new_suite.countTestCases():
-        subsuites.append(new_suite)
-
-  return unittest.TestSuite(subsuites)
-
-
-def main():
-  parser = optparse.OptionParser()
-  parser.add_option(
-      '-v', '--verbose', action='count', default=0,
-      help='Increase verbosity level (repeat as needed)')
-  parser.add_option('--debug', dest='debug', action='store_true', default=False,
-                    help='Break into pdb when an assertion fails')
-  parser.add_option('--incremental', dest='incremental', action='store_true',
-                    default=False, help='Run tests one at a time.')
-  parser.add_option('--stop', dest='stop_on_error', action='store_true',
-                    default=False, help='Stop running tests on error.')
-  (options, args) = parser.parse_args()
-
-  if options.verbose >= 2:
-    logging.basicConfig(level=logging.DEBUG)
-  elif options.verbose:
-    logging.basicConfig(level=logging.INFO)
-  else:
-    logging.basicConfig(level=logging.WARNING)
-
-  # install hook on set_trace if --debug
-  if options.debug:
-    import exceptions
-    class DebuggingAssertionError(exceptions.AssertionError):
-      def __init__(self, *args):
-        exceptions.AssertionError.__init__(self, *args)
-        print "Assertion failed, entering PDB..."
-        import pdb
-        if hasattr(sys, '_getframe'):
-          pdb.Pdb().set_trace(sys._getframe().f_back.f_back)
-        else:
-          pdb.set_trace()
-    unittest.TestCase.failureException = DebuggingAssertionError
-
-    def hook(*args):
-      import traceback, pdb
-      traceback.print_exception(*args)
-      pdb.pm()
-    sys.excepthook = hook
-
-    import browser
-    browser.debug_mode = True
-
-  else:
-    def hook(exc, value, tb):
-      import traceback
-      if not str(value).startswith("_noprint"):
-        traceback.print_exception(exc, value, tb)
-      import src.message_loop
-      if src.message_loop.is_main_loop_running():
-        if not str(value).startswith("_noprint"):
-          print "Untrapped exception! Exiting message loop with exception."
-        src.message_loop.quit_main_loop(quit_with_exception=True)
-
-    sys.excepthook = hook
-
-  # make sure cwd is the base directory!
-  os.chdir(os.path.dirname(__file__))
-
-  if len(args) > 0:
-    suites = discover('trace_event_impl', args)
-  else:
-    suites = discover('trace_event_impl', ['.*'])
-
-  r = unittest.TextTestRunner()
-  if not options.incremental:
-    res = r.run(suites)
-    if res.wasSuccessful():
-      return 0
-    return 255
-  else:
-    ok = True
-    for s in suites:
-      if isinstance(s, unittest.TestSuite):
-        for t in s:
-          print '--------------------------------------------------------------'
-          print 'Running %s' % str(t)
-          res = r.run(t)
-          if not res.wasSuccessful():
-            ok = False
-            if options.stop_on_error:
-              break
-        if ok == False and options.stop_on_error:
-          break
-      else:
-        res = r.run(s)
-        if not res.wasSuccessful():
-          ok = False
-          if options.stop_on_error:
-            break
-    if ok:
-      return 0
-    return 255
-
-
-if __name__ == "__main__":
-  sys.exit(main())
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event.py b/catapult/common/py_trace_event/py_trace_event/trace_event.py
index 7745963..f87c278 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event.py
@@ -66,15 +66,20 @@
 # used in class definition scope.
 TracedMetaClass = type
 
+
 if trace_event_impl:
   import time
 
+  # Trace file formats
+  JSON = trace_event_impl.JSON
+  JSON_WITH_METADATA = trace_event_impl.JSON_WITH_METADATA
+  PROTOBUF = trace_event_impl.PROTOBUF
 
   def trace_is_enabled():
     return trace_event_impl.trace_is_enabled()
 
-  def trace_enable(logfile):
-    return trace_event_impl.trace_enable(logfile)
+  def trace_enable(logfile, format=None):
+    return trace_event_impl.trace_enable(logfile, format)
 
   def trace_disable():
     return trace_event_impl.trace_disable()
@@ -94,6 +99,9 @@
     trace_event_impl.add_trace_event("M", trace_time.Now(), "__metadata",
                                      "thread_name", {"name": thread_name})
 
+  def trace_add_benchmark_metadata(*args, **kwargs):
+    trace_event_impl.trace_add_benchmark_metadata(*args, **kwargs)
+
   def trace(name, **kwargs):
     return trace_event_impl.trace(name, **kwargs)
 
@@ -124,6 +132,11 @@
 else:
   import contextlib
 
+  # Trace file formats
+  JSON = None
+  JSON_WITH_METADATA = None
+  PROTOBUF = None
+
   def trace_enable():
     raise TraceException(
         "Cannot enable trace_event. No trace_event_impl module found.")
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py
index 5bb13ad..434a351 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py
@@ -47,7 +47,8 @@
 
 
   def test_func_names_work(self):
-    self.assertEquals('__main__.traced_func',
+    expected_method_name = __name__ + '.traced_func'
+    self.assertEquals(expected_method_name,
                       self._get_decorated_method_name(traced_func))
 
   def test_method_names_work(self):
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py
index 2d69f08..7af86da 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py
@@ -7,24 +7,47 @@
 import sys
 import time
 import threading
+import multiprocessing
+import multiprocessing_shim
 
+from py_trace_event.trace_event_impl import perfetto_trace_writer
 from py_trace_event import trace_time
 
 from py_utils import lock
 
 
+# Trace file formats:
+
+# Legacy format: json list of events.
+# Events can be written from multiple processes, but since no process
+# can be sure that it is the last one, nobody writes the closing ']'.
+# So the resulting file is not technically correct json.
+JSON = "json"
+
+# Full json with events and metadata.
+# This format produces correct json ready to feed into TraceDataBuilder.
+# Note that it is the responsibility of the user of py_trace_event to make sure
+# that trace_disable() is called after all child processes have finished.
+JSON_WITH_METADATA = "json_with_metadata"
+
+# Perfetto protobuf trace format.
+PROTOBUF = "protobuf"
+
+
 _lock = threading.Lock()
 
 _enabled = False
 _log_file = None
 
 _cur_events = [] # events that have yet to be buffered
+_benchmark_metadata = {}
 
 _tls = threading.local() # tls used to detect forking/etc
 _atexit_regsitered_for_pid = None
 
 _control_allowed = True
 
+_original_multiprocessing_process = multiprocessing.Process
 
 class TraceException(Exception):
   pass
@@ -48,11 +71,64 @@
   global _control_allowed
   _control_allowed = False
 
-def trace_enable(log_file=None):
-  _trace_enable(log_file)
+def trace_enable(log_file=None, format=None):
+  """ Enable tracing.
+
+  Args:
+    log_file: file to write trace into. Can be a file-like object,
+      a name of file, or None. If None, file name is constructed
+      from executable name.
+    format: trace file format. See trace_event.py for available options.
+  """
+  if format is None:
+    format = JSON
+  _trace_enable(log_file, format)
+
+def _write_header():
+  tid = threading.current_thread().ident
+  if not tid:
+    tid = os.getpid()
+
+  if _format == PROTOBUF:
+    tid = threading.current_thread().ident
+    perfetto_trace_writer.write_thread_descriptor_event(
+        output=_log_file,
+        pid=os.getpid(),
+        tid=tid,
+        ts=trace_time.Now(),
+    )
+    perfetto_trace_writer.write_event(
+        output=_log_file,
+        ph="M",
+        category="process_argv",
+        name="process_argv",
+        ts=trace_time.Now(),
+        args=sys.argv,
+        tid=tid,
+    )
+  else:
+    if _format == JSON:
+      _log_file.write('[')
+    elif _format == JSON_WITH_METADATA:
+      _log_file.write('{"traceEvents": [\n')
+    else:
+      raise TraceException("Unknown format: %s" % _format)
+    json.dump({
+        "ph": "M",
+        "category": "process_argv",
+        "pid": os.getpid(),
+        "tid": threading.current_thread().ident,
+        "ts": trace_time.Now(),
+        "name": "process_argv",
+        "args": {"argv": sys.argv},
+    }, _log_file)
+    _log_file.write('\n')
+
 
 @_locked
-def _trace_enable(log_file=None):
+def _trace_enable(log_file=None, format=None):
+  global _format
+  _format = format
   global _enabled
   if _enabled:
     raise TraceException("Already enabled")
@@ -65,15 +141,18 @@
       n = 'trace_event'
     else:
       n = sys.argv[0]
-    log_file = open("%s.json" % n, "ab", False)
-    _note("trace_event: tracelog name is %s.json" % n)
+    if _format == PROTOBUF:
+      log_file = open("%s.pb" % n, "ab", False)
+    else:
+      log_file = open("%s.json" % n, "ab", False)
   elif isinstance(log_file, basestring):
-    _note("trace_event: tracelog name is %s" % log_file)
     log_file = open("%s" % log_file, "ab", False)
   elif not hasattr(log_file, 'fileno'):
     raise TraceException(
         "Log file must be None, a string, or file-like object with a fileno()")
 
+  _note("trace_event: tracelog name is %s" % log_file)
+
   _log_file = log_file
   with lock.FileLock(_log_file, lock.LOCK_EX):
     _log_file.seek(0, os.SEEK_END)
@@ -82,19 +161,13 @@
     creator = lastpos == 0
     if creator:
       _note("trace_event: Opened new tracelog, lastpos=%i", lastpos)
-      _log_file.write('[')
-
-      tid = threading.current_thread().ident
-      if not tid:
-        tid = os.getpid()
-      x = {"ph": "M", "category": "process_argv",
-           "pid": os.getpid(), "tid": threading.current_thread().ident,
-           "ts": trace_time.Now(),
-           "name": "process_argv", "args": {"argv": sys.argv}}
-      _log_file.write("%s\n" % json.dumps(x))
+      _write_header()
     else:
       _note("trace_event: Opened existing tracelog")
     _log_file.flush()
+  # Monkeypatch in our process replacement for the multiprocessing.Process class
+  if multiprocessing.Process != multiprocessing_shim.ProcessShim:
+      multiprocessing.Process = multiprocessing_shim.ProcessShim
 
 @_locked
 def trace_flush():
@@ -110,22 +183,52 @@
     return
   _enabled = False
   _flush(close=True)
+  multiprocessing.Process = _original_multiprocessing_process
+
+def _write_cur_events():
+  if _format == PROTOBUF:
+    for e in _cur_events:
+      perfetto_trace_writer.write_event(
+          output=_log_file,
+          ph=e["ph"],
+          category=e["category"],
+          name=e["name"],
+          ts=e["ts"],
+          args=e["args"],
+          tid=threading.current_thread().ident,
+      )
+  elif _format in (JSON, JSON_WITH_METADATA):
+    for e in _cur_events:
+      _log_file.write(",\n")
+      json.dump(e, _log_file)
+  else:
+    raise TraceException("Unknown format: %s" % _format)
+  del _cur_events[:]
+
+def _write_footer():
+  if _format in [JSON, PROTOBUF]:
+    # In JSON format we might not be the only process writing to this logfile.
+    # So, we will simply close the file rather than writing the trailing ] that
+    # it technically requires. The trace viewer understands this and
+    # will insert a trailing ] during loading.
+    # In PROTOBUF format there's no need for a footer. The metadata has already
+    # been written in a special proto message.
+    pass
+  elif _format == JSON_WITH_METADATA:
+    _log_file.write('],\n"metadata": ')
+    json.dump(_benchmark_metadata, _log_file)
+    _log_file.write('}')
+  else:
+    raise TraceException("Unknown format: %s" % _format)
 
 def _flush(close=False):
   global _log_file
   with lock.FileLock(_log_file, lock.LOCK_EX):
     _log_file.seek(0, os.SEEK_END)
     if len(_cur_events):
-      _log_file.write(",\n")
-      _log_file.write(",\n".join([json.dumps(e) for e in _cur_events]))
-      del _cur_events[:]
-
+      _write_cur_events()
     if close:
-      # We might not be the only process writing to this logfile. So,
-      # we will simply close the file rather than writign the trailing ] that
-      # it technically requires. The trace viewer understands that this may
-      # happen and will insert a trailing ] during loading.
-      pass
+      _write_footer()
     _log_file.flush()
 
   if close:
@@ -157,13 +260,15 @@
       tid = os.getpid()
     _tls.tid = tid
 
-  _cur_events.append({"ph": ph,
-                      "category": category,
-                      "pid": _tls.pid,
-                      "tid": _tls.tid,
-                      "ts": ts,
-                      "name": name,
-                      "args": args or {}});
+  _cur_events.append({
+      "ph": ph,
+      "category": category,
+      "pid": _tls.pid,
+      "tid": _tls.tid,
+      "ts": ts,
+      "name": name,
+      "args": args or {},
+  });
 
 def trace_begin(name, args=None):
   add_trace_event("B", trace_time.Now(), "python", name, args)
@@ -175,6 +280,82 @@
   add_trace_event("M", trace_time.Now(), "__metadata", "thread_name",
                   {"name": thread_name})
 
+def trace_add_benchmark_metadata(
+    benchmark_start_time_us,
+    story_run_time_us,
+    benchmark_name,
+    benchmark_description,
+    story_name,
+    story_tags,
+    story_run_index,
+    label=None,
+    had_failures=None,
+):
+  """ Add benchmark metadata to be written to trace file.
+
+  Args:
+    benchmark_start_time_us: Benchmark start time in microseconds.
+    story_run_time_us: Story start time in microseconds.
+    benchmark_name: Name of the benchmark.
+    benchmark_description: Description of the benchmark.
+    story_name: Name of the story.
+    story_tags: List of story tags.
+    story_run_index: Index of the story run.
+    label: Optional label.
+    had_failures: Whether this story run failed.
+  """
+  global _benchmark_metadata
+  if _format == PROTOBUF:
+    # Write metadata immediately.
+    perfetto_trace_writer.write_metadata(
+        output=_log_file,
+        benchmark_start_time_us=benchmark_start_time_us,
+        story_run_time_us=story_run_time_us,
+        benchmark_name=benchmark_name,
+        benchmark_description=benchmark_description,
+        story_name=story_name,
+        story_tags=story_tags,
+        story_run_index=story_run_index,
+        label=label,
+        had_failures=had_failures,
+    )
+  elif _format == JSON_WITH_METADATA:
+    # Store metadata to write it in the footer.
+    telemetry_metadata_for_json = {
+        "benchmarkStart": benchmark_start_time_us / 1000.0,
+        "traceStart": story_run_time_us / 1000.0,
+        "benchmarks": [benchmark_name],
+        "benchmarkDescriptions": [benchmark_description],
+        "stories": [story_name],
+        "storyTags": story_tags,
+        "storysetRepeats": [story_run_index],
+    }
+    if label:
+      telemetry_metadata_for_json["labels"] = [label]
+    if had_failures:
+      telemetry_metadata_for_json["hadFailures"] = [had_failures]
+
+    _benchmark_metadata = {
+        # TODO(crbug.com/948633): For right now, we use "TELEMETRY" as the
+        # clock domain to guarantee that Telemetry is given its own clock
+        # domain. Telemetry isn't really a clock domain, though: it's a
+        # system that USES a clock domain like LINUX_CLOCK_MONOTONIC or
+        # WIN_QPC. However, there's a chance that a Telemetry controller
+        # running on Linux (using LINUX_CLOCK_MONOTONIC) is interacting
+        # with an Android phone (also using LINUX_CLOCK_MONOTONIC, but
+        # on a different machine). The current logic collapses clock
+        # domains based solely on the clock domain string, but we really
+        # should to collapse based on some (device ID, clock domain ID)
+        # tuple. Giving Telemetry its own clock domain is a work-around
+        # for this.
+        "clock-domain": "TELEMETRY",
+        "telemetry": telemetry_metadata_for_json,
+    }
+  elif _format == JSON:
+    raise TraceException("Can't write metadata in JSON format")
+  else:
+    raise TraceException("Unknown format: %s" % _format)
+
 def _trace_disable_atexit():
   trace_disable()
 
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py
index 99a0621..6c03ea8 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py
@@ -5,38 +5,34 @@
 import logging
 import os
 import sys
-import tempfile
 import unittest
 
 from log import *
 from parsed_trace_events import *
+from py_utils import tempfile_ext
 
 
 class LogIOTest(unittest.TestCase):
   def test_enable_with_file(self):
-    file = tempfile.NamedTemporaryFile()
-    trace_enable(open(file.name, 'w+'))
-    trace_disable()
-    e = ParsedTraceEvents(trace_filename = file.name)
-    file.close()
-    self.assertTrue(len(e) > 0)
+    with tempfile_ext.TemporaryFileName() as filename:
+      trace_enable(open(filename, 'w+'))
+      trace_disable()
+      e = ParsedTraceEvents(trace_filename=filename)
+      self.assertTrue(len(e) > 0)
 
   def test_enable_with_filename(self):
-    file = tempfile.NamedTemporaryFile()
-    trace_enable(file.name)
-    trace_disable()
-    e = ParsedTraceEvents(trace_filename = file.name)
-    file.close()
-    self.assertTrue(len(e) > 0)
+    with tempfile_ext.TemporaryFileName() as filename:
+      trace_enable(filename)
+      trace_disable()
+      e = ParsedTraceEvents(trace_filename=filename)
+      self.assertTrue(len(e) > 0)
 
   def test_enable_with_implicit_filename(self):
     expected_filename = "%s.json" % sys.argv[0]
     def do_work():
-      file = tempfile.NamedTemporaryFile()
       trace_enable()
       trace_disable()
-      e = ParsedTraceEvents(trace_filename = expected_filename)
-      file.close()
+      e = ParsedTraceEvents(trace_filename=expected_filename)
       self.assertTrue(len(e) > 0)
     try:
       do_work()
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py
index 4ede79b..7aaa3fa 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py
@@ -10,7 +10,8 @@
 class TracedMetaClass(type):
   def __new__(cls, name, bases, attrs):
     for attr_name, attr_value in attrs.iteritems():
-      if isinstance(attr_value, types.FunctionType):
+      if (not attr_name.startswith('_') and
+          isinstance(attr_value, types.FunctionType)):
         attrs[attr_name] = decorators.traced(attr_value)
 
     return super(TracedMetaClass, cls).__new__(cls, name, bases, attrs)
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py
index 9796bdf..c2295ed 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py
@@ -86,7 +86,3 @@
 
   def __repr__(self):
     return self._proc.__repr__()
-
-# Monkeypatch in our process replacement.
-if multiprocessing.Process != ProcessShim:
-  multiprocessing.Process = ProcessShim
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_proto_classes.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_proto_classes.py
new file mode 100644
index 0000000..2da179b
--- /dev/null
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_proto_classes.py
@@ -0,0 +1,222 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+""" Classes representing perfetto trace protobuf messages.
+
+This module makes use of neither python-protobuf library nor python classes
+compiled from .proto definitions, because currently there's no way to
+deploy those to all the places where telemetry is run.
+
+TODO(crbug.com/944078): Remove this module after the python-protobuf library
+is deployed to all the bots.
+
+Definitions of perfetto messages can be found here:
+https://android.googlesource.com/platform/external/perfetto/+/refs/heads/master/protos/perfetto/trace/
+"""
+
+import encoder
+import wire_format
+
+
+class TracePacket(object):
+  def __init__(self):
+    self.interned_data = None
+    self.thread_descriptor = None
+    self.incremental_state_cleared = None
+    self.track_event = None
+    self.trusted_packet_sequence_id = None
+    self.chrome_benchmark_metadata = None
+
+  def encode(self):
+    parts = []
+    if self.trusted_packet_sequence_id is not None:
+      writer = encoder.UInt32Encoder(10, False, False)
+      writer(parts.append, self.trusted_packet_sequence_id)
+    if self.track_event is not None:
+      tag = encoder.TagBytes(11, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.track_event.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+    if self.interned_data is not None:
+      tag = encoder.TagBytes(12, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.interned_data.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+    if self.incremental_state_cleared is not None:
+      writer = encoder.BoolEncoder(41, False, False)
+      writer(parts.append, self.incremental_state_cleared)
+    if self.thread_descriptor is not None:
+      tag = encoder.TagBytes(44, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.thread_descriptor.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+    if self.chrome_benchmark_metadata is not None:
+      tag = encoder.TagBytes(48, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.chrome_benchmark_metadata.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+
+    return b"".join(parts)
+
+
+class InternedData(object):
+  def __init__(self):
+    self.event_category = None
+    self.legacy_event_name = None
+
+  def encode(self):
+    parts = []
+    if self.event_category is not None:
+      tag = encoder.TagBytes(1, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.event_category.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+    if self.legacy_event_name is not None:
+      tag = encoder.TagBytes(2, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.legacy_event_name.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+
+    return b"".join(parts)
+
+
+class EventCategory(object):
+  def __init__(self):
+    self.iid = None
+    self.name = None
+
+  def encode(self):
+    if (self.iid is None or self.name is None):
+      raise RuntimeError("Missing mandatory fields.")
+
+    parts = []
+    writer = encoder.UInt32Encoder(1, False, False)
+    writer(parts.append, self.iid)
+    writer = encoder.StringEncoder(2, False, False)
+    writer(parts.append, self.name)
+
+    return b"".join(parts)
+
+
+LegacyEventName = EventCategory
+
+
+class ThreadDescriptor(object):
+  def __init__(self):
+    self.pid = None
+    self.tid = None
+    self.reference_timestamp_us = None
+
+  def encode(self):
+    if (self.pid is None or self.tid is None or
+        self.reference_timestamp_us is None):
+      raise RuntimeError("Missing mandatory fields.")
+
+    parts = []
+    writer = encoder.UInt32Encoder(1, False, False)
+    writer(parts.append, self.pid)
+    writer = encoder.UInt32Encoder(2, False, False)
+    writer(parts.append, self.tid)
+    writer = encoder.Int64Encoder(6, False, False)
+    writer(parts.append, self.reference_timestamp_us)
+
+    return b"".join(parts)
+
+
+class TrackEvent(object):
+  def __init__(self):
+    self.timestamp_absolute_us = None
+    self.timestamp_delta_us = None
+    self.legacy_event = None
+    self.category_iids = None
+
+  def encode(self):
+    parts = []
+    if self.timestamp_delta_us is not None:
+      writer = encoder.Int64Encoder(1, False, False)
+      writer(parts.append, self.timestamp_delta_us)
+    if self.category_iids is not None:
+      writer = encoder.UInt32Encoder(3, is_repeated=True, is_packed=False)
+      writer(parts.append, self.category_iids)
+    if self.legacy_event is not None:
+      tag = encoder.TagBytes(6, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      data = self.legacy_event.encode()
+      length = encoder._VarintBytes(len(data))
+      parts += [tag, length, data]
+    if self.timestamp_absolute_us is not None:
+      writer = encoder.Int64Encoder(16, False, False)
+      writer(parts.append, self.timestamp_absolute_us)
+
+    return b"".join(parts)
+
+
+class LegacyEvent(object):
+  def __init__(self):
+    self.phase = None
+    self.name_iid = None
+
+  def encode(self):
+    parts = []
+    if self.name_iid is not None:
+      writer = encoder.UInt32Encoder(1, False, False)
+      writer(parts.append, self.name_iid)
+    if self.phase is not None:
+      writer = encoder.Int32Encoder(2, False, False)
+      writer(parts.append, self.phase)
+
+    return b"".join(parts)
+
+
+class ChromeBenchmarkMetadata(object):
+  def __init__(self):
+    self.benchmark_start_time_us = None
+    self.story_run_time_us = None
+    self.benchmark_name = None
+    self.benchmark_description = None
+    self.story_name = None
+    self.story_tags = None
+    self.story_run_index = None
+    self.label = None
+    self.had_failures = None
+
+  def encode(self):
+    parts = []
+    if self.benchmark_start_time_us is not None:
+      writer = encoder.Int64Encoder(1, False, False)
+      writer(parts.append, self.benchmark_start_time_us)
+    if self.story_run_time_us is not None:
+      writer = encoder.Int64Encoder(2, False, False)
+      writer(parts.append, self.story_run_time_us)
+    if self.benchmark_name is not None:
+      writer = encoder.StringEncoder(3, False, False)
+      writer(parts.append, self.benchmark_name)
+    if self.benchmark_description is not None:
+      writer = encoder.StringEncoder(4, False, False)
+      writer(parts.append, self.benchmark_description)
+    if self.label is not None:
+      writer = encoder.StringEncoder(5, False, False)
+      writer(parts.append, self.label)
+    if self.story_name is not None:
+      writer = encoder.StringEncoder(6, False, False)
+      writer(parts.append, self.story_name)
+    if self.story_tags is not None:
+      writer = encoder.StringEncoder(7, is_repeated=True, is_packed=False)
+      writer(parts.append, self.story_tags)
+    if self.story_run_index is not None:
+      writer = encoder.Int32Encoder(8, False, False)
+      writer(parts.append, self.story_run_index)
+    if self.had_failures is not None:
+      writer = encoder.BoolEncoder(9, False, False)
+      writer(parts.append, self.had_failures)
+
+    return b"".join(parts)
+
+
+def write_trace_packet(output, trace_packet):
+  tag = encoder.TagBytes(1, wire_format.WIRETYPE_LENGTH_DELIMITED)
+  output.write(tag)
+  binary_data = trace_packet.encode()
+  encoder._EncodeVarint(output.write, len(binary_data))
+  output.write(binary_data)
+
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer.py
new file mode 100644
index 0000000..3780953
--- /dev/null
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer.py
@@ -0,0 +1,166 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+""" Functions to write trace data in perfetto protobuf format.
+"""
+
+import collections
+
+import perfetto_proto_classes as proto
+
+
+
+# Dicts of strings for interning.
+# Note that each thread has its own interning index.
+_interned_categories_by_tid = collections.defaultdict(dict)
+_interned_event_names_by_tid = collections.defaultdict(dict)
+
+# Trusted sequence ids from telemetry should not overlap with
+# trusted sequence ids from other trace producers. Chrome assigns
+# sequence ids incrementally starting from 1 and we expect all its ids
+# to be well below 10000. Starting from 2^20 will give us enough
+# confidence that it will not overlap.
+_next_sequence_id = 1<<20
+_sequence_ids = {}
+
+# Timestamp of the last event from each thread. Used for delta-encoding
+# of timestamps.
+_last_timestamps = {}
+
+
+def _get_sequence_id(tid):
+  global _sequence_ids
+  global _next_sequence_id
+  if tid not in _sequence_ids:
+    _sequence_ids[tid] = _next_sequence_id
+    _next_sequence_id += 1
+  return _sequence_ids[tid]
+
+
+def _intern_category(category, trace_packet, tid):
+  global _interned_categories_by_tid
+  categories = _interned_categories_by_tid[tid]
+  if category not in categories:
+    # note that interning indices start from 1
+    categories[category] = len(categories) + 1
+    if trace_packet.interned_data is None:
+      trace_packet.interned_data = proto.InternedData()
+    trace_packet.interned_data.event_category = proto.EventCategory()
+    trace_packet.interned_data.event_category.iid = categories[category]
+    trace_packet.interned_data.event_category.name = category
+  return categories[category]
+
+
+def _intern_event_name(event_name, trace_packet, tid):
+  global _interned_event_names_by_tid
+  event_names = _interned_event_names_by_tid[tid]
+  if event_name not in event_names:
+    # note that interning indices start from 1
+    event_names[event_name] = len(event_names) + 1
+    if trace_packet.interned_data is None:
+      trace_packet.interned_data = proto.InternedData()
+    trace_packet.interned_data.legacy_event_name = proto.LegacyEventName()
+    trace_packet.interned_data.legacy_event_name.iid = event_names[event_name]
+    trace_packet.interned_data.legacy_event_name.name = event_name
+  return event_names[event_name]
+
+
+def write_thread_descriptor_event(output, pid, tid, ts):
+  """ Write the first event in a sequence.
+
+  Call this function before writing any other events.
+  Note that this function is NOT thread-safe.
+
+  Args:
+    output: a file-like object to write events into.
+    pid: process ID.
+    tid: thread ID.
+    ts: timestamp in microseconds.
+  """
+  global _last_timestamps
+  ts_us = int(ts)
+  _last_timestamps[tid] = ts_us
+
+  thread_descriptor_packet = proto.TracePacket()
+  thread_descriptor_packet.trusted_packet_sequence_id = _get_sequence_id(tid)
+  thread_descriptor_packet.thread_descriptor = proto.ThreadDescriptor()
+  thread_descriptor_packet.thread_descriptor.pid = pid
+  # Thread ID from threading module doesn't fit into int32.
+  # But we don't need the exact thread ID, just some number to
+  # distinguish one thread from another. We assume that the last 31 bits
+  # will do for that purpose.
+  thread_descriptor_packet.thread_descriptor.tid = tid & 0x7FFFFFFF
+  thread_descriptor_packet.thread_descriptor.reference_timestamp_us = ts_us
+  thread_descriptor_packet.incremental_state_cleared = True;
+
+  proto.write_trace_packet(output, thread_descriptor_packet)
+
+
+def write_event(output, ph, category, name, ts, args, tid):
+  """ Write a trace event.
+
+  Note that this function is NOT thread-safe.
+
+  Args:
+    output: a file-like object to write events into.
+    ph: phase of event.
+    category: category of event.
+    name: event name.
+    ts: timestamp in microseconds.
+    args: this argument is currently ignored.
+    tid: thread ID.
+  """
+  del args  # TODO(khokhlov): Encode args as DebugAnnotations.
+
+  global _last_timestamps
+  ts_us = int(ts)
+  delta_ts = ts_us - _last_timestamps[tid]
+
+  packet = proto.TracePacket()
+  packet.trusted_packet_sequence_id = _get_sequence_id(tid)
+  packet.track_event = proto.TrackEvent()
+
+  if delta_ts >= 0:
+    packet.track_event.timestamp_delta_us = delta_ts
+    _last_timestamps[tid] = ts_us
+  else:
+    packet.track_event.timestamp_absolute_us = ts_us
+
+  packet.track_event.category_iids = [_intern_category(category, packet, tid)]
+  legacy_event = proto.LegacyEvent()
+  legacy_event.phase = ord(ph)
+  legacy_event.name_iid = _intern_event_name(name, packet, tid)
+  packet.track_event.legacy_event = legacy_event
+  proto.write_trace_packet(output, packet)
+
+
+def write_metadata(
+    output,
+    benchmark_start_time_us,
+    story_run_time_us,
+    benchmark_name,
+    benchmark_description,
+    story_name,
+    story_tags,
+    story_run_index,
+    label=None,
+    had_failures=None,
+):
+  metadata = proto.ChromeBenchmarkMetadata()
+  metadata.benchmark_start_time_us = int(benchmark_start_time_us)
+  metadata.story_run_time_us = int(story_run_time_us)
+  metadata.benchmark_name = benchmark_name
+  metadata.benchmark_description = benchmark_description
+  metadata.story_name = story_name
+  metadata.story_tags = list(story_tags)
+  metadata.story_run_index = int(story_run_index)
+  if label is not None:
+    metadata.label = label
+  if had_failures is not None:
+    metadata.had_failures = had_failures
+
+  packet = proto.TracePacket()
+  packet.chrome_benchmark_metadata = metadata
+  proto.write_trace_packet(output, packet)
+
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer_unittest.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer_unittest.py
new file mode 100644
index 0000000..e49a0a4
--- /dev/null
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/perfetto_trace_writer_unittest.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+import StringIO
+
+from py_trace_event.trace_event_impl import perfetto_trace_writer
+
+
+class PerfettoTraceWriterTest(unittest.TestCase):
+  """ Tests functions that write perfetto protobufs.
+
+  TODO(crbug.com/944078): Switch to using python-protobuf library
+  and implement proper protobuf parsing then.
+  """
+
+
+  def testWriteThreadDescriptorEvent(self):
+    result = StringIO.StringIO()
+    perfetto_trace_writer.write_thread_descriptor_event(
+        output=result,
+        pid=1,
+        tid=2,
+        ts=1556716807306000,
+    )
+    expected_output = (
+        '\n\x17P\x80\x80@\xc8\x02\x01\xe2\x02\r\x08\x01\x10'
+        '\x020\x90\xf6\xc2\x82\xb6\xfa\xe1\x02'
+    )
+    self.assertEqual(expected_output, result.getvalue())
+
+  def testWriteTwoEvents(self):
+    result = StringIO.StringIO()
+    perfetto_trace_writer.write_thread_descriptor_event(
+        output=result,
+        pid=1,
+        tid=2,
+        ts=1556716807306000,
+    )
+    perfetto_trace_writer.write_event(
+        output=result,
+        ph="M",
+        category="category",
+        name="event_name",
+        ts=1556716807406000,
+        args={},
+        tid=2,
+    )
+    expected_output = (
+       '\n\x17P\x80\x80@\xc8\x02\x01\xe2\x02\r\x08\x01\x10'
+       '\x020\x90\xf6\xc2\x82\xb6\xfa\xe1\x02\n2P\x80\x80@Z\x0c\x08'
+       '\xa0\x8d\x06\x18\x012\x04\x08\x01\x10Mb\x1e\n\x0c\x08\x01'
+       '\x12\x08category\x12\x0e\x08\x01\x12\nevent_name'
+    )
+    self.assertEqual(expected_output, result.getvalue())
+
+  def testWriteMetadata(self):
+    result = StringIO.StringIO()
+    perfetto_trace_writer.write_metadata(
+        output=result,
+        benchmark_start_time_us=1556716807306000,
+        story_run_time_us=1556716807406000,
+        benchmark_name="benchmark",
+        benchmark_description="description",
+        story_name="story",
+        story_tags=["foo", "bar"],
+        story_run_index=0,
+        label="label",
+        had_failures=False,
+    )
+    expected_output = (
+        '\nI\x82\x03F\x08\x90\xf6\xc2\x82\xb6\xfa\xe1'
+        '\x02\x10\xb0\x83\xc9\x82\xb6\xfa\xe1\x02\x1a\tbenchmark"'
+        '\x0bdescription*\x05label2\x05story:\x03foo:\x03bar@\x00H\x00'
+    )
+    self.assertEqual(expected_output, result.getvalue())
+
+
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py
index 7047e0e..1216037 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py
@@ -1,7 +1,6 @@
 # Copyright 2016 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
-import tempfile
 import unittest
 
 #from .log import *
@@ -9,6 +8,7 @@
 
 from log import *
 from parsed_trace_events import *
+from py_utils import tempfile_ext
 
 class TraceTest(unittest.TestCase):
   def __init__(self, *args):
@@ -25,16 +25,16 @@
     Enables tracing, runs the provided callback, and if successful, returns a
     TraceEvents object with the results.
     """
-    self._file = tempfile.NamedTemporaryFile()
-    trace_enable(open(self._file.name, 'a+'))
-
-    try:
-      cb()
-    finally:
-      trace_disable()
-    e = ParsedTraceEvents(trace_filename = self._file.name)
-    self._file.close()
-    self._file = None
+    with tempfile_ext.TemporaryFileName() as filename:
+      self._file = open(filename, 'a+')
+      trace_enable(self._file)
+      try:
+        cb()
+      finally:
+        trace_disable()
+      e = ParsedTraceEvents(trace_filename=self._file.name)
+      self._file.close()
+      self._file = None
     return e
 
   @property
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_event_unittest.py b/catapult/common/py_trace_event/py_trace_event/trace_event_unittest.py
index f88ef95..9916c71 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_event_unittest.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_event_unittest.py
@@ -8,34 +8,29 @@
 import math
 import multiprocessing
 import os
-import tempfile
 import time
 import unittest
+import sys
 
 from py_trace_event import trace_event
 from py_trace_event import trace_time
 from py_trace_event.trace_event_impl import log
+from py_trace_event.trace_event_impl import multiprocessing_shim
+from py_utils import tempfile_ext
 
 
 class TraceEventTests(unittest.TestCase):
 
-  def setUp(self):
-    tf = tempfile.NamedTemporaryFile(delete=False)
-    self._log_path = tf.name
-    tf.close()
-
-  def tearDown(self):
-    if os.path.exists(self._log_path):
-      os.remove(self._log_path)
-
   @contextlib.contextmanager
-  def _test_trace(self, disable=True):
-    try:
-      trace_event.trace_enable(self._log_path)
-      yield
-    finally:
-      if disable:
-        trace_event.trace_disable()
+  def _test_trace(self, disable=True, format=None):
+    with tempfile_ext.TemporaryFileName() as filename:
+      self._log_path = filename
+      try:
+        trace_event.trace_enable(self._log_path, format=format)
+        yield
+      finally:
+        if disable:
+          trace_event.trace_disable()
 
   def testNoImpl(self):
     orig_impl = trace_event.trace_event_impl
@@ -77,10 +72,15 @@
     assert False
 
   def testDisable(self):
+    _old_multiprocessing_process = multiprocessing.Process
     with self._test_trace(disable=False):
       with open(self._log_path, 'r') as f:
         self.assertTrue(trace_event.trace_is_enabled())
+        self.assertEqual(
+            multiprocessing.Process, multiprocessing_shim.ProcessShim)
         trace_event.trace_disable()
+        self.assertEqual(
+            multiprocessing.Process, _old_multiprocessing_process)
         self.assertEquals(len(json.loads(f.read() + ']')), 1)
         self.assertFalse(trace_event.trace_is_enabled())
 
@@ -187,6 +187,7 @@
       with open(self._log_path, 'r') as f:
         log_output = json.loads(f.read() + ']')
         self.assertEquals(len(log_output), 3)
+        expected_name = __name__ + '.test_decorator'
         current_entry = log_output.pop(0)
         self.assertEquals(current_entry['category'], 'process_argv')
         self.assertEquals(current_entry['name'], 'process_argv')
@@ -194,12 +195,12 @@
         self.assertEquals(current_entry['ph'], 'M')
         current_entry = log_output.pop(0)
         self.assertEquals(current_entry['category'], 'python')
-        self.assertEquals(current_entry['name'], '__main__.test_decorator')
+        self.assertEquals(current_entry['name'], expected_name)
         self.assertEquals(current_entry['args']['this'], '\'that\'')
         self.assertEquals(current_entry['ph'], 'B')
         current_entry = log_output.pop(0)
         self.assertEquals(current_entry['category'], 'python')
-        self.assertEquals(current_entry['name'], '__main__.test_decorator')
+        self.assertEquals(current_entry['name'], expected_name)
         self.assertEquals(current_entry['args'], {})
         self.assertEquals(current_entry['ph'], 'E')
 
@@ -349,7 +350,8 @@
         self.assertLessEqual(one_open['ts'], two_open['ts'])
         self.assertLessEqual(one_close['ts'], two_close['ts'])
 
-  def testMultiprocess(self):
+  # TODO(khokhlov): Fix this test on Windows. See crbug.com/945819 for details.
+  def disabled_testMultiprocess(self):
     def child_function():
       with trace_event.trace('child_event'):
         pass
@@ -392,6 +394,21 @@
         self.assertEquals(parent_close['name'], 'parent_event')
         self.assertEquals(parent_close['ph'], 'E')
 
+  @unittest.skipIf(sys.platform == 'win32', 'crbug.com/945819')
+  def testTracingControlDisabledInChildButNotInParent(self):
+    def child(resp):
+      # test tracing is not controllable in the child
+      resp.put(trace_event.is_tracing_controllable())
+
+    with self._test_trace():
+      q = multiprocessing.Queue()
+      p = multiprocessing.Process(target=child, args=[q])
+      p.start()
+      # test tracing is controllable in the parent
+      self.assertTrue(trace_event.is_tracing_controllable())
+      self.assertFalse(q.get())
+      p.join()
+
   def testMultiprocessExceptionInChild(self):
     def bad_child():
       trace_event.trace_disable()
@@ -418,6 +435,84 @@
         self.assertEquals(parent_close['name'], 'parent')
         self.assertEquals(parent_close['ph'], 'E')
 
+  def testFormatJson(self):
+    with self._test_trace(format=trace_event.JSON):
+      trace_event.trace_flush()
+      with open(self._log_path, 'r') as f:
+        log_output = json.loads(f.read() + ']')
+    self.assertEquals(len(log_output), 1)
+    self.assertEquals(log_output[0]['ph'], 'M')
+
+  def testFormatJsonWithMetadata(self):
+    with self._test_trace(format=trace_event.JSON_WITH_METADATA):
+      trace_event.trace_disable()
+      with open(self._log_path, 'r') as f:
+        log_output = json.load(f)
+    self.assertEquals(len(log_output), 2)
+    events = log_output['traceEvents']
+    self.assertEquals(len(events), 1)
+    self.assertEquals(events[0]['ph'], 'M')
+
+  def testFormatProtobuf(self):
+    with self._test_trace(format=trace_event.PROTOBUF):
+      trace_event.trace_flush()
+      with open(self._log_path, 'r') as f:
+        self.assertGreater(len(f.read()), 0)
+
+  def testAddMetadata(self):
+    with self._test_trace(format=trace_event.JSON_WITH_METADATA):
+      trace_event.trace_add_benchmark_metadata(
+          benchmark_start_time_us=1000,
+          story_run_time_us=2000,
+          benchmark_name='benchmark',
+          benchmark_description='desc',
+          story_name='story',
+          story_tags=['tag1', 'tag2'],
+          story_run_index=0,
+      )
+      trace_event.trace_disable()
+      with open(self._log_path, 'r') as f:
+        log_output = json.load(f)
+    self.assertEquals(len(log_output), 2)
+    telemetry_metadata = log_output['metadata']['telemetry']
+    self.assertEquals(len(telemetry_metadata), 7)
+    self.assertEquals(telemetry_metadata['benchmarkStart'], 1)
+    self.assertEquals(telemetry_metadata['traceStart'], 2)
+    self.assertEquals(telemetry_metadata['benchmarks'], ['benchmark'])
+    self.assertEquals(telemetry_metadata['benchmarkDescriptions'], ['desc'])
+    self.assertEquals(telemetry_metadata['stories'], ['story'])
+    self.assertEquals(telemetry_metadata['storyTags'], ['tag1', 'tag2'])
+    self.assertEquals(telemetry_metadata['storysetRepeats'], [0])
+
+  def testAddMetadataProtobuf(self):
+    with self._test_trace(format=trace_event.PROTOBUF):
+      trace_event.trace_add_benchmark_metadata(
+          benchmark_start_time_us=1000,
+          story_run_time_us=2000,
+          benchmark_name='benchmark',
+          benchmark_description='desc',
+          story_name='story',
+          story_tags=['tag1', 'tag2'],
+          story_run_index=0,
+      )
+      trace_event.trace_disable()
+      with open(self._log_path, 'r') as f:
+        self.assertGreater(len(f.read()), 0)
+
+  def testAddMetadataInJsonFormatRaises(self):
+    with self._test_trace(format=trace_event.JSON):
+      with self.assertRaises(log.TraceException):
+        trace_event.trace_add_benchmark_metadata(
+            benchmark_start_time_us=1000,
+            story_run_time_us=2000,
+            benchmark_name='benchmark',
+            benchmark_description='description',
+            story_name='story',
+            story_tags=['tag1', 'tag2'],
+            story_run_index=0,
+        )
+
+
 if __name__ == '__main__':
   logging.getLogger().setLevel(logging.DEBUG)
   unittest.main(verbosity=2)
diff --git a/catapult/common/py_trace_event/py_trace_event/trace_time_unittest.py b/catapult/common/py_trace_event/py_trace_event/trace_time_unittest.py
index ab54bd6..bae7ea8 100644
--- a/catapult/common/py_trace_event/py_trace_event/trace_time_unittest.py
+++ b/catapult/common/py_trace_event/py_trace_event/trace_time_unittest.py
@@ -86,7 +86,7 @@
     # Test requires QPC to be available on platform.
     if not trace_time.IsQPCUsable():
       return True
-    self.assertGreater(trace_time.monotonic(), 0)
+    self.assertGreater(trace_time.Now(), 0)
 
   # Works even if QPC would work.
   def testGetWinNowFunction_GetTickCount(self):
@@ -94,7 +94,7 @@
             or sys.platform.startswith(trace_time._PLATFORMS['cygwin'])):
       return True
     with self.ReplaceQPCCheck(lambda: False):
-      self.assertGreater(trace_time.monotonic(), 0)
+      self.assertGreater(trace_time.Now(), 0)
 
   # Linux tests.
   def testGetClockGetTimeClockNumber_linux(self):
diff --git a/catapult/common/py_trace_event/third_party/protobuf/README.chromium b/catapult/common/py_trace_event/third_party/protobuf/README.chromium
new file mode 100644
index 0000000..f22d684
--- /dev/null
+++ b/catapult/common/py_trace_event/third_party/protobuf/README.chromium
@@ -0,0 +1,12 @@
+Name: Protobuf
+URL: https://developers.google.com/protocol-buffers/
+Version: 3.0.0
+License: BSD
+
+Description:
+Protocol buffers are Google's language-neutral, platform-neutral,
+extensible mechanism for serializing structured data.
+
+Local Modifications:
+Removed pretty much everything except functions necessary to write
+bools, ints, and strings.
diff --git a/catapult/common/py_trace_event/third_party/protobuf/encoder.py b/catapult/common/py_trace_event/third_party/protobuf/encoder.py
new file mode 100644
index 0000000..18aaccd
--- /dev/null
+++ b/catapult/common/py_trace_event/third_party/protobuf/encoder.py
@@ -0,0 +1,224 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import six
+
+import wire_format
+
+
+def _VarintSize(value):
+  """Compute the size of a varint value."""
+  if value <= 0x7f: return 1
+  if value <= 0x3fff: return 2
+  if value <= 0x1fffff: return 3
+  if value <= 0xfffffff: return 4
+  if value <= 0x7ffffffff: return 5
+  if value <= 0x3ffffffffff: return 6
+  if value <= 0x1ffffffffffff: return 7
+  if value <= 0xffffffffffffff: return 8
+  if value <= 0x7fffffffffffffff: return 9
+  return 10
+
+
+def _SignedVarintSize(value):
+  """Compute the size of a signed varint value."""
+  if value < 0: return 10
+  if value <= 0x7f: return 1
+  if value <= 0x3fff: return 2
+  if value <= 0x1fffff: return 3
+  if value <= 0xfffffff: return 4
+  if value <= 0x7ffffffff: return 5
+  if value <= 0x3ffffffffff: return 6
+  if value <= 0x1ffffffffffff: return 7
+  if value <= 0xffffffffffffff: return 8
+  if value <= 0x7fffffffffffffff: return 9
+  return 10
+
+
+def _VarintEncoder():
+  """Return an encoder for a basic varint value (does not include tag)."""
+
+  def EncodeVarint(write, value):
+    bits = value & 0x7f
+    value >>= 7
+    while value:
+      write(six.int2byte(0x80|bits))
+      bits = value & 0x7f
+      value >>= 7
+    return write(six.int2byte(bits))
+
+  return EncodeVarint
+
+
+def _SignedVarintEncoder():
+  """Return an encoder for a basic signed varint value (does not include
+  tag)."""
+
+  def EncodeSignedVarint(write, value):
+    if value < 0:
+      value += (1 << 64)
+    bits = value & 0x7f
+    value >>= 7
+    while value:
+      write(six.int2byte(0x80|bits))
+      bits = value & 0x7f
+      value >>= 7
+    return write(six.int2byte(bits))
+
+  return EncodeSignedVarint
+
+
+_EncodeVarint = _VarintEncoder()
+_EncodeSignedVarint = _SignedVarintEncoder()
+
+
+def _VarintBytes(value):
+  """Encode the given integer as a varint and return the bytes.  This is only
+  called at startup time so it doesn't need to be fast."""
+
+  pieces = []
+  _EncodeVarint(pieces.append, value)
+  return b"".join(pieces)
+
+
+def TagBytes(field_number, wire_type):
+  """Encode the given tag and return the bytes.  Only called at startup."""
+
+  return _VarintBytes(wire_format.PackTag(field_number, wire_type))
+
+
+def _SimpleEncoder(wire_type, encode_value, compute_value_size):
+  """Return a constructor for an encoder for fields of a particular type.
+
+  Args:
+      wire_type:  The field's wire type, for encoding tags.
+      encode_value:  A function which encodes an individual value, e.g.
+        _EncodeVarint().
+      compute_value_size:  A function which computes the size of an individual
+        value, e.g. _VarintSize().
+  """
+
+  def SpecificEncoder(field_number, is_repeated, is_packed):
+    if is_packed:
+      tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
+      local_EncodeVarint = _EncodeVarint
+      def EncodePackedField(write, value):
+        write(tag_bytes)
+        size = 0
+        for element in value:
+          size += compute_value_size(element)
+        local_EncodeVarint(write, size)
+        for element in value:
+          encode_value(write, element)
+      return EncodePackedField
+    elif is_repeated:
+      tag_bytes = TagBytes(field_number, wire_type)
+      def EncodeRepeatedField(write, value):
+        for element in value:
+          write(tag_bytes)
+          encode_value(write, element)
+      return EncodeRepeatedField
+    else:
+      tag_bytes = TagBytes(field_number, wire_type)
+      def EncodeField(write, value):
+        write(tag_bytes)
+        return encode_value(write, value)
+      return EncodeField
+
+  return SpecificEncoder
+
+
+Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder(
+    wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize)
+
+UInt32Encoder = UInt64Encoder = _SimpleEncoder(
+    wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize)
+
+
+def BoolEncoder(field_number, is_repeated, is_packed):
+  """Returns an encoder for a boolean field."""
+
+  false_byte = b'\x00'
+  true_byte = b'\x01'
+  if is_packed:
+    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
+    local_EncodeVarint = _EncodeVarint
+    def EncodePackedField(write, value):
+      write(tag_bytes)
+      local_EncodeVarint(write, len(value))
+      for element in value:
+        if element:
+          write(true_byte)
+        else:
+          write(false_byte)
+    return EncodePackedField
+  elif is_repeated:
+    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)
+    def EncodeRepeatedField(write, value):
+      for element in value:
+        write(tag_bytes)
+        if element:
+          write(true_byte)
+        else:
+          write(false_byte)
+    return EncodeRepeatedField
+  else:
+    tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT)
+    def EncodeField(write, value):
+      write(tag_bytes)
+      if value:
+        return write(true_byte)
+      return write(false_byte)
+    return EncodeField
+
+
+def StringEncoder(field_number, is_repeated, is_packed):
+  """Returns an encoder for a string field."""
+
+  tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
+  local_EncodeVarint = _EncodeVarint
+  local_len = len
+  assert not is_packed
+  if is_repeated:
+    def EncodeRepeatedField(write, value):
+      for element in value:
+        encoded = element.encode('utf-8')
+        write(tag)
+        local_EncodeVarint(write, local_len(encoded))
+        write(encoded)
+    return EncodeRepeatedField
+  else:
+    def EncodeField(write, value):
+      encoded = value.encode('utf-8')
+      write(tag)
+      local_EncodeVarint(write, local_len(encoded))
+      return write(encoded)
+    return EncodeField
+
diff --git a/catapult/common/py_trace_event/third_party/protobuf/wire_format.py b/catapult/common/py_trace_event/third_party/protobuf/wire_format.py
new file mode 100644
index 0000000..9341e6f
--- /dev/null
+++ b/catapult/common/py_trace_event/third_party/protobuf/wire_format.py
@@ -0,0 +1,52 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+TAG_TYPE_BITS = 3  # Number of bits used to hold type info in a proto tag.
+
+WIRETYPE_VARINT = 0
+WIRETYPE_FIXED64 = 1
+WIRETYPE_LENGTH_DELIMITED = 2
+WIRETYPE_START_GROUP = 3
+WIRETYPE_END_GROUP = 4
+WIRETYPE_FIXED32 = 5
+_WIRETYPE_MAX = 5
+
+def PackTag(field_number, wire_type):
+  """Returns an unsigned 32-bit integer that encodes the field number and
+  wire type information in standard protocol message wire format.
+
+  Args:
+    field_number: Expected to be an integer in the range [1, 1 << 29)
+    wire_type: One of the WIRETYPE_* constants.
+  """
+  if not 0 <= wire_type <= _WIRETYPE_MAX:
+    raise RuntimeError('Unknown wire type: %d' % wire_type)
+  return (field_number << TAG_TYPE_BITS) | wire_type
+
diff --git a/catapult/common/py_utils/bin/run_tests b/catapult/common/py_utils/bin/run_tests
new file mode 100755
index 0000000..66a4b59
--- /dev/null
+++ b/catapult/common/py_utils/bin/run_tests
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# Copyright (c) 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+_CATAPULT_PATH = os.path.abspath(
+    os.path.join(os.path.dirname(__file__), '..', '..', '..'))
+
+_PY_UTILS_PATH = os.path.abspath(
+    os.path.join(_CATAPULT_PATH, 'common', 'py_utils'))
+
+
+def _RunTestsOrDie(top_level_dir):
+  exit_code = run_with_typ.Run(top_level_dir, path=[_PY_UTILS_PATH])
+  if exit_code:
+    sys.exit(exit_code)
+
+
+def _AddToPathIfNeeded(path):
+  if path not in sys.path:
+    sys.path.insert(0, path)
+
+
+if __name__ == '__main__':
+  _AddToPathIfNeeded(_CATAPULT_PATH)
+
+  from hooks import install
+  if '--no-install-hooks' in sys.argv:
+    sys.argv.remove('--no-install-hooks')
+  else:
+    install.InstallHooks()
+
+  from catapult_build import run_with_typ
+  _RunTestsOrDie(_PY_UTILS_PATH)
+  sys.exit(0)
diff --git a/catapult/common/py_utils/py_utils/__init__.py b/catapult/common/py_utils/py_utils/__init__.py
index dcec4ed..0d7b052 100644
--- a/catapult/common/py_utils/py_utils/__init__.py
+++ b/catapult/common/py_utils/py_utils/__init__.py
@@ -4,6 +4,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import print_function
+
 import functools
 import inspect
 import os
@@ -100,7 +102,7 @@
     try:
       return timeout_retry.Run(func, timeout, 0, args=args)
     except reraiser_thread.TimeoutError:
-      print '%s timed out.' % func.__name__
+      print('%s timed out.' % func.__name__)
       return False
   return RunWithTimeout
 
diff --git a/catapult/common/py_utils/py_utils/camel_case.py b/catapult/common/py_utils/py_utils/camel_case.py
index 9a76890..dbebb22 100644
--- a/catapult/common/py_utils/py_utils/camel_case.py
+++ b/catapult/common/py_utils/py_utils/camel_case.py
@@ -2,7 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 import re
+import six
 
 
 def ToUnderscore(obj):
@@ -11,7 +15,7 @@
   Descends recursively into lists and dicts, converting all dict keys.
   Returns a newly allocated object of the same structure as the input.
   """
-  if isinstance(obj, basestring):
+  if isinstance(obj, six.string_types):
     return re.sub('(?!^)([A-Z]+)', r'_\1', obj).lower()
 
   elif isinstance(obj, list):
@@ -19,7 +23,7 @@
 
   elif isinstance(obj, dict):
     output = {}
-    for k, v in obj.iteritems():
+    for k, v in six.iteritems(obj):
       if isinstance(v, list) or isinstance(v, dict):
         output[ToUnderscore(k)] = ToUnderscore(v)
       else:
diff --git a/catapult/common/py_utils/py_utils/chrome_binaries.json b/catapult/common/py_utils/py_utils/chrome_binaries.json
index 8a9b6bf..437cbb3 100644
--- a/catapult/common/py_utils/py_utils/chrome_binaries.json
+++ b/catapult/common/py_utils/py_utils/chrome_binaries.json
@@ -6,22 +6,22 @@
       "cloud_storage_bucket": "chrome-telemetry",
       "file_info": {
         "mac_x86_64": {
-          "cloud_storage_hash": "6278cf24b700076fd17ae8616fd980d18f33ed5d",
+          "cloud_storage_hash": "381a491e14ab523b8db4cdf3c993713678237af8",
           "download_path": "bin/reference_builds/chrome-mac64.zip",
           "path_within_archive": "chrome-mac/Google Chrome.app/Contents/MacOS/Google Chrome",
-          "version_in_cs": "70.0.3509.0"
+          "version_in_cs": "77.0.3822.0"
         },
         "win_AMD64": {
-          "cloud_storage_hash": "a78facdb295d2ee36aaf5af89e54b5c5fcd48f7c",
+          "cloud_storage_hash": "600ee522c410efe1de2f593c0efc32ae113a7d99",
           "download_path": "bin\\reference_build\\chrome-win64-clang.zip",
           "path_within_archive": "chrome-win64-clang\\chrome.exe",
-          "version_in_cs": "70.0.3509.0"
+          "version_in_cs": "77.0.3822.0"
         },
         "win_x86": {
-          "cloud_storage_hash": "348e8133c5fa687864a3d8eff13ed5be6852e95d",
+          "cloud_storage_hash": "5b79a181bfbd94d8288529b0da1defa3ef097197",
           "download_path": "bin\\reference_build\\chrome-win32-clang.zip",
           "path_within_archive": "chrome-win32-clang\\chrome.exe",
-          "version_in_cs": "70.0.3509.0"
+          "version_in_cs": "77.0.3822.0"
         }
       }
     },
@@ -30,10 +30,10 @@
       "cloud_storage_bucket": "chrome-telemetry",
       "file_info": {
         "linux_x86_64": {
-          "cloud_storage_hash": "1ef8d8ebf114b47aecf11a36c21377376ced3794",
+          "cloud_storage_hash": "61d68a6b00f25c964f5162f5251962468c886f3a",
           "download_path": "bin/reference_build/chrome-linux64.zip",
           "path_within_archive": "chrome-linux64/chrome",
-          "version_in_cs": "69.0.3497.23"
+          "version_in_cs": "76.0.3809.21"
         }
       }
     },
@@ -42,48 +42,83 @@
       "cloud_storage_bucket": "chrome-telemetry",
       "file_info": {
         "android_k_armeabi-v7a": {
-          "cloud_storage_hash": "5a4cc68b2ef5e6073f9f8f42987155d5fc8a3c48",
+          "cloud_storage_hash": "28b913c720d56a30c092625c7862f00175a316c7",
           "download_path": "bin/reference_build/android_k_armeabi-v7a/ChromeStable.apk",
-          "version_in_cs": "68.0.3440.85"
+          "version_in_cs": "75.0.3770.67"
         },
         "android_l_arm64-v8a": {
-          "cloud_storage_hash": "42d527ca74e99fb9398826204db09c8740df7fd4",
+          "cloud_storage_hash": "4b953c33c61f94c2198e8001d0d8142c6504a875",
           "download_path": "bin/reference_build/android_l_arm64-v8a/ChromeStable.apk",
-          "version_in_cs": "68.0.3440.85"
+          "version_in_cs": "75.0.3770.67"
         },
         "android_l_armeabi-v7a": {
-          "cloud_storage_hash": "5a4cc68b2ef5e6073f9f8f42987155d5fc8a3c48",
+          "cloud_storage_hash": "28b913c720d56a30c092625c7862f00175a316c7",
           "download_path": "bin/reference_build/android_l_armeabi-v7a/ChromeStable.apk",
-          "version_in_cs": "68.0.3440.85"
+          "version_in_cs": "75.0.3770.67"
+        },
+        "android_n_arm64-v8a": {
+          "cloud_storage_hash": "84152ba8f7a25cacc79d588ed827ea75f0e4ab94",
+          "download_path": "bin/reference_build/android_n_arm64-v8a/Monochrome.apk",
+          "version_in_cs": "75.0.3770.67"
         },
         "android_n_armeabi-v7a": {
-          "cloud_storage_hash": "d37f47a804e815daf001f65d0c13a2cf38641f3e",
+          "cloud_storage_hash": "656bb9e3982d0d35decd5347ced2c320a7267f33",
           "download_path": "bin/reference_build/android_n_armeabi-v7a/Monochrome.apk",
-          "version_in_cs": "68.0.3440.85"
+          "version_in_cs": "75.0.3770.67"
         },
         "linux_x86_64": {
-          "cloud_storage_hash": "aab60e4a4ee4f3d638aa6a33e52ffb6423fa7080",
+          "cloud_storage_hash": "dee8469e8dcd8453efd33f3a00d7ea302a126a4b",
           "download_path": "bin/reference_build/chrome-linux64.zip",
           "path_within_archive": "chrome-linux64/chrome",
-          "version_in_cs": "68.0.3440.84"
+          "version_in_cs": "75.0.3770.80"
         },
         "mac_x86_64": {
-          "cloud_storage_hash": "8a020bc9caa2526408dc23044e8dcfaaf6b6948e",
+          "cloud_storage_hash": "16a43a1e794bb99ec1ebcd40569084985b3c6626",
           "download_path": "bin/reference_builds/chrome-mac64.zip",
           "path_within_archive": "chrome-mac/Google Chrome.app/Contents/MacOS/Google Chrome",
-          "version_in_cs": "68.0.3440.84"
+          "version_in_cs": "75.0.3770.80"
         },
         "win_AMD64": {
-          "cloud_storage_hash": "19da10346662d8e791076a0ddcfbf2a435b6915a",
+          "cloud_storage_hash": "1ec52bd4164f2d93c53113a093dae9e041eb2d73",
           "download_path": "bin\\reference_build\\chrome-win64-clang.zip",
           "path_within_archive": "chrome-win64-clang\\chrome.exe",
-          "version_in_cs": "68.0.3440.84"
+          "version_in_cs": "75.0.3770.80"
         },
         "win_x86": {
-          "cloud_storage_hash": "760ff8661550f6aebadedba99075efe6adae3414",
+          "cloud_storage_hash": "0f9eb991ba618dc61f2063ea252f44be94c2252e",
           "download_path": "bin\\reference_build\\chrome-win-clang.zip",
           "path_within_archive": "chrome-win-clang\\chrome.exe",
-          "version_in_cs": "68.0.3440.84"
+          "version_in_cs": "75.0.3770.80"
+        }
+      }
+    },
+    "chrome_m72": {
+      "cloud_storage_base_folder": "binary_dependencies",
+      "cloud_storage_bucket": "chrome-telemetry",
+      "file_info": {
+        "linux_x86_64": {
+          "cloud_storage_hash": "537c19346b20340cc6807242e1eb6d82dfcfa2e8",
+          "download_path": "bin/reference_build/chrome-linux64.zip",
+          "path_within_archive": "chrome-linux64/chrome",
+          "version_in_cs": "72.0.3626.119"
+        },
+        "mac_x86_64": {
+          "cloud_storage_hash": "7f6a931f696f57561703538c6f799781d6e22e7e",
+          "download_path": "bin/reference_builds/chrome-mac64.zip",
+          "path_within_archive": "chrome-mac/Google Chrome.app/Contents/MacOS/Google Chrome",
+          "version_in_cs": "72.0.3626.119"
+        },
+        "win_AMD64": {
+          "cloud_storage_hash": "563d7985c85bfe77e92b8253d0389ff8551018c7",
+          "download_path": "bin\\reference_build\\chrome-win64-clang.zip",
+          "path_within_archive": "chrome-win64-clang\\chrome.exe",
+          "version_in_cs": "72.0.3626.119"
+        },
+        "win_x86": {
+          "cloud_storage_hash": "1802179da16e44b83bd3f0b296f9e5b0b053d59c",
+          "download_path": "bin\\reference_build\\chrome-win-clang.zip",
+          "path_within_archive": "chrome-win-clang\\chrome.exe",
+          "version_in_cs": "72.0.3626.119"
         }
       }
     }
diff --git a/catapult/common/py_utils/py_utils/cloud_storage.py b/catapult/common/py_utils/py_utils/cloud_storage.py
index df5589b..b4988c5 100644
--- a/catapult/common/py_utils/py_utils/cloud_storage.py
+++ b/catapult/common/py_utils/py_utils/cloud_storage.py
@@ -9,21 +9,21 @@
 import hashlib
 import logging
 import os
+import re
 import shutil
 import stat
 import subprocess
-import re
 import sys
 import tempfile
 import time
 
 import py_utils
+from py_utils import cloud_storage_global_lock  # pylint: disable=unused-import
 from py_utils import lock
 
 # Do a no-op import here so that cloud_storage_global_lock dep is picked up
 # by https://cs.chromium.org/chromium/src/build/android/test_runner.pydeps.
 # TODO(nedn, jbudorick): figure out a way to get rid of this ugly hack.
-from py_utils import cloud_storage_global_lock  # pylint: disable=unused-import
 
 logger = logging.getLogger(__name__)  # pylint: disable=invalid-name
 
@@ -42,7 +42,7 @@
     ('output', TELEMETRY_OUTPUT),
 ))
 
-BUCKET_ALIAS_NAMES = BUCKET_ALIASES.keys()
+BUCKET_ALIAS_NAMES = list(BUCKET_ALIASES.keys())
 
 
 _GSUTIL_PATH = os.path.join(py_utils.GetCatapultDir(), 'third_party', 'gsutil',
diff --git a/catapult/common/py_utils/py_utils/discover.py b/catapult/common/py_utils/py_utils/discover.py
index 09d5c5e..ae8ba87 100644
--- a/catapult/common/py_utils/py_utils/discover.py
+++ b/catapult/common/py_utils/py_utils/discover.py
@@ -107,7 +107,7 @@
     # crbug.com/548652
     if index_by_class_name:
       AssertNoKeyConflicts(classes, new_classes)
-    classes = dict(classes.items() + new_classes.items())
+    classes = dict(list(classes.items()) + list(new_classes.items()))
   return classes
 
 
diff --git a/catapult/common/py_utils/py_utils/discover_unittest.py b/catapult/common/py_utils/py_utils/discover_unittest.py
index 137d85f..2d4fd27 100644
--- a/catapult/common/py_utils/py_utils/discover_unittest.py
+++ b/catapult/common/py_utils/py_utils/discover_unittest.py
@@ -1,10 +1,15 @@
 # Copyright 2013 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import os
 import unittest
 
 from py_utils import discover
+import six
 
 
 class DiscoverTest(unittest.TestCase):
@@ -20,8 +25,8 @@
                                        self._base_class,
                                        index_by_class_name=False)
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'another_discover_dummyclass': 'DummyExceptionWithParameterImpl1',
         'discover_dummyclass': 'DummyException',
@@ -35,8 +40,8 @@
                                        self._base_class,
                                        directly_constructable=True)
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'dummy_exception': 'DummyException',
         'dummy_exception_impl1': 'DummyExceptionImpl1',
@@ -48,8 +53,8 @@
     classes = discover.DiscoverClasses(self._start_dir, self._base_dir,
                                        self._base_class)
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'dummy_exception': 'DummyException',
         'dummy_exception_impl1': 'DummyExceptionImpl1',
@@ -68,8 +73,8 @@
                                        pattern='another*',
                                        index_by_class_name=False)
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'another_discover_dummyclass': 'DummyExceptionWithParameterImpl1'
     }
@@ -83,8 +88,8 @@
                                        pattern='another*',
                                        directly_constructable=True)
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'dummy_exception_impl1': 'DummyExceptionImpl1',
         'dummy_exception_impl2': 'DummyExceptionImpl2',
@@ -97,8 +102,8 @@
                                        self._base_class,
                                        pattern='another*')
 
-    actual_classes = dict((name, cls.__name__)
-                          for name, cls in classes.iteritems())
+    actual_classes = dict(
+        (name, cls.__name__) for name, cls in six.iteritems(classes))
     expected_classes = {
         'dummy_exception_impl1': 'DummyExceptionImpl1',
         'dummy_exception_impl2': 'DummyExceptionImpl2',
diff --git a/catapult/common/py_utils/py_utils/exc_util.py b/catapult/common/py_utils/py_utils/exc_util.py
new file mode 100644
index 0000000..538ced2
--- /dev/null
+++ b/catapult/common/py_utils/py_utils/exc_util.py
@@ -0,0 +1,84 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import functools
+import logging
+import sys
+
+
+def BestEffort(func):
+  """Decorator to log and dismiss exceptions if one if already being handled.
+
+  Note: This is largely a workaround for the lack of support of exception
+  chaining in Python 2.7, this decorator will no longer be needed in Python 3.
+
+  Typical usage would be in |Close| or |Disconnect| methods, to dismiss but log
+  any further exceptions raised if the current execution context is already
+  handling an exception. For example:
+
+      class Client(object):
+        def Connect(self):
+          # code to connect ...
+
+        @exc_util.BestEffort
+        def Disconnect(self):
+          # code to disconnect ...
+
+      client = Client()
+      try:
+        client.Connect()
+      except:
+        client.Disconnect()
+        raise
+
+  If an exception is raised by client.Connect(), and then a second exception
+  is raised by client.Disconnect(), the decorator will log the second exception
+  and let the original one be re-raised.
+
+  Otherwise, in Python 2.7 and without the decorator, the second exception is
+  the one propagated to the caller; while information about the original one,
+  usually more important, is completely lost.
+
+  Note that if client.Disconnect() is called in a context where an exception
+  is *not* being handled, then any exceptions raised within the method will
+  get through and be passed on to callers for them to handle in the usual way.
+
+  The decorator can also be used on cleanup functions meant to be called on
+  a finally block, however you must also include an except-raise clause to
+  properly signal (in Python 2.7) whether an exception is being handled; e.g.:
+
+      @exc_util.BestEffort
+      def cleanup():
+        # do cleanup things ...
+
+      try:
+        process(thing)
+      except:
+        raise  # Needed to let cleanup know if an exception is being handled.
+      finally:
+        cleanup()
+
+  Failing to include the except-raise block has the same effect as not
+  including the decorator at all. Namely: exceptions during |cleanup| are
+  raised and swallow any prior exceptions that occurred during |process|.
+  """
+  @functools.wraps(func)
+  def Wrapper(*args, **kwargs):
+    exc_type = sys.exc_info()[0]
+    if exc_type is None:
+      # Not currently handling an exception; let any errors raise exceptions
+      # as usual.
+      func(*args, **kwargs)
+    else:
+      # Otherwise, we are currently handling an exception, dismiss and log
+      # any further cascading errors. Callers are responsible to handle the
+      # original exception.
+      try:
+        func(*args, **kwargs)
+      except Exception:  # pylint: disable=broad-except
+        logging.exception(
+            'While handling a %s, the following exception was also raised:',
+            exc_type.__name__)
+
+  return Wrapper
diff --git a/catapult/common/py_utils/py_utils/exc_util_unittest.py b/catapult/common/py_utils/py_utils/exc_util_unittest.py
new file mode 100644
index 0000000..31e3b57
--- /dev/null
+++ b/catapult/common/py_utils/py_utils/exc_util_unittest.py
@@ -0,0 +1,183 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import re
+import sys
+import unittest
+
+from py_utils import exc_util
+
+
+class FakeConnectionError(Exception):
+  pass
+
+
+class FakeDisconnectionError(Exception):
+  pass
+
+
+class FakeProcessingError(Exception):
+  pass
+
+
+class FakeCleanupError(Exception):
+  pass
+
+
+class FaultyClient(object):
+  def __init__(self, *args):
+    self.failures = set(args)
+    self.called = set()
+
+  def Connect(self):
+    self.called.add('Connect')
+    if FakeConnectionError in self.failures:
+      raise FakeConnectionError('Oops!')
+
+  def Process(self):
+    self.called.add('Process')
+    if FakeProcessingError in self.failures:
+      raise FakeProcessingError('Oops!')
+
+  @exc_util.BestEffort
+  def Disconnect(self):
+    self.called.add('Disconnect')
+    if FakeDisconnectionError in self.failures:
+      raise FakeDisconnectionError('Oops!')
+
+  @exc_util.BestEffort
+  def Cleanup(self):
+    self.called.add('Cleanup')
+    if FakeCleanupError in self.failures:
+      raise FakeCleanupError('Oops!')
+
+
+class ReraiseTests(unittest.TestCase):
+  def assertLogMatches(self, pattern):
+    self.assertRegexpMatches(
+        sys.stderr.getvalue(), pattern)  # pylint: disable=no-member
+
+  def assertLogNotMatches(self, pattern):
+    self.assertNotRegexpMatches(
+        sys.stderr.getvalue(), pattern)  # pylint: disable=no-member
+
+  def testTryRaisesExceptRaises(self):
+    client = FaultyClient(FakeConnectionError, FakeDisconnectionError)
+
+    # The connection error reaches the top level, while the disconnection
+    # error is logged.
+    with self.assertRaises(FakeConnectionError):
+      try:
+        client.Connect()
+      except:
+        client.Disconnect()
+        raise
+
+    self.assertLogMatches(re.compile(
+        r'While handling a FakeConnectionError, .* was also raised:\n'
+        r'Traceback \(most recent call last\):\n'
+        r'.*\n'
+        r'FakeDisconnectionError: Oops!\n', re.DOTALL))
+    self.assertItemsEqual(client.called, ['Connect', 'Disconnect'])
+
+  def testTryRaisesExceptDoesnt(self):
+    client = FaultyClient(FakeConnectionError)
+
+    # The connection error reaches the top level, disconnecting did not raise
+    # an exception (so nothing is logged).
+    with self.assertRaises(FakeConnectionError):
+      try:
+        client.Connect()
+      except:
+        client.Disconnect()
+        raise
+
+    self.assertLogNotMatches('FakeDisconnectionError')
+    self.assertItemsEqual(client.called, ['Connect', 'Disconnect'])
+
+  def testTryPassesNoException(self):
+    client = FaultyClient(FakeDisconnectionError)
+
+    # If there is no connection error, the except clause is not called (even if
+    # it would have raised an exception).
+    try:
+      client.Connect()
+    except:
+      client.Disconnect()
+      raise
+
+    self.assertLogNotMatches('FakeConnectionError')
+    self.assertLogNotMatches('FakeDisconnectionError')
+    self.assertItemsEqual(client.called, ['Connect'])
+
+  def testTryRaisesFinallyRaises(self):
+    worker = FaultyClient(FakeProcessingError, FakeCleanupError)
+
+    # The processing error reaches the top level, the cleanup error is logged.
+    with self.assertRaises(FakeProcessingError):
+      try:
+        worker.Process()
+      except:
+        raise  # Needed for Cleanup to know if an exception is handled.
+      finally:
+        worker.Cleanup()
+
+    self.assertLogMatches(re.compile(
+        r'While handling a FakeProcessingError, .* was also raised:\n'
+        r'Traceback \(most recent call last\):\n'
+        r'.*\n'
+        r'FakeCleanupError: Oops!\n', re.DOTALL))
+    self.assertItemsEqual(worker.called, ['Process', 'Cleanup'])
+
+  def testTryRaisesFinallyDoesnt(self):
+    worker = FaultyClient(FakeProcessingError)
+
+    # The processing error reaches the top level, the cleanup code runs fine.
+    with self.assertRaises(FakeProcessingError):
+      try:
+        worker.Process()
+      except:
+        raise  # Needed for Cleanup to know if an exception is handled.
+      finally:
+        worker.Cleanup()
+
+    self.assertLogNotMatches('FakeProcessingError')
+    self.assertLogNotMatches('FakeCleanupError')
+    self.assertItemsEqual(worker.called, ['Process', 'Cleanup'])
+
+  def testTryPassesFinallyRaises(self):
+    worker = FaultyClient(FakeCleanupError)
+
+    # The processing code runs fine, the cleanup code raises an exception
+    # which reaches the top level.
+    with self.assertRaises(FakeCleanupError):
+      try:
+        worker.Process()
+      except:
+        raise  # Needed for Cleanup to know if an exception is handled.
+      finally:
+        worker.Cleanup()
+
+    self.assertLogNotMatches('FakeProcessingError')
+    self.assertLogNotMatches('FakeCleanupError')
+    self.assertItemsEqual(worker.called, ['Process', 'Cleanup'])
+
+  def testTryRaisesExceptRaisesFinallyRaises(self):
+    worker = FaultyClient(
+        FakeProcessingError, FakeDisconnectionError, FakeCleanupError)
+
+    # Chaining try-except-finally works fine. Only the processing error reaches
+    # the top level; the other two are logged.
+    with self.assertRaises(FakeProcessingError):
+      try:
+        worker.Process()
+      except:
+        worker.Disconnect()
+        raise
+      finally:
+        worker.Cleanup()
+
+    self.assertLogMatches('FakeDisconnectionError')
+    self.assertLogMatches('FakeCleanupError')
+    self.assertItemsEqual(worker.called, ['Process', 'Disconnect', 'Cleanup'])
diff --git a/catapult/common/py_utils/py_utils/expectations_parser.py b/catapult/common/py_utils/py_utils/expectations_parser.py
index 6fa9407..534b352 100644
--- a/catapult/common/py_utils/py_utils/expectations_parser.py
+++ b/catapult/common/py_utils/py_utils/expectations_parser.py
@@ -2,7 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 import re
+import six
 
 
 class ParseError(Exception):
@@ -20,9 +24,9 @@
           Conditions are combined using logical and. Example: ['Mac', 'Debug']
       results: List of outcomes for test. Example: ['Skip', 'Pass']
     """
-    assert isinstance(reason, basestring) or reason is None
+    assert isinstance(reason, six.string_types) or reason is None
     self._reason = reason
-    assert isinstance(test, basestring)
+    assert isinstance(test, six.string_types)
     self._test = test
     assert isinstance(conditions, list)
     self._conditions = conditions
diff --git a/catapult/common/py_utils/py_utils/expectations_parser_unittest.py b/catapult/common/py_utils/py_utils/expectations_parser_unittest.py
index a842c4c..523e871 100644
--- a/catapult/common/py_utils/py_utils/expectations_parser_unittest.py
+++ b/catapult/common/py_utils/py_utils/expectations_parser_unittest.py
@@ -3,9 +3,14 @@
 # found in the LICENSE file.
 
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import unittest
 
 from py_utils import expectations_parser
+from six.moves import range # pylint: disable=redefined-builtin
 
 
 class TestExpectationParserTest(unittest.TestCase):
diff --git a/catapult/common/py_utils/py_utils/file_util.py b/catapult/common/py_utils/py_utils/file_util.py
index 36cc42f..b1602c9 100644
--- a/catapult/common/py_utils/py_utils/file_util.py
+++ b/catapult/common/py_utils/py_utils/file_util.py
@@ -17,7 +17,7 @@
   assert os.path.exists(source_path)
   try:
     os.makedirs(os.path.dirname(dest_path))
-  except OSError, e:
+  except OSError as e:
     if e.errno != errno.EEXIST:
       raise
   shutil.copy(source_path, dest_path)
diff --git a/catapult/common/py_utils/py_utils/lock.py b/catapult/common/py_utils/py_utils/lock.py
index f655618..ade4d1f 100644
--- a/catapult/common/py_utils/py_utils/lock.py
+++ b/catapult/common/py_utils/py_utils/lock.py
@@ -84,7 +84,7 @@
   hfile = win32file._get_osfhandle(target_file.fileno())
   try:
     win32file.LockFileEx(hfile, flags, 0, -0x10000, _OVERLAPPED)
-  except pywintypes.error, exc_value:
+  except pywintypes.error as exc_value:
     if exc_value[0] == 33:
       raise LockException('Error trying acquiring lock of %s: %s' %
                           (target_file.name, exc_value[2]))
@@ -96,7 +96,7 @@
   hfile = win32file._get_osfhandle(target_file.fileno())
   try:
     win32file.UnlockFileEx(hfile, 0, -0x10000, _OVERLAPPED)
-  except pywintypes.error, exc_value:
+  except pywintypes.error as exc_value:
     if exc_value[0] == 158:
       # error: (158, 'UnlockFileEx', 'The segment is already unlocked.')
       # To match the 'posix' implementation, silently ignore this error
@@ -109,7 +109,7 @@
 def _LockImplPosix(target_file, flags):
   try:
     fcntl.flock(target_file.fileno(), flags)
-  except IOError, exc_value:
+  except IOError as exc_value:
     if exc_value[0] == 11 or exc_value[0] == 35:
       raise LockException('Error trying acquiring lock of %s: %s' %
                           (target_file.name, exc_value[1]))
diff --git a/catapult/common/py_utils/py_utils/lock_unittest.py b/catapult/common/py_utils/py_utils/lock_unittest.py
index a260621..7e17e55 100644
--- a/catapult/common/py_utils/py_utils/lock_unittest.py
+++ b/catapult/common/py_utils/py_utils/lock_unittest.py
@@ -2,14 +2,18 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import multiprocessing
 import os
+import tempfile
 import time
 import unittest
-import tempfile
-
 
 from py_utils import lock
+from six.moves import range # pylint: disable=redefined-builtin
 
 
 def _AppendTextToFile(file_name):
diff --git a/catapult/common/py_utils/py_utils/logging_util_unittest.py b/catapult/common/py_utils/py_utils/logging_util_unittest.py
index a957705..eb26098 100644
--- a/catapult/common/py_utils/py_utils/logging_util_unittest.py
+++ b/catapult/common/py_utils/py_utils/logging_util_unittest.py
@@ -2,15 +2,19 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 import logging
-import StringIO
 import unittest
 
+try:
+  from six import StringIO
+except ImportError:
+  from io import StringIO
+
 from py_utils import logging_util
 
 
 class LoggingUtilTest(unittest.TestCase):
   def testCapture(self):
-    s = StringIO.StringIO()
+    s = StringIO()
     with logging_util.CaptureLogs(s):
       logging.fatal('test')
 
diff --git a/catapult/common/py_utils/py_utils/memory_debug.py b/catapult/common/py_utils/py_utils/memory_debug.py
index e63938f..26f10ae 100755
--- a/catapult/common/py_utils/py_utils/memory_debug.py
+++ b/catapult/common/py_utils/py_utils/memory_debug.py
@@ -73,7 +73,9 @@
     pinfos_by_names[pname]['pids'].append(str(pinfo['pid']))
 
   sorted_pinfo_groups = heapq.nlargest(
-      top_n, pinfos_by_names.values(), key=lambda item: item['total_mem_rss'])
+      top_n,
+      list(pinfos_by_names.values()),
+      key=lambda item: item['total_mem_rss'])
   for group in sorted_pinfo_groups:
     group['total_mem_rss_fmt'] = FormatBytes(group['total_mem_rss'])
     group['pids_fmt'] = ', '.join(group['pids'])
diff --git a/catapult/common/py_utils/py_utils/modules_util.py b/catapult/common/py_utils/py_utils/modules_util.py
new file mode 100644
index 0000000..6c1106d
--- /dev/null
+++ b/catapult/common/py_utils/py_utils/modules_util.py
@@ -0,0 +1,35 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from distutils import version  # pylint: disable=no-name-in-module
+
+
+def RequireVersion(module, min_version, max_version=None):
+  """Ensure that an imported module's version is within a required range.
+
+  Version strings are parsed with LooseVersion, so versions like "1.8.0rc1"
+  (default numpy on macOS Sierra) and "2.4.13.2" (a version of OpenCV 2.x)
+  are allowed.
+
+  Args:
+    module: An already imported python module.
+    min_version: The module must have this or a higher version.
+    max_version: Optional, the module should not have this or a higher version.
+
+  Raises:
+    ImportError if the module's __version__ is not within the allowed range.
+  """
+  module_version = version.LooseVersion(module.__version__)
+  min_version = version.LooseVersion(str(min_version))
+  valid_version = min_version <= module_version
+
+  if max_version is not None:
+    max_version = version.LooseVersion(str(max_version))
+    valid_version = valid_version and (module_version < max_version)
+    wants_version = 'at or above %s and below %s' % (min_version, max_version)
+  else:
+    wants_version = '%s or higher' % min_version
+
+  if not valid_version:
+    raise ImportError('%s has version %s, but version %s is required' % (
+        module.__name__, module_version, wants_version))
diff --git a/catapult/common/py_utils/py_utils/modules_util_unittest.py b/catapult/common/py_utils/py_utils/modules_util_unittest.py
new file mode 100644
index 0000000..aa05674
--- /dev/null
+++ b/catapult/common/py_utils/py_utils/modules_util_unittest.py
@@ -0,0 +1,41 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from py_utils import modules_util
+
+
+class FakeModule(object):
+  def __init__(self, name, version):
+    self.__name__ = name
+    self.__version__ = version
+
+
+class ModulesUitlTest(unittest.TestCase):
+  def testRequireVersion_valid(self):
+    numpy = FakeModule('numpy', '2.3')
+    try:
+      modules_util.RequireVersion(numpy, '1.0')
+    except ImportError:
+      self.fail('ImportError raised unexpectedly')
+
+  def testRequireVersion_versionTooLow(self):
+    numpy = FakeModule('numpy', '2.3')
+    with self.assertRaises(ImportError) as error:
+      modules_util.RequireVersion(numpy, '2.5')
+    self.assertEqual(
+        str(error.exception),
+        'numpy has version 2.3, but version 2.5 or higher is required')
+
+  def testRequireVersion_versionTooHigh(self):
+    numpy = FakeModule('numpy', '2.3')
+    with self.assertRaises(ImportError) as error:
+      modules_util.RequireVersion(numpy, '1.0', '2.0')
+    self.assertEqual(
+        str(error.exception), 'numpy has version 2.3, but version'
+        ' at or above 1.0 and below 2.0 is required')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catapult/common/py_utils/py_utils/refactor/__init__.py b/catapult/common/py_utils/py_utils/refactor/__init__.py
index e3fbb5f..938ff68 100644
--- a/catapult/common/py_utils/py_utils/refactor/__init__.py
+++ b/catapult/common/py_utils/py_utils/refactor/__init__.py
@@ -12,7 +12,7 @@
 import multiprocessing
 
 # pylint: disable=wildcard-import
-from py_utils.refactor.annotated_symbol import *
+from py_utils.refactor.annotated_symbol import *  # pylint: disable=redefined-builtin
 from py_utils.refactor.module import Module
 
 
diff --git a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/__init__.py b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/__init__.py
index 610bc15..1bed84b 100644
--- a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/__init__.py
+++ b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/__init__.py
@@ -6,7 +6,7 @@
 from py_utils.refactor.annotated_symbol.class_definition import *
 from py_utils.refactor.annotated_symbol.function_definition import *
 from py_utils.refactor.annotated_symbol.import_statement import *
-from py_utils.refactor.annotated_symbol.reference import *
+from py_utils.refactor.annotated_symbol.reference import *  # pylint: disable=redefined-builtin
 from py_utils.refactor import snippet
 
 
@@ -55,7 +55,7 @@
   if not isinstance(node, snippet.Symbol):
     return node
 
-  children = map(_AnnotateNode, node.children)
+  children = [_AnnotateNode(c) for c in node.children]
 
   for symbol_type in ANNOTATED_GROUPINGS:
     annotated_grouping = symbol_type.Annotate(children)
diff --git a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/base_symbol.py b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/base_symbol.py
index 2e28e89..5e473bc 100644
--- a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/base_symbol.py
+++ b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/base_symbol.py
@@ -2,7 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 from py_utils.refactor import snippet
+from six.moves import range # pylint: disable=redefined-builtin
 
 
 class AnnotatedSymbol(snippet.Symbol):
@@ -23,7 +27,7 @@
     return super(AnnotatedSymbol, self).__setattr__(name, value)
 
   def Cut(self, child):
-    for i in xrange(len(self._children)):
+    for i in range(len(self._children)):
       if self._children[i] == child:
         self._modified = True
         del self._children[i]
diff --git a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/import_statement.py b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/import_statement.py
index 94e608c..6318eff 100644
--- a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/import_statement.py
+++ b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/import_statement.py
@@ -2,13 +2,17 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import itertools
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import keyword
 import symbol
 import token
 
-from py_utils.refactor.annotated_symbol import base_symbol
 from py_utils.refactor import snippet
+from py_utils.refactor.annotated_symbol import base_symbol
+from six.moves import zip_longest # pylint: disable=redefined-builtin
 
 
 __all__ = [
@@ -43,8 +47,7 @@
     self._children = self._children[:len(value_parts)*2-1]
 
     # Update child nodes.
-    for child, value_part in itertools.izip_longest(
-        self._children[::2], value_parts):
+    for child, value_part in zip_longest(self._children[::2], value_parts):
       if child:
         # Modify existing children. This helps preserve comments and spaces.
         child.value = value_part
@@ -83,13 +86,13 @@
       raise ValueError('%s is a reserved keyword.' % value)
 
     if value:
-       # pylint: disable=access-member-before-definition
+      # pylint: disable=access-member-before-definition
       if len(self.children) < 3:
         # If we currently have no alias, add one.
-         # pylint: disable=access-member-before-definition
+        # pylint: disable=access-member-before-definition
         self.children.append(
             snippet.TokenSnippet.Create(token.NAME, 'as', (0, 1)))
-         # pylint: disable=access-member-before-definition
+        # pylint: disable=access-member-before-definition
         self.children.append(
             snippet.TokenSnippet.Create(token.NAME, value, (0, 1)))
       else:
diff --git a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/reference.py b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/reference.py
index 9102c86..9a273d8 100644
--- a/catapult/common/py_utils/py_utils/refactor/annotated_symbol/reference.py
+++ b/catapult/common/py_utils/py_utils/refactor/annotated_symbol/reference.py
@@ -2,12 +2,17 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import itertools
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import symbol
 import token
 
-from py_utils.refactor.annotated_symbol import base_symbol
 from py_utils.refactor import snippet
+from py_utils.refactor.annotated_symbol import base_symbol
+from six.moves import range # pylint: disable=redefined-builtin
+from six.moves import zip_longest # pylint: disable=redefined-builtin
 
 
 __all__ = [
@@ -25,7 +30,7 @@
     if not nodes[0].children or nodes[0].children[0].type != token.NAME:
       return None
 
-    for i in xrange(1, len(nodes)):
+    for i in range(1, len(nodes)):
       if not nodes:
         break
       if nodes[i].type != symbol.trailer:
@@ -62,8 +67,7 @@
     self._children = self._children[:len(value_parts)]
 
     # Update child nodes.
-    for child, value_part in itertools.izip_longest(
-        self._children, value_parts):
+    for child, value_part in zip_longest(self._children, value_parts):
       if child:
         # Modify existing children. This helps preserve comments and spaces.
         child.children[-1].value = value_part
diff --git a/catapult/common/py_utils/py_utils/refactor/offset_token.py b/catapult/common/py_utils/py_utils/refactor/offset_token.py
index 5fa953e..deca085 100644
--- a/catapult/common/py_utils/py_utils/refactor/offset_token.py
+++ b/catapult/common/py_utils/py_utils/refactor/offset_token.py
@@ -1,18 +1,23 @@
+# Lint as: python2, python3
 # Copyright 2015 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 import collections
 import itertools
 import token
 import tokenize
+from six.moves import zip # pylint: disable=redefined-builtin
 
 
 def _Pairwise(iterable):
   """s -> (None, s0), (s0, s1), (s1, s2), (s2, s3), ..."""
   a, b = itertools.tee(iterable)
   a = itertools.chain((None,), a)
-  return itertools.izip(a, b)
+  return zip(a, b)
 
 
 class OffsetToken(object):
diff --git a/catapult/common/py_utils/py_utils/refactor/snippet.py b/catapult/common/py_utils/py_utils/refactor/snippet.py
index b98561a..7056abf 100644
--- a/catapult/common/py_utils/py_utils/refactor/snippet.py
+++ b/catapult/common/py_utils/py_utils/refactor/snippet.py
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import print_function
+
 import parser
 import symbol
 import sys
@@ -140,13 +142,13 @@
   def PrintTree(self, indent=0, stream=sys.stdout):
     stream.write(' ' * indent)
     if not self.tokens:
-      print >> stream, self.type_name
+      print(self.type_name, file=stream)
       return
 
-    print >> stream, '%-4s' % self.type_name, repr(self.tokens[0].string)
+    print('%-4s' % self.type_name, repr(self.tokens[0].string), file=stream)
     for tok in self.tokens[1:]:
       stream.write(' ' * indent)
-      print >> stream, ' ' * max(len(self.type_name), 4), repr(tok.string)
+      print(' ' * max(len(self.type_name), 4), repr(tok.string), file=stream)
 
 
 class Symbol(Snippet):
@@ -191,10 +193,10 @@
     # If there's only one child, collapse it onto the same line.
     node = self
     while len(node.children) == 1 and len(node.children[0].children) == 1:
-      print >> stream, node.type_name,
+      print(node.type_name, end=' ', file=stream)
       node = node.children[0]
 
-    print >> stream, node.type_name
+    print(node.type_name, file=stream)
     for child in node.children:
       child.PrintTree(indent + 2, stream)
 
diff --git a/catapult/common/py_utils/py_utils/refactor_util/move.py b/catapult/common/py_utils/py_utils/refactor_util/move.py
index d68f93b..6d0a7cb 100644
--- a/catapult/common/py_utils/py_utils/refactor_util/move.py
+++ b/catapult/common/py_utils/py_utils/refactor_util/move.py
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import print_function
+
 import functools
 import os
 import sys
@@ -35,7 +37,7 @@
         if move.UpdateImportAndReferences(module, import_statement):
           break
       except NotImplementedError as e:
-        print >> sys.stderr, 'Error updating %s: %s' % (module.file_path, e)
+        print('Error updating %s: %s' % (module.file_path, e), file=sys.stderr)
 
 
 class _Move(object):
diff --git a/catapult/common/py_utils/py_utils/retry_util.py b/catapult/common/py_utils/py_utils/retry_util.py
index e5826ca..a11bd80 100644
--- a/catapult/common/py_utils/py_utils/retry_util.py
+++ b/catapult/common/py_utils/py_utils/retry_util.py
@@ -1,9 +1,13 @@
 # Copyright 2018 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 import functools
 import logging
 import time
+from six.moves import range # pylint: disable=redefined-builtin
 
 
 def RetryOnException(exc_type, retries):
@@ -42,7 +46,7 @@
     def Wrapper(*args, **kwargs):
       wait = 1
       kwargs.setdefault('retries', retries)
-      for _ in xrange(kwargs['retries']):
+      for _ in range(kwargs['retries']):
         try:
           return f(*args, **kwargs)
         except exc_type as exc:
diff --git a/catapult/common/py_utils/py_utils/shell_util.py b/catapult/common/py_utils/py_utils/shell_util.py
index 2a529c8..6af7f8e 100644
--- a/catapult/common/py_utils/py_utils/shell_util.py
+++ b/catapult/common/py_utils/py_utils/shell_util.py
@@ -4,6 +4,8 @@
 #
 # Shell scripting helpers (created for Telemetry dependency roll scripts).
 
+from __future__ import print_function
+
 import os as _os
 import shutil as _shutil
 import subprocess as _subprocess
@@ -14,12 +16,12 @@
 def ScopedChangeDir(new_path):
   old_path = _os.getcwd()
   _os.chdir(new_path)
-  print '> cd', _os.getcwd()
+  print('> cd', _os.getcwd())
   try:
     yield
   finally:
     _os.chdir(old_path)
-    print '> cd', old_path
+    print('> cd', old_path)
 
 @_contextmanager
 def ScopedTempDir():
@@ -36,5 +38,5 @@
   args = [_os.path.join(*path_parts)] + list(args)
   env = dict(_os.environ)
   env.update(kwargs)
-  print '>', ' '.join(args)
+  print('>', ' '.join(args))
   _subprocess.check_call(args, env=env)
diff --git a/catapult/common/py_utils/py_utils/slots_metaclass_unittest.py b/catapult/common/py_utils/py_utils/slots_metaclass_unittest.py
index 79bb343..fe21b27 100644
--- a/catapult/common/py_utils/py_utils/slots_metaclass_unittest.py
+++ b/catapult/common/py_utils/py_utils/slots_metaclass_unittest.py
@@ -2,15 +2,21 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import unittest
 
 from py_utils import slots_metaclass
+import six
+
 
 class SlotsMetaclassUnittest(unittest.TestCase):
 
   def testSlotsMetaclass(self):
-    class NiceClass(object):
-      __metaclass__ = slots_metaclass.SlotsMetaclass
+
+    class NiceClass(six.with_metaclass(slots_metaclass.SlotsMetaclass, object)):
       __slots__ = '_nice',
 
       def __init__(self, nice):
diff --git a/catapult/common/py_utils/py_utils/tempfile_ext.py b/catapult/common/py_utils/py_utils/tempfile_ext.py
index 394ad5b..ba68c52 100644
--- a/catapult/common/py_utils/py_utils/tempfile_ext.py
+++ b/catapult/common/py_utils/py_utils/tempfile_ext.py
@@ -2,8 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-
 import contextlib
+import os
 import shutil
 import tempfile
 
@@ -28,3 +28,32 @@
     yield d
   finally:
     shutil.rmtree(d)
+
+
+@contextlib.contextmanager
+def NamedTemporaryFile(mode='w+b', suffix='', prefix='tmp'):
+  """A conext manager to hold a named temporary file.
+
+  It's similar to Python's tempfile.NamedTemporaryFile except:
+  - The file is _not_ deleted when you close the temporary file handle, so you
+    can close it and then use the name of the file to re-open it later.
+  - The file *is* always deleted when exiting the context managed code.
+  """
+  with NamedTemporaryDirectory() as temp_dir:
+    yield tempfile.NamedTemporaryFile(
+        mode=mode, suffix=suffix, prefix=prefix, dir=temp_dir, delete=False)
+
+
+@contextlib.contextmanager
+def TemporaryFileName(prefix='tmp', suffix=''):
+  """A context manager to just get the path to a file that does not exist.
+
+  The parent directory of the file is a newly clreated temporary directory,
+  and the name of the file is just `prefix + suffix`. The file istelf is not
+  created, you are in fact guaranteed that it does not exit.
+
+  The entire parent directory, possibly including the named temporary file and
+  any sibling files, is entirely deleted when exiting the context managed code.
+  """
+  with NamedTemporaryDirectory() as temp_dir:
+    yield os.path.join(temp_dir, prefix + suffix)
diff --git a/catapult/common/py_utils/py_utils/tempfile_ext_unittest.py b/catapult/common/py_utils/py_utils/tempfile_ext_unittest.py
index 6844623..76a0efd 100644
--- a/catapult/common/py_utils/py_utils/tempfile_ext_unittest.py
+++ b/catapult/common/py_utils/py_utils/tempfile_ext_unittest.py
@@ -2,14 +2,15 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import filecmp
 import os
+import shutil
 
 from py_utils import tempfile_ext
 from pyfakefs import fake_filesystem_unittest
 
 
 class NamedTemporaryDirectoryTest(fake_filesystem_unittest.TestCase):
-
   def setUp(self):
     self.setUpPyfakefs()
 
@@ -37,3 +38,37 @@
     self.fs.CreateDirectory(test_dir)
     with tempfile_ext.NamedTemporaryDirectory(dir=test_dir) as d:
       self.assertEquals(test_dir, os.path.dirname(d))
+
+
+class TemporaryFilesTest(fake_filesystem_unittest.TestCase):
+  def setUp(self):
+    self.setUpPyfakefs()
+
+  def tearDown(self):
+    self.tearDownPyfakefs()
+
+  def testNamedTemporaryFile(self):
+    with tempfile_ext.NamedTemporaryFile() as f:
+      self.assertTrue(os.path.isfile(f.name))
+      f.write('<data>')
+      f.close()
+      self.assertTrue(os.path.exists(f.name))
+      with open(f.name) as f2:
+        self.assertEqual(f2.read(), '<data>')
+
+    self.assertFalse(os.path.exists(f.name))
+
+  def testTemporaryFileName(self):
+    with tempfile_ext.TemporaryFileName('foo') as filepath:
+      self.assertTrue(os.path.basename(filepath), 'foo')
+      self.assertFalse(os.path.exists(filepath))
+
+      with open(filepath, 'w') as f:
+        f.write('<data>')
+      self.assertTrue(os.path.exists(filepath))
+
+      shutil.copyfile(filepath, filepath + '.bak')
+      self.assertTrue(filecmp.cmp(filepath, filepath + '.bak'))
+
+    self.assertFalse(os.path.exists(filepath))
+    self.assertFalse(os.path.exists(os.path.dirname(filepath)))
diff --git a/catapult/common/py_utils/py_utils/xvfb.py b/catapult/common/py_utils/py_utils/xvfb.py
index c09f3e3..06ce7dd 100644
--- a/catapult/common/py_utils/py_utils/xvfb.py
+++ b/catapult/common/py_utils/py_utils/xvfb.py
@@ -10,6 +10,8 @@
 
 
 def ShouldStartXvfb():
+  # TODO(crbug.com/973847): Note that you can locally change this to return
+  # False to diagnose timeouts for dev server tests.
   return platform.system() == 'Linux'
 
 
diff --git a/catapult/common/py_vulcanize/py_vulcanize/fake_fs.py b/catapult/common/py_vulcanize/py_vulcanize/fake_fs.py
index dfcb5e6..40b01bb 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/fake_fs.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/fake_fs.py
@@ -2,14 +2,19 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import codecs
+import collections
 import os
 import sys
-import collections
-import StringIO
+
+import six
 
 
-class WithableStringIO(StringIO.StringIO):
+class WithableStringIO(six.StringIO):
 
   def __enter__(self, *args):
     return self
@@ -23,7 +28,7 @@
   def __init__(self, initial_filenames_and_contents=None):
     self._file_contents = {}
     if initial_filenames_and_contents:
-      for k, v in initial_filenames_and_contents.iteritems():
+      for k, v in six.iteritems(initial_filenames_and_contents):
         self._file_contents[k] = v
 
     self._bound = False
@@ -106,7 +111,7 @@
 
   def _FakeWalk(self, top):
     assert os.path.isabs(top)
-    all_filenames = self._file_contents.keys()
+    all_filenames = list(self._file_contents.keys())
     pending_prefixes = collections.deque()
     pending_prefixes.append(top)
     visited_prefixes = set()
diff --git a/catapult/common/py_vulcanize/py_vulcanize/fake_fs_unittest.py b/catapult/common/py_vulcanize/py_vulcanize/fake_fs_unittest.py
index 0825013..7e225f5 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/fake_fs_unittest.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/fake_fs_unittest.py
@@ -34,19 +34,19 @@
     fs.AddFile('/a.txt', 'foobar')
     with fs:
       gen = os.walk(os.path.normpath('/'))
-      r = gen.next()
+      r = next(gen)
       self.assertEquals((os.path.normpath('/'), ['x'], ['a.txt']), r)
 
-      r = gen.next()
+      r = next(gen)
       self.assertEquals((os.path.normpath('/x'), ['w', 'w2'], ['y.txt']), r)
 
-      r = gen.next()
+      r = next(gen)
       self.assertEquals((os.path.normpath('/x/w'), [], ['z.txt']), r)
 
-      r = gen.next()
+      r = next(gen)
       self.assertEquals((os.path.normpath('/x/w2'), ['w3'], []), r)
 
-      r = gen.next()
+      r = next(gen)
       self.assertEquals((os.path.normpath('/x/w2/w3'), [], ['z3.txt']), r)
 
       self.assertRaises(StopIteration, gen.next)
diff --git a/catapult/common/py_vulcanize/py_vulcanize/generate.py b/catapult/common/py_vulcanize/py_vulcanize/generate.py
index 6368380..484c705 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/generate.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/generate.py
@@ -2,14 +2,23 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import os
-import sys
 import subprocess
+import sys
 import tempfile
-import StringIO
 
 from py_vulcanize import html_generation_controller
 
+try:
+  from six import StringIO
+except ImportError:
+  from io import StringIO
+
+
 
 html_warning_message = """
 
@@ -45,7 +54,7 @@
 
 
 def _AssertIsUTF8(f):
-  if isinstance(f, StringIO.StringIO):
+  if isinstance(f, StringIO):
     return
   assert f.encoding == 'utf-8'
 
@@ -79,7 +88,7 @@
                dir_for_include_tag_root=None,
                minify=False,
                report_sizes=False):
-  f = StringIO.StringIO()
+  f = StringIO()
   GenerateJSToFile(f,
                    load_sequence,
                    use_include_tags_for_scripts,
@@ -106,7 +115,7 @@
   if not minify:
     flatten_to_file = f
   else:
-    flatten_to_file = StringIO.StringIO()
+    flatten_to_file = StringIO()
 
   for module in load_sequence:
     module.AppendJSContentsToFile(flatten_to_file,
@@ -120,7 +129,7 @@
 
   if report_sizes:
     for module in load_sequence:
-      s = StringIO.StringIO()
+      s = StringIO()
       module.AppendJSContentsToFile(s,
                                     use_include_tags_for_scripts,
                                     dir_for_include_tag_root)
@@ -140,7 +149,8 @@
       sln = '.'.join(parts[:2])
 
       # Output
-      print '%i\t%s\t%s\t%s\t%s' % (len(js), min_js_size, module.name, tln, sln)
+      print(('%i\t%s\t%s\t%s\t%s' %
+             (len(js), min_js_size, module.name, tln, sln)))
       sys.stdout.flush()
 
 
@@ -192,7 +202,7 @@
 
 
 def GenerateStandaloneHTMLAsString(*args, **kwargs):
-  f = StringIO.StringIO()
+  f = StringIO()
   GenerateStandaloneHTMLToFile(f, *args, **kwargs)
   return f.getvalue()
 
diff --git a/catapult/common/py_vulcanize/py_vulcanize/html_module_unittest.py b/catapult/common/py_vulcanize/py_vulcanize/html_module_unittest.py
index fb4af16..e8438f4 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/html_module_unittest.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/html_module_unittest.py
@@ -2,9 +2,12 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import os
 import unittest
-import StringIO
 
 from py_vulcanize import fake_fs
 from py_vulcanize import generate
@@ -14,6 +17,7 @@
 from py_vulcanize import project as project_module
 from py_vulcanize import resource
 from py_vulcanize import resource_loader as resource_loader
+import six
 
 
 class ResourceWithFakeContents(resource.Resource):
@@ -40,7 +44,7 @@
     self._source_paths = source_paths
     self._file_contents = {}
     if initial_filenames_and_contents:
-      for k, v in initial_filenames_and_contents.iteritems():
+      for k, v in six.iteritems(initial_filenames_and_contents):
         self._file_contents[k] = v
 
   def FindResourceGivenAbsolutePath(self, absolute_path):
@@ -272,7 +276,7 @@
       loader = resource_loader.ResourceLoader(project)
       my_component = loader.LoadModule(module_name='a.b.my_component')
 
-      f = StringIO.StringIO()
+      f = six.StringIO()
       my_component.AppendJSContentsToFile(
           f,
           use_include_tags_for_scripts=False,
@@ -309,7 +313,7 @@
                         set([os.path.normpath('/tmp/a/b/my_component.html'),
                              os.path.normpath('/tmp/a/something.jpg')]))
 
-      f = StringIO.StringIO()
+      f = six.StringIO()
       ctl = html_generation_controller.HTMLGenerationController()
       my_component.AppendHTMLContentsToFile(f, ctl)
       html = f.getvalue().rstrip()
diff --git a/catapult/common/py_vulcanize/py_vulcanize/module.py b/catapult/common/py_vulcanize/py_vulcanize/module.py
index bd6a68f..d27f350 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/module.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/module.py
@@ -11,11 +11,16 @@
 Other resources include HTML templates, raw JavaScript files, and stylesheets.
 """
 
-import os
-import inspect
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import codecs
+import inspect
+import os
 
 from py_vulcanize import js_utils
+import six
 
 
 class DepsException(Exception):
@@ -92,7 +97,7 @@
   """
 
   def __init__(self, loader, name, resource, load_resource=True):
-    assert isinstance(name, basestring), 'Got %s instead' % repr(name)
+    assert isinstance(name, six.string_types), 'Got %s instead' % repr(name)
 
     global _next_module_id
     self._id = _next_module_id
diff --git a/catapult/common/py_vulcanize/py_vulcanize/parse_html_deps.py b/catapult/common/py_vulcanize/py_vulcanize/parse_html_deps.py
index 4a0888c..88ce218 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/parse_html_deps.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/parse_html_deps.py
@@ -2,13 +2,18 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import os
 import sys
 
+from py_vulcanize import html_generation_controller
 from py_vulcanize import js_utils
 from py_vulcanize import module
 from py_vulcanize import strip_js_comments
-from py_vulcanize import html_generation_controller
+import six
 
 
 def _AddToPathIfNeeded(path):
@@ -53,7 +58,7 @@
 
   @property
   def contents(self):
-    return unicode(self._soup.string)
+    return six.text_type(self._soup.string)
 
   @property
   def stripped_contents(self):
@@ -187,13 +192,13 @@
   @property
   def inline_stylesheets(self):
     tags = self._soup.findAll('style')
-    return [unicode(t.string) for t in tags]
+    return [six.text_type(t.string) for t in tags]
 
   def YieldHTMLInPieces(self, controller, minify=False):
     yield self.GenerateHTML(controller, minify)
 
   def GenerateHTML(self, controller, minify=False, prettify=False):
-    soup = _CreateSoupWithoutHeadOrBody(unicode(self._soup))
+    soup = _CreateSoupWithoutHeadOrBody(six.text_type(self._soup))
 
     # Remove declaration.
     for x in soup.contents:
@@ -223,7 +228,7 @@
     # Process all in-line styles.
     inline_styles = soup.findAll('style')
     for style in inline_styles:
-      html = controller.GetHTMLForInlineStylesheet(unicode(style.string))
+      html = controller.GetHTMLForInlineStylesheet(six.text_type(style.string))
       if html:
         ns = soup.new_tag('style')
         ns.append(bs4.NavigableString(html))
@@ -252,7 +257,7 @@
       return soup.prettify('utf-8').strip()
 
     # We are done.
-    return unicode(soup).strip()
+    return six.text_type(soup).strip()
 
   @property
   def html_contents_without_links_and_script(self):
diff --git a/catapult/common/py_vulcanize/py_vulcanize/project.py b/catapult/common/py_vulcanize/py_vulcanize/project.py
index d8f9ca4..7a16988 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/project.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/project.py
@@ -2,11 +2,19 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
 import collections
 import os
-import cStringIO
+
+try:
+  from six import StringIO
+except ImportError:
+  from io import StringIO
 
 from py_vulcanize import resource_loader
+import six
 
 
 def _FindAllFilesRecursive(source_paths):
@@ -209,7 +217,7 @@
     self.edges = []
 
   def AddModule(self, m):
-    f = cStringIO.StringIO()
+    f = StringIO()
     m.AppendJSContentsToFile(f, False, None)
 
     attrs = {
@@ -218,7 +226,7 @@
 
     f.close()
 
-    attr_items = ['%s="%s"' % (x, y) for x, y in attrs.iteritems()]
+    attr_items = ['%s="%s"' % (x, y) for x, y in six.iteritems(attrs)]
     node = 'M%i [%s];' % (m.id, ','.join(attr_items))
     self.nodes.append(node)
 
diff --git a/catapult/common/py_vulcanize/py_vulcanize/strip_js_comments.py b/catapult/common/py_vulcanize/py_vulcanize/strip_js_comments.py
index d63c667..73c3a88 100644
--- a/catapult/common/py_vulcanize/py_vulcanize/strip_js_comments.py
+++ b/catapult/common/py_vulcanize/py_vulcanize/strip_js_comments.py
@@ -51,14 +51,14 @@
   token_stream = _TokenizeJS(text).__iter__()
   while True:
     try:
-      t = token_stream.next()
+      t = next(token_stream)
     except StopIteration:
       break
 
     if t == '//':
       while True:
         try:
-          t2 = token_stream.next()
+          t2 = next(token_stream)
           if t2 == '\n':
             break
         except StopIteration:
@@ -67,7 +67,7 @@
       nesting = 1
       while True:
         try:
-          t2 = token_stream.next()
+          t2 = next(token_stream)
           if t2 == '/*':
             nesting += 1
           elif t2 == '*/':
diff --git a/catapult/dependency_manager/dependency_manager/local_path_info.py b/catapult/dependency_manager/dependency_manager/local_path_info.py
index de544c7..8ac0152 100644
--- a/catapult/dependency_manager/dependency_manager/local_path_info.py
+++ b/catapult/dependency_manager/dependency_manager/local_path_info.py
@@ -32,7 +32,7 @@
       Local file path, if found, or None otherwise.
     """
     for priority_group in self._path_priority_groups:
-      priority_group = filter(os.path.exists, priority_group)
+      priority_group = [g for g in priority_group if os.path.exists(g)]
       if not priority_group:
         continue
       return max(priority_group, key=lambda path: os.stat(path).st_mtime)
diff --git a/catapult/devil/BUILD.gn b/catapult/devil/BUILD.gn
index 661a24f..cf1255d 100644
--- a/catapult/devil/BUILD.gn
+++ b/catapult/devil/BUILD.gn
@@ -2,20 +2,31 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//build/symlink.gni")
-import("//testing/android/empty_apk/empty_apk.gni")
-
-empty_apk("empty_system_webview_apk") {
-  package_name = "com.android.webview"
-  apk_name = "EmptySystemWebView"
-}
-
 group("devil") {
   testonly = true
-  deps = [
-    ":empty_system_webview_apk",
-    "//buildtools/third_party/libc++($host_toolchain)",
-    "//tools/android/forwarder2",
-    "//tools/android/md5sum",
+  deps = []
+  data_deps = [
+    "../third_party/gsutil",
   ]
+  data = [
+    "devil/",
+  ]
+
+  if (is_android) {
+    deps += [
+      ":empty_system_webview_apk",
+      "//buildtools/third_party/libc++($host_toolchain)",
+      "//tools/android/forwarder2",
+      "//tools/android/md5sum",
+    ]
+  }
+}
+
+if (is_android) {
+  import("//testing/android/empty_apk/empty_apk.gni")
+
+  empty_apk("empty_system_webview_apk") {
+    package_name = "com.android.webview"
+    apk_name = "EmptySystemWebView"
+  }
 }
diff --git a/catapult/devil/README.md b/catapult/devil/README.md
index 852ac37..9953e6a 100644
--- a/catapult/devil/README.md
+++ b/catapult/devil/README.md
@@ -6,8 +6,8 @@
 
 😈
 
-devil is a library used by the Chromium developers to interact with Android
-devices. It currently supports SDK level 16 and above.
+devil (device interaction layer) is a library used by the Chromium developers to
+interact with Android devices. It currently supports SDK level 16 and above.
 
 ## Interfaces
 
diff --git a/catapult/devil/bin/run_py_tests b/catapult/devil/bin/run_py_tests
index 44ec61e..a74fa83 100755
--- a/catapult/devil/bin/run_py_tests
+++ b/catapult/devil/bin/run_py_tests
@@ -16,6 +16,12 @@
 
 
 def main():
+  # Tests mock out internal details of methods, and the ANDROID_SERIAL can
+  # change which internal methods are called. Since tests don't actually use
+  # devices, it should be fine to delete the variable.
+  if 'ANDROID_SERIAL' in os.environ:
+    del os.environ['ANDROID_SERIAL']
+
   return run_with_typ.Run(top_level_dir=_DEVIL_PATH)
 
 if __name__ == '__main__':
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py
index ab7649f..abdf907 100644
--- a/catapult/devil/devil/android/apk_helper.py
+++ b/catapult/devil/devil/android/apk_helper.py
@@ -5,10 +5,13 @@
 """Module containing utilities for apk packages."""
 
 import re
+import xml.etree.ElementTree
 import zipfile
 
 from devil import base_error
+from devil.android.ndk import abis
 from devil.android.sdk import aapt
+from devil.utils import cmd_helper
 
 
 _MANIFEST_ATTRIBUTE_RE = re.compile(
@@ -45,9 +48,8 @@
 # matches the height of the stack). Each line parsed (either an attribute or an
 # element) is added to the node at the top of the stack (after the stack has
 # been popped/pushed due to indentation).
-def _ParseManifestFromApk(apk_path):
-  aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml')
-
+def _ParseManifestFromApk(apk):
+  aapt_output = aapt.Dump('xmltree', apk.path, 'AndroidManifest.xml')
   parsed_manifest = {}
   node_stack = [parsed_manifest]
   indent = '  '
@@ -96,7 +98,8 @@
       manifest_key = m.group(1)
       if manifest_key in node:
         raise base_error.BaseError(
-            "A single attribute should have one key and one value")
+            "A single attribute should have one key and one value: {}"
+            .format(line))
       else:
         node[manifest_key] = m.group(2) or m.group(3)
       continue
@@ -104,6 +107,47 @@
   return parsed_manifest
 
 
+def _ParseManifestFromBundle(bundle):
+  cmd = [bundle.path, 'dump-manifest']
+  status, stdout, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
+  if status != 0:
+    raise Exception('Failed running {} with output\n{}\n{}'.format(
+        ' '.join(cmd), stdout, stderr))
+  return ParseManifestFromXml(stdout)
+
+
+def ParseManifestFromXml(xml_str):
+  """Parse an android bundle manifest.
+
+    As ParseManifestFromAapt, but uses the xml output from bundletool. Each
+    element is a dict, mapping attribute or children by name. Attributes map to
+    a dict (as they are unique), children map to a list of dicts (as there may
+    be multiple children with the same name).
+
+  Args:
+    xml_str (str) An xml string that is an android manifest.
+
+  Returns:
+    A dict holding the parsed manifest, as with ParseManifestFromAapt.
+  """
+  root = xml.etree.ElementTree.fromstring(xml_str)
+  return {root.tag: [_ParseManifestXMLNode(root)]}
+
+
+def _ParseManifestXMLNode(node):
+  out = {}
+  for name, value in node.attrib.items():
+    cleaned_name = name.replace(
+        '{http://schemas.android.com/apk/res/android}',
+        'android:').replace(
+            '{http://schemas.android.com/tools}',
+            'tools:')
+    out[cleaned_name] = value
+  for child in node:
+    out.setdefault(child.tag, []).append(_ParseManifestXMLNode(child))
+  return out
+
+
 def _ParseNumericKey(obj, key, default=0):
   val = obj.get(key)
   if val is None:
@@ -152,6 +196,10 @@
   def path(self):
     return self._apk_path
 
+  @property
+  def is_bundle(self):
+    return self._apk_path.endswith('_bundle')
+
   def GetActivityName(self):
     """Returns the name of the first launcher Activity in the apk."""
     manifest_info = self._GetManifest()
@@ -233,9 +281,73 @@
     except KeyError:
       return []
 
+  def GetVersionCode(self):
+    """Returns the versionCode as an integer, or None if not available."""
+    manifest_info = self._GetManifest()
+    try:
+      version_code = manifest_info['manifest'][0]['android:versionCode']
+      return int(version_code, 16)
+    except KeyError:
+      return None
+
+  def GetVersionName(self):
+    """Returns the versionName as a string."""
+    manifest_info = self._GetManifest()
+    try:
+      version_name = manifest_info['manifest'][0]['android:versionName']
+      return version_name
+    except KeyError:
+      return ''
+
+  def GetMinSdkVersion(self):
+    """Returns the minSdkVersion as a string, or None if not available.
+
+    Note: this cannot always be cast to an integer."""
+    manifest_info = self._GetManifest()
+    try:
+      uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+      min_sdk_version = uses_sdk['android:minSdkVersion']
+      try:
+        # The common case is for this to be an integer. Convert to decimal
+        # notation (rather than hexadecimal) for readability, but convert back
+        # to a string for type consistency with the general case.
+        return str(int(min_sdk_version, 16))
+      except ValueError:
+        # In general (ex. apps with minSdkVersion set to pre-release Android
+        # versions), minSdkVersion can be a string (usually, the OS codename
+        # letter). For simplicity, don't do any validation on the value.
+        return min_sdk_version
+    except KeyError:
+      return None
+
+  def GetTargetSdkVersion(self):
+    """Returns the targetSdkVersion as a string, or None if not available.
+
+    Note: this cannot always be cast to an integer."""
+    manifest_info = self._GetManifest()
+    try:
+      uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+      target_sdk_version = uses_sdk['android:targetSdkVersion']
+      try:
+        # The common case is for this to be an integer. Convert to decimal
+        # notation (rather than hexadecimal) for readability, but convert back
+        # to a string for type consistency with the general case.
+        return str(int(target_sdk_version, 16))
+      except ValueError:
+        # In general (ex. apps targeting pre-release Android versions),
+        # targetSdkVersion can be a string (usually, the OS codename letter).
+        # For simplicity, don't do any validation on the value.
+        return target_sdk_version
+    except KeyError:
+      return None
+
   def _GetManifest(self):
     if not self._manifest:
-      self._manifest = _ParseManifestFromApk(self._apk_path)
+      app = ToHelper(self._apk_path)
+      if app.is_bundle:
+        self._manifest = _ParseManifestFromBundle(app)
+      else:
+        self._manifest = _ParseManifestFromApk(app)
     return self._manifest
 
   def _ResolveName(self, name):
@@ -257,10 +369,10 @@
       if len(path_tokens) >= 2 and path_tokens[0] == 'lib':
         libs.add(path_tokens[1])
     lib_to_abi = {
-        'armeabi-v7a': ['armeabi-v7a', 'arm64-v8a'],
-        'arm64-v8a': ['arm64-v8a'],
-        'x86': ['x86', 'x64'],
-        'x64': ['x64']
+        abis.ARM: [abis.ARM, abis.ARM_64],
+        abis.ARM_64: [abis.ARM_64],
+        abis.X86: [abis.X86, abis.X86_64],
+        abis.X86_64: [abis.X86_64]
     }
     try:
       output = set()
diff --git a/catapult/devil/devil/android/apk_helper_test.py b/catapult/devil/devil/android/apk_helper_test.py
index 3be9d81..3258bb0 100755
--- a/catapult/devil/devil/android/apk_helper_test.py
+++ b/catapult/devil/devil/android/apk_helper_test.py
@@ -10,16 +10,23 @@
 from devil import base_error
 from devil import devil_env
 from devil.android import apk_helper
+from devil.android.ndk import abis
 from devil.utils import mock_calls
 
 with devil_env.SysPath(devil_env.PYMOCK_PATH):
   import mock  # pylint: disable=import-error
 
 
+# pylint: disable=line-too-long
 _MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
   E: manifest (line=1)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x166de1ea
+    A: android:versionName(0x0101021c)="75.0.3763.0" (Raw: "75.0.3763.0")
     A: package="org.chromium.abc" (Raw: "org.chromium.abc")
     A: split="random_split" (Raw: "random_split")
+    E: uses-sdk (line=2)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x15
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
     E: uses-permission (line=2)
       A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
     E: uses-permission (line=3)
@@ -113,6 +120,14 @@
       A: junit4=(type 0x12)0xffffffff (Raw: "true")
 """
 
+_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=1)
+    A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
+    E: uses-sdk (line=2)
+      A: android:minSdkVersion(0x0101020c)="Q" (Raw: "Q")
+      A: android:targetSdkVersion(0x01010270)="Q" (Raw: "Q")
+"""
+
 _NO_NAMESPACE_MANIFEST_DUMP = """E: manifest (line=1)
   A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
   E: instrumentation (line=8)
@@ -120,6 +135,7 @@
     A: http://schemas.android.com/apk/res/android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
     A: http://schemas.android.com/apk/res/android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
 """
+# pylint: enable=line-too-long
 
 
 def _MockAaptDump(manifest_dump):
@@ -221,6 +237,36 @@
       self.assertEquals([('name1', 'value1'), ('name2', 'value2')],
                         helper.GetAllMetadata())
 
+  def testGetVersionCode(self):
+    with _MockAaptDump(_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals(376300010, helper.GetVersionCode())
+
+  def testGetVersionName(self):
+    with _MockAaptDump(_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals('75.0.3763.0', helper.GetVersionName())
+
+  def testGetMinSdkVersion_integerValue(self):
+    with _MockAaptDump(_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals('21', helper.GetMinSdkVersion())
+
+  def testGetMinSdkVersion_stringValue(self):
+    with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals('Q', helper.GetMinSdkVersion())
+
+  def testGetTargetSdkVersion_integerValue(self):
+    with _MockAaptDump(_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals('28', helper.GetTargetSdkVersion())
+
+  def testGetTargetSdkVersion_stringValue(self):
+    with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
+      helper = apk_helper.ApkHelper('')
+      self.assertEquals('Q', helper.GetTargetSdkVersion())
+
   def testGetSingleInstrumentationName_strippedNamespaces(self):
     with _MockAaptDump(_NO_NAMESPACE_MANIFEST_DUMP):
       helper = apk_helper.ApkHelper('')
@@ -229,8 +275,8 @@
 
   def testGetArchitectures(self):
     AbiPair = collections.namedtuple('AbiPair', ['abi32bit', 'abi64bit'])
-    for abi_pair in [AbiPair('lib/armeabi-v7a', 'lib/arm64-v8a'),
-                     AbiPair('lib/x86', 'lib/x64')]:
+    for abi_pair in [AbiPair('lib/' + abis.ARM, 'lib/' + abis.ARM_64),
+                     AbiPair('lib/' + abis.X86, 'lib/' + abis.X86_64)]:
       with _MockListApkPaths([abi_pair.abi32bit]):
         helper = apk_helper.ApkHelper('')
         self.assertEquals(set([os.path.basename(abi_pair.abi32bit),
@@ -246,6 +292,91 @@
         self.assertEquals(set([os.path.basename(abi_pair.abi64bit)]),
                           set(helper.GetAbis()))
 
+  def testParseXmlManifest(self):
+    self.assertEquals({
+        'manifest': [
+            {'android:compileSdkVersion': '28',
+             'android:versionCode': '2',
+             'uses-sdk': [
+                 {'android:minSdkVersion': '24',
+                  'android:targetSdkVersion': '28'}],
+             'uses-permission': [
+                 {'android:name':
+                  'android.permission.ACCESS_COARSE_LOCATION'},
+                 {'android:name':
+                  'android.permission.ACCESS_NETWORK_STATE'}],
+             'application': [
+                 {'android:allowBackup': 'true',
+                  'android:extractNativeLibs': 'false',
+                  'android:fullBackupOnly': 'false',
+                  'meta-data': [
+                      {'android:name': 'android.allow_multiple',
+                       'android:value': 'true'},
+                      {'android:name': 'multiwindow',
+                       'android:value': 'true'}],
+                  'activity': [
+                      {'android:configChanges': '0x00001fb3',
+                       'android:excludeFromRecents': 'true',
+                       'android:name': 'ChromeLauncherActivity',
+                       'intent-filter': [
+                           {'action': [
+                               {'android:name': 'dummy.action'}],
+                            'category': [
+                                {'android:name': 'DAYDREAM'},
+                                {'android:name': 'CARDBOARD'}]}]},
+                      {'android:enabled': 'false',
+                       'android:name': 'MediaLauncherActivity',
+                       'intent-filter': [
+                           {'tools:ignore': 'AppLinkUrlError',
+                            'action': [{'android:name': 'VIEW'}],
+                            'category': [{'android:name': 'DEFAULT'}],
+                            'data': [
+                                {'android:mimeType': 'audio/*'},
+                                {'android:mimeType': 'image/*'},
+                                {'android:mimeType': 'video/*'},
+                                {'android:scheme': 'file'},
+                                {'android:scheme': 'content'}]}]}]}]}]},
+        apk_helper.ParseManifestFromXml("""
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:tools="http://schemas.android.com/tools"
+              android:compileSdkVersion="28" android:versionCode="2">
+      <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28"/>
+      <uses-permission
+         android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+      <application android:allowBackup="true"
+                   android:extractNativeLibs="false"
+                   android:fullBackupOnly="false">
+        <meta-data android:name="android.allow_multiple"
+                   android:value="true"/>
+        <meta-data android:name="multiwindow"
+                   android:value="true"/>
+        <activity android:configChanges="0x00001fb3"
+                  android:excludeFromRecents="true"
+                  android:name="ChromeLauncherActivity">
+            <intent-filter>
+                <action android:name="dummy.action"/>
+                <category android:name="DAYDREAM"/>
+                <category android:name="CARDBOARD"/>
+            </intent-filter>
+        </activity>
+        <activity android:enabled="false"
+                  android:name="MediaLauncherActivity">
+            <intent-filter tools:ignore="AppLinkUrlError">
+                <action android:name="VIEW"/>
+
+                <category android:name="DEFAULT"/>
+
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="image/*"/>
+                <data android:mimeType="video/*"/>
+                <data android:scheme="file"/>
+                <data android:scheme="content"/>
+            </intent-filter>
+        </activity>
+      </application>
+    </manifest>"""))
+
 
 if __name__ == '__main__':
   unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/battery_utils.py b/catapult/devil/devil/android/battery_utils.py
index 9c83b5b..c41c19a 100644
--- a/catapult/devil/devil/android/battery_utils.py
+++ b/catapult/devil/devil/android/battery_utils.py
@@ -241,44 +241,6 @@
         'Unable to find fuel gauge.')
 
   @decorators.WithTimeoutAndRetriesFromInstance()
-  def GetNetworkData(self, package, timeout=None, retries=None):
-    """Get network data for specific package.
-
-    Args:
-      package: package name you want network data for.
-      timeout: timeout in seconds
-      retries: number of retries
-
-    Returns:
-      Tuple of (sent_data, recieved_data)
-      None if no network data found
-    """
-    # If device_utils clears cache, cache['uids'] doesn't exist
-    if 'uids' not in self._cache:
-      self._cache['uids'] = {}
-    if package not in self._cache['uids']:
-      self.GetPowerData()
-      if package not in self._cache['uids']:
-        logger.warning('No UID found for %s. Can\'t get network data.',
-                       package)
-        return None
-
-    network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
-    try:
-      send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
-    # If ReadFile throws exception, it means no network data usage file for
-    # package has been recorded. Return 0 sent and 0 received.
-    except device_errors.AdbShellCommandFailedError:
-      logger.warning('No sent data found for package %s', package)
-      send_data = 0
-    try:
-      recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
-    except device_errors.AdbShellCommandFailedError:
-      logger.warning('No received data found for package %s', package)
-      recv_data = 0
-    return (send_data, recv_data)
-
-  @decorators.WithTimeoutAndRetriesFromInstance()
   def GetPowerData(self, timeout=None, retries=None):
     """Get power data for device.
 
diff --git a/catapult/devil/devil/android/battery_utils_test.py b/catapult/devil/devil/android/battery_utils_test.py
index feccf79..07c7496 100755
--- a/catapult/devil/devil/android/battery_utils_test.py
+++ b/catapult/devil/devil/android/battery_utils_test.py
@@ -401,57 +401,6 @@
       self.assertFalse(self.battery.GetCharging())
 
 
-class BatteryUtilsGetNetworkDataTest(BatteryUtilsTest):
-
-  def testGetNetworkData_noDataUsage(self):
-    with self.assertCalls(
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '-c'],
-            check_return=True, large_output=True),
-         _DUMPSYS_OUTPUT),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'),
-            self.ShellError()),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'),
-            self.ShellError())):
-      self.assertEquals(self.battery.GetNetworkData('test_package1'), (0, 0))
-
-  def testGetNetworkData_badPackage(self):
-    with self.assertCall(
-        self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '-c'],
-            check_return=True, large_output=True),
-        _DUMPSYS_OUTPUT):
-      self.assertEqual(self.battery.GetNetworkData('asdf'), None)
-
-  def testGetNetworkData_packageNotCached(self):
-    with self.assertCalls(
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '-c'],
-            check_return=True, large_output=True),
-         _DUMPSYS_OUTPUT),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
-      self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
-  def testGetNetworkData_packageCached(self):
-    self.battery._cache['uids'] = {'test_package1': '1000'}
-    with self.assertCalls(
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
-      self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
-  def testGetNetworkData_clearedCache(self):
-    with self.assertCalls(
-        (self.call.device.RunShellCommand(
-            ['dumpsys', 'batterystats', '-c'],
-            check_return=True, large_output=True),
-         _DUMPSYS_OUTPUT),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
-        (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
-      self.battery._cache.clear()
-      self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
-
 class BatteryUtilsLetBatteryCoolToTemperatureTest(BatteryUtilsTest):
 
   @mock.patch('time.sleep', mock.Mock())
diff --git a/catapult/devil/devil/android/cpu_temperature.py b/catapult/devil/devil/android/cpu_temperature.py
new file mode 100644
index 0000000..58ce87a
--- /dev/null
+++ b/catapult/devil/devil/android/cpu_temperature.py
@@ -0,0 +1,154 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Provides device interactions for CPU temperature monitoring."""
+# pylint: disable=unused-argument
+
+import logging
+
+from devil.android import device_utils
+from devil.android.perf import perf_control
+from devil.utils import timeout_retry
+
+logger = logging.getLogger(__name__)
+
+# NB: when adding devices to this structure, be aware of the impact it may
+# have on the chromium.perf waterfall, as it may increase testing time.
+# Please contact a person responsible for the waterfall to see if the
+# device you're adding is currently being tested.
+_DEVICE_THERMAL_INFORMATION = {
+    # Pixel 3
+    'blueline': {
+        'cpu_temps': {
+            # See /sys/class/thermal/thermal_zone<number>/type for description
+            # Types:
+            # cpu0: cpu0-silver-step
+            # cpu1: cpu1-silver-step
+            # cpu2: cpu2-silver-step
+            # cpu3: cpu3-silver-step
+            # cpu4: cpu0-gold-step
+            # cpu5: cpu1-gold-step
+            # cpu6: cpu2-gold-step
+            # cpu7: cpu3-gold-step
+            'cpu0': '/sys/class/thermal/thermal_zone11/temp',
+            'cpu1': '/sys/class/thermal/thermal_zone12/temp',
+            'cpu2': '/sys/class/thermal/thermal_zone13/temp',
+            'cpu3': '/sys/class/thermal/thermal_zone14/temp',
+            'cpu4': '/sys/class/thermal/thermal_zone15/temp',
+            'cpu5': '/sys/class/thermal/thermal_zone16/temp',
+            'cpu6': '/sys/class/thermal/thermal_zone17/temp',
+            'cpu7': '/sys/class/thermal/thermal_zone18/temp'
+        },
+        # Different device sensors use different multipliers
+        # e.g. Pixel 3 35 degrees c is 35000
+        'temp_multiplier': 1000
+    },
+    # Pixel
+    'sailfish': {
+        'cpu_temps': {
+            # The following thermal zones tend to produce the most accurate
+            # readings
+            # Types:
+            # cpu0: tsens_tz_sensor0
+            # cpu1: tsens_tz_sensor1
+            # cpu2: tsens_tz_sensor2
+            # cpu3: tsens_tz_sensor3
+            'cpu0': '/sys/class/thermal/thermal_zone1/temp',
+            'cpu1': '/sys/class/thermal/thermal_zone2/temp',
+            'cpu2': '/sys/class/thermal/thermal_zone3/temp',
+            'cpu3': '/sys/class/thermal/thermal_zone4/temp'
+        },
+        'temp_multiplier': 10
+    }
+}
+
+
+class CpuTemperature(object):
+
+  def __init__(self, device):
+    """CpuTemperature constructor.
+
+      Args:
+        device: A DeviceUtils instance.
+      Raises:
+        TypeError: If it is not passed a DeviceUtils instance.
+    """
+    if not isinstance(device, device_utils.DeviceUtils):
+      raise TypeError('Must be initialized with DeviceUtils object.')
+    self._device = device
+    self._perf_control = perf_control.PerfControl(self._device)
+    self._device_info = None
+
+  def InitThermalDeviceInformation(self):
+    """Init the current devices thermal information.
+    """
+    self._device_info = _DEVICE_THERMAL_INFORMATION.get(
+                        self._device.build_product)
+
+  def IsSupported(self):
+    """Check if the current device is supported.
+
+      Returns:
+        True if the device is in _DEVICE_THERMAL_INFORMATION and the temp
+        files exist. False otherwise.
+    """
+    # Init device info if it hasnt been manually initialised already
+    if self._device_info is None:
+      self.InitThermalDeviceInformation()
+
+    if self._device_info is not None:
+      return all(
+          self._device.FileExists(f)
+          for f in self._device_info['cpu_temps'].values())
+    return False
+
+  def LetCpuCoolToTemperature(self, target_temp, wait_period=30):
+    """Lets device sit to give CPU time to cool down.
+
+      Implements a similar mechanism to
+      battery_utils.LetBatteryCoolToTemperature
+
+      Args:
+        temp: A float containing the maximum temperature to allow
+          in degrees c.
+        wait_period: An integer indicating time in seconds to wait
+          between checking.
+    """
+    target_temp = int(target_temp * self._device_info['temp_multiplier'])
+
+    def cool_cpu():
+      # Get the temperatures
+      cpu_temp_paths = self._device_info['cpu_temps']
+      temps = []
+      for temp_path in cpu_temp_paths.values():
+        temp_return = self._device.ReadFile(temp_path)
+        # Output is an array of strings, only need the first line.
+        temps.append(int(temp_return))
+
+      if not temps:
+        logger.warning('Unable to read temperature files provided.')
+        return True
+
+      logger.info('Current CPU temperatures: %s', str(temps)[1:-1])
+
+      return all(t <= target_temp for t in temps)
+
+    logger.info('Waiting for the CPU to cool down to %s',
+                target_temp / self._device_info['temp_multiplier'])
+
+    # Set the governor to powersave to aid the cooling down of the CPU
+    self._perf_control.SetScalingGovernor('powersave')
+
+    # Retry 3 times, each time waiting 30 seconds.
+    # This negates most (if not all) of the noise in recorded results without
+    # taking too long
+    timeout_retry.WaitFor(cool_cpu, wait_period=wait_period, max_tries=3)
+
+    # Set the performance mode
+    self._perf_control.SetHighPerfMode()
+
+  def GetDeviceForTesting(self):
+    return self._device
+
+  def GetDeviceInfoForTesting(self):
+    return self._device_info
diff --git a/catapult/devil/devil/android/cpu_temperature_test.py b/catapult/devil/devil/android/cpu_temperature_test.py
new file mode 100644
index 0000000..f0f99de
--- /dev/null
+++ b/catapult/devil/devil/android/cpu_temperature_test.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""
+Unit tests for the contents of cpu_temperature.py
+"""
+
+# pylint: disable=unused-argument
+
+import logging
+import unittest
+
+from devil import devil_env
+from devil.android import cpu_temperature
+from devil.android import device_utils
+from devil.utils import mock_calls
+from devil.android.sdk import adb_wrapper
+
+with devil_env.SysPath(devil_env.PYMOCK_PATH):
+  import mock  # pylint: disable=import-error
+
+
+class CpuTemperatureTest(mock_calls.TestCase):
+
+  @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+  def setUp(self):
+    # Mock the device
+    self.mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+    self.mock_device.build_product = 'blueline'
+    self.mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
+    self.mock_device.FileExists.return_value = True
+
+    self.cpu_temp = cpu_temperature.CpuTemperature(self.mock_device)
+    self.cpu_temp.InitThermalDeviceInformation()
+
+
+class CpuTemperatureInitTest(unittest.TestCase):
+
+  @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+  def testInitWithDeviceUtil(self):
+    d = mock.Mock(spec=device_utils.DeviceUtils)
+    d.build_product = 'blueline'
+    c = cpu_temperature.CpuTemperature(d)
+    self.assertEqual(d, c.GetDeviceForTesting())
+
+  def testInitWithMissing_fails(self):
+    with self.assertRaises(TypeError):
+      cpu_temperature.CpuTemperature(None)
+    with self.assertRaises(TypeError):
+      cpu_temperature.CpuTemperature('')
+
+
+class CpuTemperatureGetThermalDeviceInformationTest(CpuTemperatureTest):
+
+  @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+  def testGetThermalDeviceInformation_noneWhenIncorrectLabel(self):
+    invalid_device = mock.Mock(spec=device_utils.DeviceUtils)
+    invalid_device.build_product = 'invalid_name'
+    c = cpu_temperature.CpuTemperature(invalid_device)
+    c.InitThermalDeviceInformation()
+    self.assertEqual(c.GetDeviceInfoForTesting(), None)
+
+  def testGetThermalDeviceInformation_getsCorrectInformation(self):
+    correct_information = {
+        'cpu0': '/sys/class/thermal/thermal_zone11/temp',
+        'cpu1': '/sys/class/thermal/thermal_zone12/temp',
+        'cpu2': '/sys/class/thermal/thermal_zone13/temp',
+        'cpu3': '/sys/class/thermal/thermal_zone14/temp',
+        'cpu4': '/sys/class/thermal/thermal_zone15/temp',
+        'cpu5': '/sys/class/thermal/thermal_zone16/temp',
+        'cpu6': '/sys/class/thermal/thermal_zone17/temp',
+        'cpu7': '/sys/class/thermal/thermal_zone18/temp'
+    }
+    self.assertEqual(
+        cmp(correct_information,
+            self.cpu_temp.GetDeviceInfoForTesting().get('cpu_temps')), 0)
+
+
+class CpuTemperatureIsSupportedTest(CpuTemperatureTest):
+
+  @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+  def testIsSupported_returnsTrue(self):
+    d = mock.Mock(spec=device_utils.DeviceUtils)
+    d.build_product = 'blueline'
+    d.FileExists.return_value = True
+    c = cpu_temperature.CpuTemperature(d)
+    self.assertTrue(c.IsSupported())
+
+  @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+  def testIsSupported_returnsFalse(self):
+    d = mock.Mock(spec=device_utils.DeviceUtils)
+    d.build_product = 'blueline'
+    d.FileExists.return_value = False
+    c = cpu_temperature.CpuTemperature(d)
+    self.assertFalse(c.IsSupported())
+
+
+class CpuTemperatureLetCpuCoolToTemperatureTest(CpuTemperatureTest):
+  # Return values for the mock side effect
+  cooling_down0 = ([45000 for _ in range(8)] + [43000 for _ in range(8)] +
+                   [41000 for _ in range(8)])
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testLetBatteryCoolToTemperature_coolWithin24Calls(self):
+    self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down0)
+    self.cpu_temp.LetCpuCoolToTemperature(42)
+    self.mock_device.ReadFile.assert_called()
+    self.assertEquals(self.mock_device.ReadFile.call_count, 24)
+
+  cooling_down1 = [45000 for _ in range(8)] + [41000 for _ in range(16)]
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testLetBatteryCoolToTemperature_coolWithin16Calls(self):
+    self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down1)
+    self.cpu_temp.LetCpuCoolToTemperature(42)
+    self.mock_device.ReadFile.assert_called()
+    self.assertEquals(self.mock_device.ReadFile.call_count, 16)
+
+  constant_temp = [45000 for _ in range(40)]
+
+  @mock.patch('time.sleep', mock.Mock())
+  def testLetBatteryCoolToTemperature_timeoutAfterThree(self):
+    self.mock_device.ReadFile = mock.Mock(side_effect=self.constant_temp)
+    self.cpu_temp.LetCpuCoolToTemperature(42)
+    self.mock_device.ReadFile.assert_called()
+    self.assertEquals(self.mock_device.ReadFile.call_count, 24)
+
+
+if __name__ == '__main__':
+  logging.getLogger().setLevel(logging.DEBUG)
+  unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/crash_handler.py b/catapult/devil/devil/android/crash_handler.py
index 7cfabcf..028e787 100644
--- a/catapult/devil/devil/android/crash_handler.py
+++ b/catapult/devil/devil/android/crash_handler.py
@@ -37,6 +37,9 @@
         raise
       try:
         logger.warning('Device is unreachable. Waiting for recovery...')
+        # Treat the device being unreachable as an unexpected reboot and clear
+        # any cached state.
+        device.ClearCache()
         device.WaitUntilFullyBooted()
       except base_error.BaseError:
         logger.exception('Device never recovered. X(')
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py
index 575865b..6182a52 100644
--- a/catapult/devil/devil/android/device_utils.py
+++ b/catapult/devil/devil/android/device_utils.py
@@ -10,6 +10,7 @@
 
 import calendar
 import collections
+import contextlib
 import fnmatch
 import json
 import logging
@@ -21,6 +22,7 @@
 import re
 import shutil
 import stat
+import sys
 import tempfile
 import time
 import threading
@@ -50,6 +52,12 @@
 
 from py_utils import tempfile_ext
 
+try:
+  from devil.utils import reset_usb
+except ImportError:
+  # Fail silently if we can't import reset_usb. We're likely on windows.
+  reset_usb = None
+
 logger = logging.getLogger(__name__)
 
 _DEFAULT_TIMEOUT = 30
@@ -212,7 +220,12 @@
     'taimen', # Pixel 2 XL
     'vega', # Lenovo Mirage Solo
     'walleye', # Pixel 2
+    'crosshatch', # Pixel 3 XL
+    'blueline', # Pixel 3
 ]
+_SPECIAL_ROOT_DEVICE_LIST += ['aosp_%s' % _d for _d in
+                              _SPECIAL_ROOT_DEVICE_LIST]
+
 _IMEI_RE = re.compile(r'  Device ID = (.+)$')
 # The following regex is used to match result parcels like:
 """
@@ -226,6 +239,25 @@
 _EBUSY_RE = re.compile(
     r'mkdir failed for ([^,]*), Device or resource busy')
 
+# http://bit.ly/2WLZhUF added a timeout to adb wait-for-device. We sometimes
+# want to wait longer than the implicit call within adb root allows.
+_WAIT_FOR_DEVICE_TIMEOUT_STR = 'timeout expired while waiting for device'
+
+_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE = re.compile(
+    r'Current WebView package.*:.*\(([a-z.]*),')
+_WEBVIEW_SYSUPDATE_NULL_PKG_RE = re.compile(
+    r'Current WebView package is null')
+_WEBVIEW_SYSUPDATE_FALLBACK_LOGIC_RE = re.compile(
+    r'Fallback logic enabled: (true|false)')
+_WEBVIEW_SYSUPDATE_PACKAGE_INSTALLED_RE = re.compile(
+    r'(?:Valid|Invalid) package\s+(\S+)\s+\(.*\),?\s+(.*)$')
+_WEBVIEW_SYSUPDATE_PACKAGE_NOT_INSTALLED_RE = re.compile(
+    r'(\S+)\s+(is NOT installed\.)')
+_WEBVIEW_SYSUPDATE_MIN_VERSION_CODE = re.compile(
+    r'Minimum WebView version code: (\d+)')
+
+_GOOGLE_FEATURES_RE = re.compile(r'^\s*com\.google\.')
+
 PS_COLUMNS = ('name', 'pid', 'ppid')
 ProcessInfo = collections.namedtuple('ProcessInfo', PS_COLUMNS)
 
@@ -369,7 +401,7 @@
     assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR)
     assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR)
 
-    self._ClearCache()
+    self.ClearCache()
 
   @property
   def serial(self):
@@ -453,6 +485,10 @@
       DeviceUnreachableError on missing device.
     """
     try:
+      if self.build_type == 'eng':
+        # 'eng' builds have root enabled by default and the adb session cannot
+        # be unrooted.
+        return True
       if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
         return self.GetProp('service.adb.root') == '1'
       self.RunShellCommand(['ls', '/root'], check_return=True)
@@ -516,17 +552,22 @@
 
     try:
       self.adb.Root()
-    except device_errors.AdbCommandFailedError:
+    except device_errors.AdbCommandFailedError as e:
       if self.IsUserBuild():
         raise device_errors.CommandFailedError(
             'Unable to root device with user build.', str(self))
+      elif e.output and _WAIT_FOR_DEVICE_TIMEOUT_STR in e.output:
+        # adb 1.0.41 added a call to wait-for-device *inside* root
+        # with a timeout that can be too short in some cases.
+        # If we hit that timeout, ignore it & do our own wait below.
+        pass
       else:
         raise  # Failed probably due to some other reason.
 
     def device_online_with_root():
       try:
         self.adb.WaitForDevice()
-        return self.GetProp('service.adb.root', cache=False) == '1'
+        return self.HasRoot()
       except (device_errors.AdbCommandFailedError,
               device_errors.DeviceUnreachableError):
         return False
@@ -841,29 +882,35 @@
       return not self.IsOnline()
 
     self.adb.Reboot()
-    self._ClearCache()
+    self.ClearCache()
     timeout_retry.WaitFor(device_offline, wait_period=1)
     if block:
       self.WaitUntilFullyBooted(wifi=wifi)
 
-  INSTALL_DEFAULT_TIMEOUT = 4 * _DEFAULT_TIMEOUT
+  INSTALL_DEFAULT_TIMEOUT = 8 * _DEFAULT_TIMEOUT
 
   @decorators.WithTimeoutAndRetriesFromInstance(
       min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
   def Install(self, apk, allow_downgrade=False, reinstall=False,
-              permissions=None, timeout=None, retries=None):
-    """Install an APK.
+              permissions=None, timeout=None, retries=None, modules=None):
+    """Install an APK or app bundle.
 
-    Noop if an identical APK is already installed.
+    Noop if an identical APK is already installed. If installing a bundle, the
+    bundletools helper script (bin/*_bundle) should be used rather than the .aab
+    file.
 
     Args:
-      apk: An ApkHelper instance or string containing the path to the APK.
+      apk: An ApkHelper instance or string containing the path to the APK or
+        bundle.
       allow_downgrade: A boolean indicating if we should allow downgrades.
       reinstall: A boolean indicating if we should keep any existing app data.
+        Ignored if |apk| is a bundle.
       permissions: Set of permissions to set. If not set, finds permissions with
           apk helper. To set no permissions, pass [].
       timeout: timeout in seconds
       retries: number of retries
+      modules: An iterable containing specific bundle modules to install.
+          Error if set and |apk| points to an APK instead of a bundle.
 
     Raises:
       CommandFailedError if the installation fails.
@@ -871,7 +918,8 @@
       DeviceUnreachableError on missing device.
     """
     self._InstallInternal(apk, None, allow_downgrade=allow_downgrade,
-                          reinstall=reinstall, permissions=permissions)
+                          reinstall=reinstall, permissions=permissions,
+                          modules=modules)
 
   @decorators.WithTimeoutAndRetriesFromInstance(
       min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
@@ -907,12 +955,29 @@
 
   def _InstallInternal(self, base_apk, split_apks, allow_downgrade=False,
                        reinstall=False, allow_cached_props=False,
-                       permissions=None):
+                       permissions=None, modules=None):
+    base_apk = apk_helper.ToHelper(base_apk)
+    if base_apk.is_bundle:
+      if split_apks:
+        raise device_errors.CommandFailedError(
+            'Attempted to install a bundle {} while specifying split apks'
+            .format(base_apk))
+      if allow_downgrade:
+        logging.warning('Installation of a bundle requested with '
+                        'allow_downgrade=False. This is not possible with '
+                        'bundletools, no downgrading is possible. This '
+                        'flag will be ignored and installation will proceed.')
+      # |allow_cached_props| is unused and ignored for bundles.
+      self._InstallBundleInternal(base_apk, permissions, modules)
+      return
+
+    if modules:
+      raise device_errors.CommandFailedError(
+          'Attempted to specify modules to install when providing an APK')
+
     if split_apks:
       self._CheckSdkLevel(version_codes.LOLLIPOP)
 
-    base_apk = apk_helper.ToHelper(base_apk)
-
     all_apks = [base_apk.path]
     if split_apks:
       all_apks += split_select.SelectSplits(
@@ -949,9 +1014,11 @@
         logger.warning('Error calculating md5: %s', e)
         apks_to_install, host_checksums = all_apks, None
       if apks_to_install and not reinstall:
-        self.Uninstall(package_name)
         apks_to_install = all_apks
 
+    if device_apk_paths and apks_to_install and not reinstall:
+      self.Uninstall(package_name)
+
     if apks_to_install:
       # Assume that we won't know the resulting device state.
       self._cache['package_apk_paths'].pop(package_name, 0)
@@ -977,6 +1044,20 @@
     if host_checksums is not None:
       self._cache['package_apk_checksums'][package_name] = host_checksums
 
+  def _InstallBundleInternal(self, bundle, permissions, modules):
+    cmd = [bundle.path, 'install', '--device', self.serial]
+    if modules:
+      for m in modules:
+        cmd.extend(['-m', m])
+    status = cmd_helper.RunCmd(cmd)
+    if status != 0:
+      raise device_errors.CommandFailedError('Cound not install {}'.format(
+          bundle.path))
+    if (permissions is None
+        and self.build_version_sdk >= version_codes.MARSHMALLOW):
+      permissions = bundle.GetPermissions()
+    self.GrantPermissions(bundle.GetPackageName(), permissions)
+
   @decorators.WithTimeoutAndRetriesFromInstance()
   def Uninstall(self, package_name, keep_data=False, timeout=None,
                 retries=None):
@@ -998,15 +1079,11 @@
     installed = self._GetApplicationPathsInternal(package_name)
     if not installed:
       return
-    try:
-      self.adb.Uninstall(package_name, keep_data)
-      self._cache['package_apk_paths'][package_name] = []
-      self._cache['package_apk_checksums'][package_name] = set()
-    except:
-      # Clear cache since we can't be sure of the state.
-      self._cache['package_apk_paths'].pop(package_name, 0)
-      self._cache['package_apk_checksums'].pop(package_name, 0)
-      raise
+    # cached package paths are indeterminate due to system apps taking over
+    # user apps after uninstall, so clear it
+    self._cache['package_apk_paths'].pop(package_name, 0)
+    self._cache['package_apk_checksums'].pop(package_name, 0)
+    self.adb.Uninstall(package_name, keep_data)
 
   def _CheckSdkLevel(self, required_sdk_level):
     """Raises an exception if the device does not have the required SDK level.
@@ -1020,8 +1097,8 @@
   @decorators.WithTimeoutAndRetriesFromInstance()
   def RunShellCommand(self, cmd, shell=False, check_return=False, cwd=None,
                       env=None, run_as=None, as_root=False, single_line=False,
-                      large_output=False, raw_output=False,
-                      ensure_logs_on_timeout=False, timeout=None, retries=None):
+                      large_output=False, raw_output=False, timeout=None,
+                      retries=None):
     """Run an ADB shell command.
 
     The command to run |cmd| should be a sequence of program arguments
@@ -1064,10 +1141,6 @@
         this large output will be truncated.
       raw_output: Whether to only return the raw output
           (no splitting into lines).
-      ensure_logs_on_timeout: If True, will use a slightly smaller timeout for
-          the internal adb command, which allows to retrive logs on timeout.
-          Note that that logs are not guaranteed to be produced with this option
-          as adb command may still hang and fail to respect the reduced timeout.
       timeout: timeout in seconds
       retries: number of retries
 
@@ -1091,7 +1164,7 @@
       return '%s=%s' % (key, cmd_helper.DoubleQuote(value))
 
     def run(cmd):
-      return self.adb.Shell(cmd, ensure_logs_on_timeout=ensure_logs_on_timeout)
+      return self.adb.Shell(cmd)
 
     def handle_check_return(cmd):
       try:
@@ -1115,11 +1188,16 @@
     def handle_large_output(cmd, large_output_mode):
       if large_output_mode:
         with device_temp_file.DeviceTempFile(self.adb) as large_output_file:
-          cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name)
+          large_output_cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name)
           logger.debug('Large output mode enabled. Will write output to '
                        'device and read results from file.')
-          handle_large_command(cmd)
-          return self.ReadFile(large_output_file.name, force_pull=True)
+          try:
+            handle_large_command(large_output_cmd)
+            return self.ReadFile(large_output_file.name, force_pull=True)
+          except device_errors.AdbShellCommandFailedError as exc:
+            output = self.ReadFile(large_output_file.name, force_pull=True)
+            raise device_errors.AdbShellCommandFailedError(
+                cmd, output, exc.status, exc.device_serial)
       else:
         try:
           return handle_large_command(cmd)
@@ -1869,8 +1947,29 @@
           device_path if not rename else [_RenamePath(p) for p in device_path])
     self.RunShellCommand(args, as_root=as_root, check_return=True)
 
+  @contextlib.contextmanager
+  def _CopyToReadableLocation(self, device_path):
+    """Context manager to copy a file to a globally readable temp file.
+
+    This uses root permission to copy a file to a globally readable named
+    temporary file. The temp file is removed when this contextmanager is closed.
+
+    Args:
+      device_path: A string containing the absolute path of the file (on the
+        device) to copy.
+    Yields:
+      The globally readable file object.
+    """
+    with device_temp_file.DeviceTempFile(self.adb) as device_temp:
+      cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % (
+          cmd_helper.SingleQuote(device_path),
+          cmd_helper.SingleQuote(device_temp.name))
+      self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True)
+      yield device_temp
+
   @decorators.WithTimeoutAndRetriesFromInstance()
-  def PullFile(self, device_path, host_path, timeout=None, retries=None):
+  def PullFile(self, device_path, host_path, as_root=False, timeout=None,
+               retries=None):
     """Pull a file from the device.
 
     Args:
@@ -1878,6 +1977,7 @@
                    from the device.
       host_path: A string containing the absolute path of the destination on
                  the host.
+      as_root: Whether root permissions should be used to pull the file.
       timeout: timeout in seconds
       retries: number of retries
 
@@ -1889,7 +1989,14 @@
     dirname = os.path.dirname(host_path)
     if dirname and not os.path.exists(dirname):
       os.makedirs(dirname)
-    self.adb.Pull(device_path, host_path)
+    if as_root and self.NeedsSU():
+      if not self.PathExists(device_path, as_root=True):
+        raise device_errors.CommandFailedError(
+            '%r: No such file or directory' % device_path, str(self))
+      with self._CopyToReadableLocation(device_path) as readable_temp_file:
+        self.adb.Pull(readable_temp_file.name, host_path)
+    else:
+      self.adb.Pull(device_path, host_path)
 
   def _ReadFileWithPull(self, device_path):
     try:
@@ -1936,12 +2043,8 @@
       return _JoinLines(self.RunShellCommand(
           ['cat', device_path], as_root=as_root, check_return=True))
     elif as_root and self.NeedsSU():
-      with device_temp_file.DeviceTempFile(self.adb) as device_temp:
-        cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % (
-            cmd_helper.SingleQuote(device_path),
-            cmd_helper.SingleQuote(device_temp.name))
-        self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True)
-        return self._ReadFileWithPull(device_temp.name)
+      with self._CopyToReadableLocation(device_path) as readable_temp_file:
+        return self._ReadFileWithPull(readable_temp_file.name)
     else:
       return self._ReadFileWithPull(device_path)
 
@@ -2216,20 +2319,42 @@
     else:
       return False
 
+  def GetLocale(self, cache=False):
+    """Returns the locale setting on the device.
+
+    Args:
+      cache: Whether to use cached properties when available.
+    Returns:
+      A pair (language, country).
+    """
+    locale = self.GetProp('persist.sys.locale', cache=cache)
+    if locale:
+      if '-' not in locale:
+        logging.error('Unparsable locale: %s', locale)
+        return ('', '')  # Behave as if persist.sys.locale is undefined.
+      return tuple(locale.split('-', 1))
+    return (self.GetProp('persist.sys.language', cache=cache),
+            self.GetProp('persist.sys.country', cache=cache))
+
   def GetLanguage(self, cache=False):
     """Returns the language setting on the device.
+
+    DEPRECATED: Prefer GetLocale() instead.
+
     Args:
       cache: Whether to use cached properties when available.
     """
-    return self.GetProp('persist.sys.language', cache=cache)
+    return self.GetLocale(cache=cache)[0]
 
   def GetCountry(self, cache=False):
     """Returns the country setting on the device.
 
+    DEPRECATED: Prefer GetLocale() instead.
+
     Args:
       cache: Whether to use cached properties when available.
     """
-    return self.GetProp('persist.sys.country', cache=cache)
+    return self.GetLocale(cache=cache)[1]
 
   @property
   def screen_density(self):
@@ -2302,7 +2427,11 @@
 
   @property
   def product_cpu_abi(self):
-    """Returns the product cpu abi of the device (e.g. 'armeabi-v7a')."""
+    """Returns the product cpu abi of the device (e.g. 'armeabi-v7a').
+
+    For supported ABIs, the return value will be one of the values defined in
+    devil.android.ndk.abis.
+    """
     return self.GetProp('ro.product.cpu.abi', cache=True)
 
   @property
@@ -2428,7 +2557,8 @@
       retries: number of retries
 
     Returns:
-      The device's main ABI name.
+      The device's main ABI name. For supported ABIs, the return value will be
+      one of the values defined in devil.android.ndk.abis.
 
     Raises:
       CommandTimeoutError on timeout.
@@ -2625,6 +2755,69 @@
         check_return=True)
 
   @decorators.WithTimeoutAndRetriesFromInstance()
+  def GetWebViewUpdateServiceDump(self, timeout=None, retries=None):
+    """Get the WebView update command sysdump on the device.
+
+    Returns:
+      A dictionary with these possible entries:
+        FallbackLogicEnabled: True|False
+        CurrentWebViewPackage: "package name" or None
+        MinimumWebViewVersionCode: int
+        WebViewPackages: Dict of installed WebView providers, mapping "package
+            name" to "reason it's valid/invalid."
+
+    It may return an empty dictionary if device does not
+    support the "dumpsys webviewupdate" command.
+
+    Raises:
+      CommandFailedError on failure.
+      CommandTimeoutError on timeout.
+      DeviceUnreachableError on missing device.
+    """
+    result = {}
+
+    # Command was implemented starting in Oreo
+    if self.build_version_sdk < version_codes.OREO:
+      return result
+
+    output = self.RunShellCommand(
+        ['dumpsys', 'webviewupdate'], check_return=True)
+    webview_packages = {}
+    for line in output:
+      match = re.search(_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE, line)
+      if match:
+        result['CurrentWebViewPackage'] = match.group(1)
+      match = re.search(_WEBVIEW_SYSUPDATE_NULL_PKG_RE, line)
+      if match:
+        result['CurrentWebViewPackage'] = None
+      match = re.search(_WEBVIEW_SYSUPDATE_FALLBACK_LOGIC_RE, line)
+      if match:
+        result['FallbackLogicEnabled'] = \
+            True if match.group(1) == 'true' else False
+      match = re.search(_WEBVIEW_SYSUPDATE_PACKAGE_INSTALLED_RE, line)
+      if match:
+        package_name = match.group(1)
+        reason = match.group(2)
+        webview_packages[package_name] = reason
+      match = re.search(_WEBVIEW_SYSUPDATE_PACKAGE_NOT_INSTALLED_RE, line)
+      if match:
+        package_name = match.group(1)
+        reason = match.group(2)
+        webview_packages[package_name] = reason
+      match = re.search(_WEBVIEW_SYSUPDATE_MIN_VERSION_CODE, line)
+      if match:
+        result['MinimumWebViewVersionCode'] = int(match.group(1))
+    if webview_packages:
+      result['WebViewPackages'] = webview_packages
+
+    missing_fields = set(['CurrentWebViewPackage', 'FallbackLogicEnabled']) - \
+                     set(result.keys())
+    if len(missing_fields) > 0:
+      raise device_errors.CommandFailedError(
+          '%s not found in dumpsys webviewupdate' % str(list(missing_fields)))
+    return result
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
   def SetWebViewImplementation(self, package_name, timeout=None, retries=None):
     """Select the WebView implementation to the specified package.
 
@@ -2639,16 +2832,104 @@
       CommandTimeoutError on timeout.
       DeviceUnreachableError on missing device.
     """
+    installed = self.GetApplicationPaths(package_name)
+    if not installed:
+      raise device_errors.CommandFailedError(
+          '%s is not installed' % package_name, str(self))
     output = self.RunShellCommand(
         ['cmd', 'webviewupdate', 'set-webview-implementation', package_name],
-        single_line=True, check_return=True)
+        single_line=True,
+        check_return=False)
     if output == 'Success':
       logging.info('WebView provider set to: %s', package_name)
     else:
+      dumpsys_output = self.GetWebViewUpdateServiceDump()
+      webview_packages = dumpsys_output.get('WebViewPackages')
+      if webview_packages:
+        reason = webview_packages.get(package_name)
+        if not reason:
+          all_provider_package_names = webview_packages.keys()
+          raise device_errors.CommandFailedError(
+              '%s is not in the system WebView provider list. Must choose one '
+              'of %r.' % (package_name, all_provider_package_names), str(self))
+        if re.search(r'is\s+NOT\s+installed/enabled for all users', reason):
+          raise device_errors.CommandFailedError(
+              '%s is disabled, make sure to disable WebView fallback logic' %
+              package_name, str(self))
+        if re.search(r'No WebView-library manifest flag', reason):
+          raise device_errors.CommandFailedError(
+              '%s does not declare a WebView native library, so it cannot '
+              'be a WebView provider' % package_name, str(self))
+        if re.search(r'SDK version too low', reason):
+          raise device_errors.CommandFailedError(
+              '%s needs a higher targetSdkVersion (must be >= %d)' %
+              (package_name, self.build_version_sdk), str(self))
+        if re.search(r'Version code too low', reason):
+          raise device_errors.CommandFailedError(
+              '%s needs a higher versionCode (must be >= %d)' %
+              (package_name, dumpsys_output.get('MinimumWebViewVersionCode')),
+              str(self))
+        if re.search(r'Incorrect signature', reason):
+          raise device_errors.CommandFailedError(
+              '%s is not signed with release keys (but user builds require '
+              'this for WebView providers)' % package_name, str(self))
       raise device_errors.CommandFailedError(
           'Error setting WebView provider: %s' % output, str(self))
 
   @decorators.WithTimeoutAndRetriesFromInstance()
+  def SetWebViewFallbackLogic(self, enabled, timeout=None, retries=None):
+    """Set whether WebViewUpdateService's "fallback logic" should be enabled.
+
+    WebViewUpdateService has nonintuitive "fallback logic" for devices where
+    Monochrome (Chrome Stable) is preinstalled as the WebView provider, with a
+    "stub" (little-to-no code) implementation of standalone WebView.
+
+    "Fallback logic" (enabled by default) is designed, in the case where the
+    user has disabled Chrome, to fall back to the stub standalone WebView by
+    enabling the package. The implementation plumbs through the Chrome APK until
+    Play Store installs an update with the full implementation.
+
+    A surprising side-effect of "fallback logic" is that, immediately after
+    sideloading WebView, WebViewUpdateService re-disables the package and
+    uninstalls the update. This can prevent successfully using standalone
+    WebView for development, although "fallback logic" can be disabled on
+    userdebug/eng devices.
+
+    Because this is only relevant for devices with the standalone WebView stub,
+    this command is only relevant on N-P (inclusive).
+
+    You can determine if "fallback logic" is currently enabled by checking
+    FallbackLogicEnabled in the dictionary returned by
+    GetWebViewUpdateServiceDump.
+
+    Args:
+      enabled: bool - True for enabled, False for disabled
+      timeout: timeout in seconds
+      retries: number of retries
+
+    Raises:
+      CommandFailedError on failure.
+      CommandTimeoutError on timeout.
+      DeviceUnreachableError on missing device.
+    """
+
+    # Command is only available on devices which preinstall stub WebView.
+    if not version_codes.NOUGAT <= self.build_version_sdk <= version_codes.PIE:
+      return
+
+    # redundant-packages is the opposite of fallback logic
+    enable_string = 'disable' if enabled else 'enable'
+    output = self.RunShellCommand(
+        ['cmd', 'webviewupdate', '%s-redundant-packages' % enable_string],
+        single_line=True, check_return=True)
+    if output == 'Success':
+      logging.info('WebView Fallback Logic is %s',
+                   'enabled' if enabled else 'disabled')
+    else:
+      raise device_errors.CommandFailedError(
+          'Error setting WebView Fallback Logic: %s' % output, str(self))
+
+  @decorators.WithTimeoutAndRetriesFromInstance()
   def TakeScreenshot(self, host_path=None, timeout=None, retries=None):
     """Takes a screenshot of the device.
 
@@ -2722,7 +3003,7 @@
       self._client_caches[client_name] = {}
     return self._client_caches[client_name]
 
-  def _ClearCache(self):
+  def ClearCache(self):
     """Clears all caches."""
     for client in self._client_caches:
       self._client_caches[client].clear()
@@ -2826,7 +3107,7 @@
 
   @classmethod
   def HealthyDevices(cls, blacklist=None, device_arg='default', retries=1,
-                     abis=None, **kwargs):
+                     enable_usb_resets=False, abis=None, **kwargs):
     """Returns a list of DeviceUtils instances.
 
     Returns a list of DeviceUtils instances that are attached, not blacklisted,
@@ -2851,8 +3132,11 @@
       retries: Number of times to restart adb server and query it again if no
           devices are found on the previous attempts, with exponential backoffs
           up to 60s between each retry.
+      enable_usb_resets: If true, will attempt to trigger a USB reset prior to
+          the last attempt if there are no available devices. It will only reset
+          those that appear to be android devices.
       abis: A list of ABIs for which the device needs to support at least one of
-          (optional).
+          (optional). See devil.android.ndk.abis for valid values.
       A device serial, or a list of device serials (optional).
 
     Returns:
@@ -2912,6 +3196,18 @@
         raise device_errors.MultipleDevicesError(devices)
       return sorted(devices)
 
+    def _reset_devices():
+      if not reset_usb:
+        logging.error(
+            'reset_usb.py not supported on this platform (%s). Skipping usb '
+            'resets.', sys.platform)
+        return
+      if device_arg:
+        for serial in device_arg:
+          reset_usb.reset_android_usb(serial)
+      else:
+        reset_usb.reset_all_android_devices()
+
     for attempt in xrange(retries+1):
       try:
         return _get_devices()
@@ -2919,6 +3215,11 @@
         if attempt == retries:
           logging.error('No devices found after exhausting all retries.')
           raise
+        elif attempt == retries - 1 and enable_usb_resets:
+          logging.warning(
+              'Attempting to reset relevant USB devices prior to the last '
+              'attempt.')
+          _reset_devices()
         # math.pow returns floats, so cast to int for easier testing
         sleep_s = min(int(math.pow(2, attempt + 1)), 60)
         logger.warning(
diff --git a/catapult/devil/devil/android/device_utils_test.py b/catapult/devil/devil/android/device_utils_test.py
index e0ed666..5799c7b 100755
--- a/catapult/devil/devil/android/device_utils_test.py
+++ b/catapult/devil/devil/android/device_utils_test.py
@@ -15,12 +15,14 @@
 import logging
 import os
 import stat
+import sys
 import unittest
 
 from devil import devil_env
 from devil.android import device_errors
 from devil.android import device_signal
 from devil.android import device_utils
+from devil.android.ndk import abis
 from devil.android.sdk import adb_wrapper
 from devil.android.sdk import intent
 from devil.android.sdk import keyevent
@@ -31,9 +33,6 @@
 with devil_env.SysPath(devil_env.PYMOCK_PATH):
   import mock  # pylint: disable=import-error
 
-ARM32_ABI = 'armeabi-v7a'
-ARM64_ABI = 'arm64-v8a'
-
 def Process(name, pid, ppid='1'):
   return device_utils.ProcessInfo(name=name, pid=pid, ppid=ppid)
 
@@ -57,9 +56,10 @@
 
   def __init__(self, path, package_name, perms=None):
     self.path = path
+    self.is_bundle = path.endswith('_bundle')
     self.package_name = package_name
     self.perms = perms
-    self.abis = [ARM32_ABI]
+    self.abis = [abis.ARM]
 
   def GetPackageName(self):
     return self.package_name
@@ -314,36 +314,62 @@
 class DeviceUtilsHasRootTest(DeviceUtilsTest):
 
   def testHasRoot_true(self):
-    with self.patch_call(self.call.device.product_name,
-                          return_value='notasailfish'), (
-        self.assertCall(self.call.adb.Shell(
-          'ls /root', ensure_logs_on_timeout=False), 'foo\n')):
+    with self.patch_call(self.call.device.build_type,
+                          return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='notasailfish')), (
+        self.assertCall(self.call.adb.Shell('ls /root'), 'foo\n')):
       self.assertTrue(self.device.HasRoot())
 
   def testhasRootSpecial_true(self):
-    with self.patch_call(self.call.device.product_name,
-                         return_value='sailfish'), (
-        self.assertCall(
-          self.call.adb.Shell('getprop service.adb.root',
-            ensure_logs_on_timeout=False), '1\n')):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='sailfish')), (
+        self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+                        '1\n')):
+      self.assertTrue(self.device.HasRoot())
+
+  def testhasRootSpecialAosp_true(self):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='aosp_sailfish')), (
+        self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+                        '1\n')):
+      self.assertTrue(self.device.HasRoot())
+
+  def testhasRootEngBuild_true(self):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='eng'):
       self.assertTrue(self.device.HasRoot())
 
   def testHasRoot_false(self):
-    with self.patch_call(self.call.device.product_name,
-                         return_value='notasailfish'), (
-        self.assertCall(
-          self.call.adb.Shell(
-            'ls /root', ensure_logs_on_timeout=False), self.ShellError())):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='notasailfish')), (
+        self.assertCall(self.call.adb.Shell('ls /root'),
+                        self.ShellError())):
       self.assertFalse(self.device.HasRoot())
 
   def testHasRootSpecial_false(self):
-    with self.patch_call(self.call.device.product_name,
-                         return_value='sailfish'), (
-        self.assertCall(
-          self.call.adb.Shell(
-            'getprop service.adb.root', ensure_logs_on_timeout=False), '\n')):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='sailfish')), (
+        self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+                        '\n')):
       self.assertFalse(self.device.HasRoot())
 
+  def testHasRootSpecialAosp_false(self):
+    with self.patch_call(self.call.device.build_type,
+                         return_value='userdebug'), (
+        self.patch_call(self.call.device.product_name,
+                        return_value='aosp_sailfish')), (
+        self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+                        '\n')):
+      self.assertFalse(self.device.HasRoot())
 
 class DeviceUtilsEnableRootTest(DeviceUtilsTest):
 
@@ -351,7 +377,7 @@
     with self.assertCalls(
         self.call.adb.Root(),
         self.call.adb.WaitForDevice(),
-        (self.call.device.GetProp('service.adb.root', cache=False), '1')):
+        (self.call.device.HasRoot(), True)):
       self.device.EnableRoot()
 
   def testEnableRoot_userBuild(self):
@@ -368,6 +394,16 @@
       with self.assertRaises(device_errors.AdbCommandFailedError):
         self.device.EnableRoot()
 
+  def testEnableRoot_timeoutInWaitForDevice(self):
+    with self.assertCalls(
+        (self.call.adb.Root(),
+         self.AdbCommandError(
+             output='timeout expired while waiting for device')),
+        (self.call.device.IsUserBuild(), False),
+        self.call.adb.WaitForDevice(),
+        (self.call.device.HasRoot(), True)):
+      self.device.EnableRoot()
+
 
 class DeviceUtilsIsUserBuildTest(DeviceUtilsTest):
 
@@ -449,8 +485,7 @@
 
   def test_GetApplicationVersion_exists(self):
     with self.assertCalls(
-        (self.call.adb.Shell(
-          'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('dumpsys package com.android.chrome'),
          'Packages:\n'
          '  Package [com.android.chrome] (3901ecfb):\n'
          '    userId=1234 gids=[123, 456, 789]\n'
@@ -461,16 +496,13 @@
 
   def test_GetApplicationVersion_notExists(self):
     with self.assertCalls(
-        (self.call.adb.Shell(
-          'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
-          '')):
+        (self.call.adb.Shell('dumpsys package com.android.chrome'), '')):
       self.assertEquals(None,
                         self.device.GetApplicationVersion('com.android.chrome'))
 
   def test_GetApplicationVersion_fails(self):
     with self.assertCalls(
-        (self.call.adb.Shell(
-          'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('dumpsys package com.android.chrome'),
          'Packages:\n'
          '  Package [com.android.chrome] (3901ecfb):\n'
          '    userId=1234 gids=[123, 456, 789]\n'
@@ -487,7 +519,7 @@
             'dumpsys package com.android.chrome | grep -F primaryCpuAbi'),
         ['  primaryCpuAbi=armeabi-v7a']):
       self.assertEquals(
-          ARM32_ABI,
+          abis.ARM,
           self.device.GetPackageArchitecture('com.android.chrome'))
 
   def test_GetPackageArchitecture_notExists(self):
@@ -528,8 +560,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -543,8 +574,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -552,8 +582,7 @@
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed', cache=False), '1'),
         # wifi_enabled
-        (self.call.adb.Shell(
-          'dumpsys wifi', ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('dumpsys wifi'),
          'stuff\nWi-Fi is enabled\nmore stuff\n')):
       self.device.WaitUntilFullyBooted(wifi=True)
 
@@ -570,8 +599,7 @@
         (self.call.device.GetExternalStoragePath(), self.AdbCommandError()),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -585,8 +613,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -611,18 +638,13 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False),
-          self.ShellError()),
+        (self.call.adb.Shell('test -d /fake/storage/path'), self.ShellError()),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False),
-          self.ShellError()),
+        (self.call.adb.Shell('test -d /fake/storage/path'), self.ShellError()),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('test -d /fake/storage/path'),
          self.TimeoutError())):
       with self.assertRaises(device_errors.CommandTimeoutError):
         self.device.WaitUntilFullyBooted(wifi=False)
@@ -632,8 +654,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -654,8 +675,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -675,8 +695,7 @@
         self.call.adb.WaitForDevice(),
         # sd_card_ready
         (self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
-        (self.call.adb.Shell(
-          'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('test -d /fake/storage/path'), ''),
         # pm_ready
         (self.call.device._GetApplicationPathsInternal('android',
                                                        skip_cache=True),
@@ -684,14 +703,11 @@
         # boot_completed
         (self.call.device.GetProp('sys.boot_completed', cache=False), '1'),
         # wifi_enabled
-        (self.call.adb.Shell(
-          'dumpsys wifi', ensure_logs_on_timeout=False), 'stuff\nmore stuff\n'),
+        (self.call.adb.Shell('dumpsys wifi'), 'stuff\nmore stuff\n'),
         # wifi_enabled
-        (self.call.adb.Shell(
-          'dumpsys wifi', ensure_logs_on_timeout=False), 'stuff\nmore stuff\n'),
+        (self.call.adb.Shell('dumpsys wifi'), 'stuff\nmore stuff\n'),
         # wifi_enabled
-        (self.call.adb.Shell(
-          'dumpsys wifi', ensure_logs_on_timeout=False), self.TimeoutError())):
+        (self.call.adb.Shell('dumpsys wifi'), self.TimeoutError())):
       with self.assertRaises(device_errors.CommandTimeoutError):
         self.device.WaitUntilFullyBooted(wifi=True)
 
@@ -766,6 +782,18 @@
       self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
                           permissions=['p1', 'p2'])
 
+  def testInstall_identicalPriorInstall(self):
+    with self.assertCalls(
+        (mock.call.os.path.exists('/fake/test/app.apk'), True),
+        (self.call.device._GetApplicationPathsInternal('test.package'),
+         ['/fake/data/app/test.package.apk']),
+        (self.call.device._ComputeStaleApks('test.package',
+            ['/fake/test/app.apk']),
+         ([], None)),
+        (self.call.device.ForceStop('test.package'))):
+      self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
+                          permissions=[])
+
   def testInstall_differentPriorInstall(self):
     with self.assertCalls(
         (mock.call.os.path.exists('/fake/test/app.apk'), True),
@@ -780,6 +808,18 @@
       self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
                           permissions=[])
 
+  def testInstall_differentPriorInstallSplitApk(self):
+    with self.assertCalls(
+        (mock.call.os.path.exists('/fake/test/app.apk'), True),
+        (self.call.device._GetApplicationPathsInternal('test.package'),
+         ['/fake/data/app/test.package.apk',
+          '/fake/data/app/test.package2.apk']),
+        self.call.device.Uninstall('test.package'),
+        self.call.adb.Install('/fake/test/app.apk', reinstall=False,
+                              allow_downgrade=False)):
+      self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
+                          permissions=[])
+
   def testInstall_differentPriorInstall_reinstall(self):
     with self.assertCalls(
         (mock.call.os.path.exists('/fake/test/app.apk'), True),
@@ -834,6 +874,11 @@
       self.device.Install(DeviceUtilsInstallTest.mock_apk,
           reinstall=True, retries=0, permissions=[], allow_downgrade=True)
 
+  def testInstall_modulesSpecified(self):
+    with self.assertRaises(device_errors.CommandFailedError):
+      self.device.Install(DeviceUtilsInstallTest.mock_apk,
+          modules=['base'])
+
 
 class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
 
@@ -916,6 +961,61 @@
             ['split1.apk', 'split2.apk', 'split3.apk'], permissions=[],
             retries=0)
 
+  def testInstallSplitApk_previouslyNonSplit(self):
+    with self.assertCalls(
+        (self.call.device._CheckSdkLevel(21)),
+        (mock.call.devil.android.sdk.split_select.SelectSplits(
+            self.device, 'base.apk',
+            ['split1.apk', 'split2.apk', 'split3.apk'],
+            allow_cached_props=False),
+         ['split2.apk']),
+        (mock.call.os.path.exists('base.apk'), True),
+        (mock.call.os.path.exists('split2.apk'), True),
+        (self.call.device._GetApplicationPathsInternal(
+            'test.package'), ['/fake/data/app/test.package.apk']),
+        self.call.device.Uninstall('test.package'),
+        (self.call.adb.InstallMultiple(
+            ['base.apk', 'split2.apk'], partial=None, reinstall=False,
+            allow_downgrade=False))):
+      self.device.InstallSplitApk(DeviceUtilsInstallSplitApkTest.mock_apk,
+          ['split1.apk', 'split2.apk', 'split3.apk'], permissions=[], retries=0)
+
+
+class DeviceUtilsInstallBundleTest(DeviceUtilsTest):
+  mock_apk = _MockApkHelper('/fake/test/app_bundle', 'test.package', ['p1'])
+
+  def testInstallBundle_noPriorInstall(self):
+    with self.patch_call(self.call.device.build_version_sdk, return_value=23):
+      with self.assertCalls(
+          (mock.call.devil.utils.cmd_helper.RunCmd(
+              ['/fake/test/app_bundle', 'install', '--device',
+                  self.device.serial]), 0),
+          (self.call.device.GrantPermissions('test.package', ['p1']), [])):
+        self.device.Install(DeviceUtilsInstallBundleTest.mock_apk)
+
+  def testInstallBundle_modulesSpecified(self):
+    with self.patch_call(self.call.device.build_version_sdk, return_value=23):
+      with self.assertCalls(
+          (mock.call.devil.utils.cmd_helper.RunCmd(
+              ['/fake/test/app_bundle', 'install', '--device',
+                  self.device.serial, '-m', 'base']), 0),
+          (self.call.device.GrantPermissions('test.package', ['p1']), [])):
+        self.device.Install(
+            DeviceUtilsInstallBundleTest.mock_apk, modules=['base'])
+
+  def testInstallBundle_permissionsPreM(self):
+    with self.patch_call(self.call.device.build_version_sdk, return_value=20):
+      with self.assertCalls(
+          (mock.call.devil.utils.cmd_helper.RunCmd(
+              ['/fake/test/app_bundle', 'install', '--device',
+                  self.device.serial]), 0)):
+        self.device.Install(DeviceUtilsInstallBundleTest.mock_apk)
+
+  def testInstallBundle_splitApks(self):
+    with self.assertRaises(device_errors.CommandFailedError):
+      self.device.InstallSplitApk(
+          DeviceUtilsInstallBundleTest.mock_apk, ['apk1', 'apk2'])
+
 
 class DeviceUtilsUninstallTest(DeviceUtilsTest):
 
@@ -954,36 +1054,30 @@
     self.device.NeedsSU = mock.Mock(return_value=False)
 
   def testRunShellCommand_commandAsList(self):
-    with self.assertCall(self.call.adb.Shell(
-      'pm list packages', ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell('pm list packages'), ''):
       self.device.RunShellCommand(
           ['pm', 'list', 'packages'], check_return=True)
 
   def testRunShellCommand_commandAsListQuoted(self):
-    with self.assertCall(self.call.adb.Shell(
-      "echo 'hello world' '$10'", ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell("echo 'hello world' '$10'"), ''):
       self.device.RunShellCommand(
           ['echo', 'hello world', '$10'], check_return=True)
 
   def testRunShellCommand_commandAsString(self):
-    with self.assertCall(self.call.adb.Shell(
-      'echo "$VAR"', ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell('echo "$VAR"'), ''):
       self.device.RunShellCommand(
           'echo "$VAR"', shell=True, check_return=True)
 
   def testNewRunShellImpl_withEnv(self):
     with self.assertCall(
-        self.call.adb.Shell(
-          'VAR=some_string echo "$VAR"', ensure_logs_on_timeout=False), ''):
+        self.call.adb.Shell('VAR=some_string echo "$VAR"'), ''):
       self.device.RunShellCommand(
           'echo "$VAR"', shell=True, check_return=True,
           env={'VAR': 'some_string'})
 
   def testNewRunShellImpl_withEnvQuoted(self):
     with self.assertCall(
-        self.call.adb.Shell(
-          'PATH="$PATH:/other/path" run_this', ensure_logs_on_timeout=False),
-        ''):
+        self.call.adb.Shell('PATH="$PATH:/other/path" run_this'), ''):
       self.device.RunShellCommand(
           ['run_this'], check_return=True, env={'PATH': '$PATH:/other/path'})
 
@@ -993,17 +1087,13 @@
           ['some_cmd'], check_return=True, env={'INVALID NAME': 'value'})
 
   def testNewRunShellImpl_withCwd(self):
-    with self.assertCall(self.call.adb.Shell(
-      'cd /some/test/path && ls', ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell('cd /some/test/path && ls'), ''):
       self.device.RunShellCommand(
           ['ls'], check_return=True, cwd='/some/test/path')
 
   def testNewRunShellImpl_withCwdQuoted(self):
     with self.assertCall(
-        self.call.adb.Shell(
-          "cd '/some test/path with/spaces' && ls",
-          ensure_logs_on_timeout=False),
-        ''):
+        self.call.adb.Shell("cd '/some test/path with/spaces' && ls"), ''):
       self.device.RunShellCommand(
           ['ls'], check_return=True, cwd='/some test/path with/spaces')
 
@@ -1014,9 +1104,7 @@
       (mock.call.devil.android.device_temp_file.DeviceTempFile(
           self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
       self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
-      (self.call.adb.Shell(
-        'sh /sdcard/temp-123.sh', ensure_logs_on_timeout=False),
-       payload + '\n')):
+      (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
       self.assertEquals(
           [payload],
           self.device.RunShellCommand(['echo', payload], check_return=True))
@@ -1031,9 +1119,7 @@
       (mock.call.devil.android.device_temp_file.DeviceTempFile(
           self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
       self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
-      (self.call.adb.Shell(
-        'sh /sdcard/temp-123.sh', ensure_logs_on_timeout=False),
-       payload + '\n')):
+      (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
       self.assertEquals(
           [payload],
           self.device.RunShellCommand(
@@ -1045,8 +1131,7 @@
     with self.assertCalls(
         (self.call.device.NeedsSU(), True),
         (self.call.device._Su(expected_cmd_without_su), expected_cmd),
-        (self.call.adb.Shell(
-          expected_cmd, ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell(expected_cmd), '')):
       self.device.RunShellCommand(
           ['setprop', 'service.adb.root', '0'],
           check_return=True, as_root=True)
@@ -1055,8 +1140,7 @@
     expected_cmd_without_run_as = "sh -c 'mkdir -p files'"
     expected_cmd = (
         'run-as org.devil.test_package %s' % expected_cmd_without_run_as)
-    with self.assertCall(self.call.adb.Shell(
-      expected_cmd, ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell(expected_cmd), ''):
       self.device.RunShellCommand(
           ['mkdir', '-p', 'files'],
           check_return=True, run_as='org.devil.test_package')
@@ -1071,8 +1155,7 @@
     with self.assertCalls(
         (self.call.device.NeedsSU(), True),
         (self.call.device._Su(expected_cmd_without_su), expected_cmd),
-        (self.call.adb.Shell(
-          expected_cmd, ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell(expected_cmd), '')):
       self.device.RunShellCommand(
           ['mkdir', '-p', 'files'],
           check_return=True, run_as='org.devil.test_package',
@@ -1080,16 +1163,14 @@
 
   def testRunShellCommand_manyLines(self):
     cmd = 'ls /some/path'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), 'file1\nfile2\nfile3\n'):
+    with self.assertCall(self.call.adb.Shell(cmd), 'file1\nfile2\nfile3\n'):
       self.assertEquals(
           ['file1', 'file2', 'file3'],
           self.device.RunShellCommand(cmd.split(), check_return=True))
 
   def testRunShellCommand_manyLinesRawOutput(self):
     cmd = 'ls /some/path'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), '\rfile1\nfile2\r\nfile3\n'):
+    with self.assertCall(self.call.adb.Shell(cmd), '\rfile1\nfile2\r\nfile3\n'):
       self.assertEquals(
           '\rfile1\nfile2\r\nfile3\n',
           self.device.RunShellCommand(
@@ -1097,8 +1178,7 @@
 
   def testRunShellCommand_singleLine_success(self):
     cmd = 'echo $VALUE'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), 'some value\n'):
+    with self.assertCall(self.call.adb.Shell(cmd), 'some value\n'):
       self.assertEquals(
           'some value',
           self.device.RunShellCommand(
@@ -1106,8 +1186,7 @@
 
   def testRunShellCommand_singleLine_successEmptyLine(self):
     cmd = 'echo $VALUE'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), '\n'):
+    with self.assertCall(self.call.adb.Shell(cmd), '\n'):
       self.assertEquals(
           '',
           self.device.RunShellCommand(
@@ -1115,8 +1194,7 @@
 
   def testRunShellCommand_singleLine_successWithoutEndLine(self):
     cmd = 'echo -n $VALUE'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), 'some value'):
+    with self.assertCall(self.call.adb.Shell(cmd), 'some value'):
       self.assertEquals(
           'some value',
           self.device.RunShellCommand(
@@ -1124,8 +1202,7 @@
 
   def testRunShellCommand_singleLine_successNoOutput(self):
     cmd = 'echo -n $VALUE'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell(cmd), ''):
       self.assertEquals(
           '',
           self.device.RunShellCommand(
@@ -1133,8 +1210,7 @@
 
   def testRunShellCommand_singleLine_failTooManyLines(self):
     cmd = 'echo $VALUE'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False),
+    with self.assertCall(self.call.adb.Shell(cmd),
                          'some value\nanother value\n'):
       with self.assertRaises(device_errors.CommandFailedError):
         self.device.RunShellCommand(
@@ -1143,8 +1219,7 @@
   def testRunShellCommand_checkReturn_success(self):
     cmd = 'echo $ANDROID_DATA'
     output = '/data\n'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), output):
+    with self.assertCall(self.call.adb.Shell(cmd), output):
       self.assertEquals(
           [output.rstrip()],
           self.device.RunShellCommand(cmd, shell=True, check_return=True))
@@ -1152,16 +1227,14 @@
   def testRunShellCommand_checkReturn_failure(self):
     cmd = 'ls /root'
     output = 'opendir failed, Permission denied\n'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), self.ShellError(output)):
+    with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
       with self.assertRaises(device_errors.AdbCommandFailedError):
         self.device.RunShellCommand(cmd.split(), check_return=True)
 
   def testRunShellCommand_checkReturn_disabled(self):
     cmd = 'ls /root'
     output = 'opendir failed, Permission denied\n'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), self.ShellError(output)):
+    with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
       self.assertEquals(
           [output.rstrip()],
           self.device.RunShellCommand(cmd.split(), check_return=False))
@@ -1173,7 +1246,7 @@
     with self.assertCalls(
         (mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
             temp_file),
-        (self.call.adb.Shell(cmd_redirect, ensure_logs_on_timeout=False)),
+        (self.call.adb.Shell(cmd_redirect)),
         (self.call.device.ReadFile(temp_file.name, force_pull=True),
          'something')):
       self.assertEquals(
@@ -1183,8 +1256,7 @@
 
   def testRunShellCommand_largeOutput_disabledNoTrigger(self):
     cmd = 'something'
-    with self.assertCall(self.call.adb.Shell(
-      cmd, ensure_logs_on_timeout=False), self.ShellError('')):
+    with self.assertCall(self.call.adb.Shell(cmd), self.ShellError('')):
       with self.assertRaises(device_errors.AdbCommandFailedError):
         self.device.RunShellCommand([cmd], check_return=True)
 
@@ -1193,12 +1265,10 @@
     temp_file = MockTempFile('/sdcard/temp-123')
     cmd_redirect = '( %s )>%s 2>&1' % (cmd, temp_file.name)
     with self.assertCalls(
-        (self.call.adb.Shell(
-          cmd, ensure_logs_on_timeout=False), self.ShellError('', None)),
+        (self.call.adb.Shell(cmd), self.ShellError('', None)),
         (mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
             temp_file),
-        (self.call.adb.Shell(
-          cmd_redirect, ensure_logs_on_timeout=False)),
+        (self.call.adb.Shell(cmd_redirect)),
         (self.call.device.ReadFile(mock.ANY, force_pull=True),
          'something')):
       self.assertEquals(
@@ -1264,8 +1334,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process.thing', 5678))),
-        (self.call.adb.Shell(
-          'kill -9 1234 5678', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('kill -9 1234 5678'), '')):
       self.assertEquals(
           2, self.device.KillAll('some.process', blocking=False))
 
@@ -1273,8 +1342,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process.thing', 5678))),
-        (self.call.adb.Shell(
-          'kill -9 1234 5678', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('kill -9 1234 5678'), ''),
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process.thing', 5678))),
         (self.call.device.ListProcesses('some.process'),
@@ -1287,8 +1355,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process.thing', 5678))),
-        (self.call.adb.Shell(
-          'kill -9 1234', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('kill -9 1234'), '')):
       self.assertEquals(
           1, self.device.KillAll('some.process', exact=True, blocking=False))
 
@@ -1296,8 +1363,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process.thing', 5678))),
-        (self.call.adb.Shell(
-          'kill -9 1234', ensure_logs_on_timeout=False), ''),
+        (self.call.adb.Shell('kill -9 1234'), ''),
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process.thing', 5678))),
         (self.call.device.ListProcesses('some.process'),
@@ -1312,8 +1378,7 @@
         (self.call.device.NeedsSU(), True),
         (self.call.device._Su("sh -c 'kill -9 1234'"),
          "su -c sh -c 'kill -9 1234'"),
-        (self.call.adb.Shell(
-          "su -c sh -c 'kill -9 1234'", ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell("su -c sh -c 'kill -9 1234'"), '')):
       self.assertEquals(
           1, self.device.KillAll('some.process', as_root=True))
 
@@ -1321,8 +1386,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234))),
-        (self.call.adb.Shell(
-          'kill -15 1234', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('kill -15 1234'), '')):
       self.assertEquals(
           1, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
 
@@ -1330,8 +1394,7 @@
     with self.assertCalls(
         (self.call.device.ListProcesses('some.process'),
          Processes(('some.process', 1234), ('some.process', 4567))),
-        (self.call.adb.Shell(
-          'kill -15 1234 4567', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('kill -15 1234 4567'), '')):
       self.assertEquals(
           2, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
 
@@ -1341,10 +1404,8 @@
   def testStartActivity_actionOnly(self):
     test_intent = intent.Intent(action='android.intent.action.VIEW')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1353,11 +1414,9 @@
                                 package='test.package',
                                 activity='.Main')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1366,11 +1425,9 @@
                                 package='test.package',
                                 activity='.Main')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main'),
         'Error: Failed to start test activity'):
       with self.assertRaises(device_errors.CommandFailedError):
         self.device.StartActivity(test_intent)
@@ -1380,12 +1437,10 @@
                                 package='test.package',
                                 activity='.Main')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-W '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-W '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent, blocking=True)
 
@@ -1395,12 +1450,10 @@
                                 activity='.Main',
                                 category='android.intent.category.HOME')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-c android.intent.category.HOME '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-c android.intent.category.HOME '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1411,13 +1464,11 @@
                                 category=['android.intent.category.HOME',
                                           'android.intent.category.BROWSABLE'])
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-c android.intent.category.HOME '
-          '-c android.intent.category.BROWSABLE '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-c android.intent.category.HOME '
+                            '-c android.intent.category.BROWSABLE '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1427,12 +1478,10 @@
                                 activity='.Main',
                                 data='http://www.google.com/')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-d http://www.google.com/ '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-d http://www.google.com/ '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1442,12 +1491,10 @@
                                 activity='.Main',
                                 extras={'foo': 'test'})
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main '
-          '--es foo test',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main '
+                            '--es foo test'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1457,12 +1504,10 @@
                                 activity='.Main',
                                 extras={'foo': True})
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main '
-          '--ez foo True',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main '
+                            '--ez foo True'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1472,12 +1517,10 @@
                                 activity='.Main',
                                 extras={'foo': 123})
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main '
-          '--ei foo 123',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main '
+                            '--ei foo 123'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1486,12 +1529,10 @@
                                 package='test.package',
                                 activity='.Main')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '--start-profiler test_trace_file.out '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '--start-profiler test_trace_file.out '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent,
                                 trace_file_name='test_trace_file.out')
@@ -1501,12 +1542,10 @@
                                 package='test.package',
                                 activity='.Main')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-S '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-S '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent, force_stop=True)
 
@@ -1519,12 +1558,10 @@
                                   intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                                 ])
     with self.assertCall(
-        self.call.adb.Shell(
-          'am start '
-          '-a android.intent.action.VIEW '
-          '-n test.package/.Main '
-          '-f 0x10200000',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am start '
+                            '-a android.intent.action.VIEW '
+                            '-n test.package/.Main '
+                            '-f 0x10200000'),
         'Starting: Intent { act=android.intent.action.VIEW }'):
       self.device.StartActivity(test_intent)
 
@@ -1537,11 +1574,9 @@
     with self.patch_call(self.call.device.build_version_sdk,
                          return_value=version_codes.NOUGAT):
       with self.assertCall(
-          self.call.adb.Shell(
-            'am startservice '
-            '-a android.intent.action.START '
-            '-n test.package/.Main',
-            ensure_logs_on_timeout=False),
+          self.call.adb.Shell('am startservice '
+                              '-a android.intent.action.START '
+                              '-n test.package/.Main'),
           'Starting service: Intent { act=android.intent.action.START }'):
         self.device.StartService(test_intent)
 
@@ -1552,11 +1587,9 @@
     with self.patch_call(self.call.device.build_version_sdk,
                          return_value=version_codes.NOUGAT):
       with self.assertCall(
-          self.call.adb.Shell(
-            'am startservice '
-            '-a android.intent.action.START '
-            '-n test.package/.Main',
-            ensure_logs_on_timeout=False),
+          self.call.adb.Shell('am startservice '
+                              '-a android.intent.action.START '
+                              '-n test.package/.Main'),
           'Error: Failed to start test service'):
         with self.assertRaises(device_errors.CommandFailedError):
           self.device.StartService(test_intent)
@@ -1568,12 +1601,10 @@
     with self.patch_call(self.call.device.build_version_sdk,
                          return_value=version_codes.NOUGAT):
       with self.assertCall(
-          self.call.adb.Shell(
-            'am startservice '
-            '--user TestUser '
-            '-a android.intent.action.START '
-            '-n test.package/.Main',
-            ensure_logs_on_timeout=False),
+          self.call.adb.Shell('am startservice '
+                              '--user TestUser '
+                              '-a android.intent.action.START '
+                              '-n test.package/.Main'),
           'Starting service: Intent { act=android.intent.action.START }'):
         self.device.StartService(test_intent, user_id='TestUser')
 
@@ -1584,11 +1615,9 @@
     with self.patch_call(self.call.device.build_version_sdk,
                          return_value=version_codes.OREO):
       with self.assertCall(
-          self.call.adb.Shell(
-            'am start-service '
-            '-a android.intent.action.START '
-            '-n test.package/.Main',
-            ensure_logs_on_timeout=False),
+          self.call.adb.Shell('am start-service '
+                              '-a android.intent.action.START '
+                              '-n test.package/.Main'),
           'Starting service: Intent { act=android.intent.action.START }'):
         self.device.StartService(test_intent)
 
@@ -1641,9 +1670,7 @@
   def testBroadcastIntent_noExtras(self):
     test_intent = intent.Intent(action='test.package.with.an.INTENT')
     with self.assertCall(
-        self.call.adb.Shell(
-          'am broadcast -a test.package.with.an.INTENT',
-          ensure_logs_on_timeout=False),
+        self.call.adb.Shell('am broadcast -a test.package.with.an.INTENT'),
         'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
       self.device.BroadcastIntent(test_intent)
 
@@ -1652,8 +1679,7 @@
                                 extras={'foo': 'bar value'})
     with self.assertCall(
         self.call.adb.Shell(
-            "am broadcast -a test.package.with.an.INTENT --es foo 'bar value'",
-            ensure_logs_on_timeout=False),
+            "am broadcast -a test.package.with.an.INTENT --es foo 'bar value'"),
         'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
       self.device.BroadcastIntent(test_intent)
 
@@ -1662,8 +1688,7 @@
                                 extras={'foo': None})
     with self.assertCall(
         self.call.adb.Shell(
-            'am broadcast -a test.package.with.an.INTENT --esn foo',
-            ensure_logs_on_timeout=False),
+            'am broadcast -a test.package.with.an.INTENT --esn foo'),
         'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
       self.device.BroadcastIntent(test_intent)
 
@@ -1827,8 +1852,7 @@
 class DeviceUtilsSendKeyEventTest(DeviceUtilsTest):
 
   def testSendKeyEvent(self):
-    with self.assertCall(self.call.adb.Shell(
-      'input keyevent 66', ensure_logs_on_timeout=False), ''):
+    with self.assertCall(self.call.adb.Shell('input keyevent 66'), ''):
       self.device.SendKeyEvent(66)
 
 
@@ -2003,6 +2027,33 @@
           self.device.PullFile('/data/app/test.file.does.not.exist',
                                '/test/file/host/path')
 
+  def testPullFile_asRoot(self):
+    with mock.patch('os.path.exists', return_value=True):
+      with self.assertCalls(
+          (self.call.device.NeedsSU(), True),
+          (self.call.device.PathExists('/this/file/can.be.read.with.su',
+                                       as_root=True), True),
+          (mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
+           MockTempFile('/sdcard/tmp/on.device')),
+          self.call.device.RunShellCommand(
+              'SRC=/this/file/can.be.read.with.su DEST=/sdcard/tmp/on.device;'
+              'cp "$SRC" "$DEST" && chmod 666 "$DEST"',
+              shell=True, as_root=True, check_return=True),
+          (self.call.adb.Pull('/sdcard/tmp/on.device',
+                              '/test/file/host/path'))):
+        self.device.PullFile('/this/file/can.be.read.with.su',
+                             '/test/file/host/path', as_root=True)
+
+  def testPullFile_asRootDoesntExistOnDevice(self):
+    with mock.patch('os.path.exists', return_value=True):
+      with self.assertCalls(
+          (self.call.device.NeedsSU(), True),
+          (self.call.device.PathExists('/data/app/test.file.does.not.exist',
+                                       as_root=True), False)):
+        with self.assertRaises(device_errors.CommandFailedError):
+          self.device.PullFile('/data/app/test.file.does.not.exist',
+                               '/test/file/host/path', as_root=True)
+
 
 class DeviceUtilsReadFileTest(DeviceUtilsTest):
 
@@ -2167,14 +2218,12 @@
 
   def testWriteFile_withEcho(self):
     with self.assertCall(self.call.adb.Shell(
-        "echo -n the.contents > /test/file/to.write",
-        ensure_logs_on_timeout=False), ''):
+        "echo -n the.contents > /test/file/to.write"), ''):
       self.device.WriteFile('/test/file/to.write', 'the.contents')
 
   def testWriteFile_withEchoAndQuotes(self):
     with self.assertCall(self.call.adb.Shell(
-        "echo -n 'the contents' > '/test/file/to write'",
-        ensure_logs_on_timeout=False), ''):
+        "echo -n 'the contents' > '/test/file/to write'"), ''):
       self.device.WriteFile('/test/file/to write', 'the contents')
 
   def testWriteFile_withEchoAndSU(self):
@@ -2183,8 +2232,7 @@
     with self.assertCalls(
         (self.call.device.NeedsSU(), True),
         (self.call.device._Su(expected_cmd_without_su), expected_cmd),
-        (self.call.adb.Shell(
-          expected_cmd, ensure_logs_on_timeout=False),
+        (self.call.adb.Shell(expected_cmd),
          '')):
       self.device.WriteFile('/test/file', 'contents', as_root=True)
 
@@ -2665,78 +2713,252 @@
 class DeviceUtilsGetSetEnforce(DeviceUtilsTest):
 
   def testGetEnforce_Enforcing(self):
-    with self.assertCall(self.call.adb.Shell(
-      'getenforce', ensure_logs_on_timeout=False), 'Enforcing'):
+    with self.assertCall(self.call.adb.Shell('getenforce'), 'Enforcing'):
       self.assertEqual(True, self.device.GetEnforce())
 
   def testGetEnforce_Permissive(self):
-    with self.assertCall(self.call.adb.Shell(
-      'getenforce', ensure_logs_on_timeout=False), 'Permissive'):
+    with self.assertCall(self.call.adb.Shell('getenforce'), 'Permissive'):
       self.assertEqual(False, self.device.GetEnforce())
 
   def testGetEnforce_Disabled(self):
-    with self.assertCall(self.call.adb.Shell(
-      'getenforce', ensure_logs_on_timeout=False), 'Disabled'):
+    with self.assertCall(self.call.adb.Shell('getenforce'), 'Disabled'):
       self.assertEqual(None, self.device.GetEnforce())
 
   def testSetEnforce_Enforcing(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 1', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 1'), '')):
       self.device.SetEnforce(enabled=True)
 
   def testSetEnforce_Permissive(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 0', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 0'), '')):
       self.device.SetEnforce(enabled=False)
 
   def testSetEnforce_EnforcingWithInt(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 1', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 1'), '')):
       self.device.SetEnforce(enabled=1)
 
   def testSetEnforce_PermissiveWithInt(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 0', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 0'), '')):
       self.device.SetEnforce(enabled=0)
 
   def testSetEnforce_EnforcingWithStr(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 1', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 1'), '')):
       self.device.SetEnforce(enabled='1')
 
   def testSetEnforce_PermissiveWithStr(self):
     with self.assertCalls(
         (self.call.device.NeedsSU(), False),
-        (self.call.adb.Shell(
-          'setenforce 0', ensure_logs_on_timeout=False), '')):
+        (self.call.adb.Shell('setenforce 0'), '')):
       self.device.SetEnforce(enabled='0')  # Not recommended but it works!
 
 
+class DeviceUtilsGetWebViewUpdateServiceDumpTest(DeviceUtilsTest):
+
+  def testGetWebViewUpdateServiceDump_success(self):
+    # Some of the lines of adb shell dumpsys webviewupdate:
+    dumpsys_lines = [
+        'Fallback logic enabled: true',
+        ('Current WebView package (name, version): '
+         '(com.android.chrome, 61.0.3163.98)'),
+        'Minimum WebView version code: 12345',
+        'WebView packages:',
+        ('Valid package com.android.chrome (versionName: '
+         '61.0.3163.98, versionCode: 1, targetSdkVersion: 26) is  '
+         'installed/enabled for all users'),
+        ('Valid package com.google.android.webview (versionName: '
+         '58.0.3029.125, versionCode: 1, targetSdkVersion: 26) is NOT '
+         'installed/enabled for all users'),
+        ('Invalid package com.google.android.apps.chrome (versionName: '
+         '56.0.2924.122, versionCode: 2, targetSdkVersion: 25), reason: SDK '
+         'version too low'),
+        ('com.chrome.canary is NOT installed.'),
+    ]
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.OREO):
+      with self.assertCall(
+          self.call.adb.Shell('dumpsys webviewupdate'),
+          '\n'.join(dumpsys_lines)):
+        update = self.device.GetWebViewUpdateServiceDump()
+        self.assertTrue(update['FallbackLogicEnabled'])
+        self.assertEqual('com.android.chrome',
+                         update['CurrentWebViewPackage'])
+        self.assertEqual(12345, update['MinimumWebViewVersionCode'])
+        # Order isn't really important, and we shouldn't have duplicates, so we
+        # convert to sets.
+        expected = {
+            'com.android.chrome', 'com.google.android.webview',
+            'com.google.android.apps.chrome', 'com.chrome.canary'
+        }
+        self.assertSetEqual(expected, set(update['WebViewPackages'].keys()))
+        self.assertEquals(
+            'is  installed/enabled for all users',
+            update['WebViewPackages']['com.android.chrome'])
+        self.assertEquals(
+            'is NOT installed/enabled for all users',
+            update['WebViewPackages']['com.google.android.webview'])
+        self.assertEquals(
+            'reason: SDK version too low',
+            update['WebViewPackages']['com.google.android.apps.chrome'])
+        self.assertEquals(
+            'is NOT installed.',
+            update['WebViewPackages']['com.chrome.canary'])
+
+  def testGetWebViewUpdateServiceDump_missingkey(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.OREO):
+      with self.assertCall(self.call.adb.Shell('dumpsys webviewupdate'),
+                           'Fallback logic enabled: true'):
+        with self.assertRaises(device_errors.CommandFailedError):
+          self.device.GetWebViewUpdateServiceDump()
+
+  def testGetWebViewUpdateServiceDump_noop(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.NOUGAT_MR1):
+      with self.assertCalls():
+        self.device.GetWebViewUpdateServiceDump()
+
+  def testGetWebViewUpdateServiceDump_noPackage(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.OREO):
+      with self.assertCall(self.call.adb.Shell('dumpsys webviewupdate'),
+                           'Fallback logic enabled: true\n'
+                           'Current WebView package is null'):
+        update = self.device.GetWebViewUpdateServiceDump()
+        self.assertEqual(True, update['FallbackLogicEnabled'])
+        self.assertEqual(None, update['CurrentWebViewPackage'])
+
+
 class DeviceUtilsSetWebViewImplementationTest(DeviceUtilsTest):
 
   def testSetWebViewImplementation_success(self):
-    with self.assertCall(self.call.adb.Shell(
-        'cmd webviewupdate set-webview-implementation foo.org',
-        ensure_logs_on_timeout=False), 'Success'):
-      self.device.SetWebViewImplementation('foo.org')
-
-  def testSetWebViewImplementation_failure(self):
-    with self.assertCall(self.call.adb.Shell(
-        'cmd webviewupdate set-webview-implementation foo.org',
-        ensure_logs_on_timeout=False), 'Oops!'):
-      with self.assertRaises(device_errors.CommandFailedError):
+    with self.patch_call(
+        self.call.device.GetApplicationPaths, return_value=['/any/path']):
+      with self.assertCall(
+          self.call.adb.Shell(
+              'cmd webviewupdate set-webview-implementation foo.org'),
+          'Success'):
         self.device.SetWebViewImplementation('foo.org')
 
+  def testSetWebViewImplementation_uninstalled(self):
+    with self.patch_call(self.call.device.GetApplicationPaths, return_value=[]):
+      with self.assertRaises(device_errors.CommandFailedError) as cfe:
+        self.device.SetWebViewImplementation('foo.org')
+      self.assertIn('is not installed', cfe.exception.message)
+
+  def _testSetWebViewImplementationHelper(self, mock_dump_sys,
+                                          exception_message_substr):
+    with self.patch_call(
+        self.call.device.GetApplicationPaths, return_value=['/any/path']):
+      with self.assertCall(
+          self.call.adb.Shell(
+              'cmd webviewupdate set-webview-implementation foo.org'), 'Oops!'):
+        with self.patch_call(
+            self.call.device.GetWebViewUpdateServiceDump,
+            return_value=mock_dump_sys):
+          with self.assertRaises(device_errors.CommandFailedError) as cfe:
+            self.device.SetWebViewImplementation('foo.org')
+          self.assertIn(exception_message_substr, cfe.exception.message)
+
+  def testSetWebViewImplementation_notInProviderList(self):
+    mock_dump_sys = {
+        'WebViewPackages': {
+            'some.package': 'any reason',
+            'other.package': 'any reason',
+        }
+    }
+    self._testSetWebViewImplementationHelper(mock_dump_sys, 'provider list')
+
+  def testSetWebViewImplementation_notEnabled(self):
+    mock_dump_sys = {
+        'WebViewPackages': {
+            'foo.org': 'is NOT installed/enabled for all users',
+        }
+    }
+    self._testSetWebViewImplementationHelper(mock_dump_sys, 'is disabled')
+
+  def testSetWebViewImplementation_missingManifestTag(self):
+    mock_dump_sys = {
+        'WebViewPackages': {
+            'foo.org': 'No WebView-library manifest flag',
+        }
+    }
+    self._testSetWebViewImplementationHelper(mock_dump_sys,
+                                             'WebView native library')
+
+  def testSetWebViewImplementation_lowTargetSdkVersion(self):
+    mock_dump_sys = {'WebViewPackages': {'foo.org': 'SDK version too low',}}
+    with self.patch_call(self.call.device.build_version_sdk, return_value=26):
+      self._testSetWebViewImplementationHelper(mock_dump_sys,
+                                               'higher targetSdkVersion')
+
+  def testSetWebViewImplementation_lowVersionCode(self):
+    mock_dump_sys = {
+        'MinimumWebViewVersionCode': 12345,
+        'WebViewPackages': {
+            'foo.org': 'Version code too low',
+        }
+    }
+    self._testSetWebViewImplementationHelper(mock_dump_sys,
+                                             'higher versionCode')
+
+  def testSetWebViewImplementation_invalidSignature(self):
+    mock_dump_sys = {
+        'WebViewPackages': {
+            'foo.org': 'Incorrect signature',
+        }
+    }
+    self._testSetWebViewImplementationHelper(mock_dump_sys,
+                                             'signed with release keys')
+
+
+class DeviceUtilsSetWebViewFallbackLogicTest(DeviceUtilsTest):
+
+  def testSetWebViewFallbackLogic_False_success(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.NOUGAT):
+      with self.assertCall(self.call.adb.Shell(
+          'cmd webviewupdate enable-redundant-packages'), 'Success'):
+        self.device.SetWebViewFallbackLogic(False)
+
+  def testSetWebViewFallbackLogic_True_success(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.NOUGAT):
+      with self.assertCall(self.call.adb.Shell(
+          'cmd webviewupdate disable-redundant-packages'), 'Success'):
+        self.device.SetWebViewFallbackLogic(True)
+
+  def testSetWebViewFallbackLogic_failure(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.NOUGAT):
+      with self.assertCall(self.call.adb.Shell(
+          'cmd webviewupdate enable-redundant-packages'), 'Oops!'):
+        with self.assertRaises(device_errors.CommandFailedError):
+          self.device.SetWebViewFallbackLogic(False)
+
+  def testSetWebViewFallbackLogic_beforeNougat(self):
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=version_codes.MARSHMALLOW):
+      with self.assertCalls():
+        self.device.SetWebViewFallbackLogic(False)
+
+  def testSetWebViewFallbackLogic_afterPie(self):
+    # TODO(ntfschr): replace this with the Q constant when the SDK is public and
+    # the codename is finalized.
+    q_version_code = version_codes.PIE + 1
+    with self.patch_call(self.call.device.build_version_sdk,
+                         return_value=q_version_code):
+      with self.assertCalls():
+        self.device.SetWebViewFallbackLogic(False)
+
 
 class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
 
@@ -2745,9 +2967,7 @@
         (mock.call.devil.android.device_temp_file.DeviceTempFile(
             self.adb, suffix='.png'),
          MockTempFile('/tmp/path/temp-123.png')),
-        (self.call.adb.Shell(
-          '/system/bin/screencap -p /tmp/path/temp-123.png',
-          ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('/system/bin/screencap -p /tmp/path/temp-123.png'),
          ''),
         self.call.device.PullFile('/tmp/path/temp-123.png',
                                   '/test/host/screenshot.png')):
@@ -2814,7 +3034,7 @@
     self.assertEqual(self.device._cache['test'], 0)
     self.assertEqual(client_cache_one, {'test': 1})
     self.assertEqual(client_cache_two, {'test': 2})
-    self.device._ClearCache()
+    self.device.ClearCache()
     self.assertTrue('test' not in self.device._cache)
     self.assertEqual(client_cache_one, {})
     self.assertEqual(client_cache_two, {})
@@ -2825,7 +3045,7 @@
     client_cache_two = self.device.GetClientCache('ClientOne')
     self.assertEqual(client_cache_one, {'test': 1})
     self.assertEqual(client_cache_two, {'test': 1})
-    self.device._ClearCache()
+    self.device.ClearCache()
     self.assertEqual(client_cache_one, {})
     self.assertEqual(client_cache_two, {})
 
@@ -2838,9 +3058,9 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI),
+         abis.ARM),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       blacklist = mock.NonCallableMock(**{'Read.return_value': []})
       devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
     for serial, device in zip(test_serials, devices):
@@ -2853,7 +3073,7 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       blacklist = mock.NonCallableMock(
           **{'Read.return_value': ['fedcba9876543210']})
       devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
@@ -2867,9 +3087,9 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI),
+         abis.ARM),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI),
+         abis.ARM),
         (mock.call.devil.android.device_errors.MultipleDevicesError(mock.ANY),
          _MockMultipleDevicesError())):
       with self.assertRaises(_MockMultipleDevicesError):
@@ -2881,7 +3101,7 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       devices = device_utils.DeviceUtils.HealthyDevices(device_arg=None)
     self.assertEquals(1, len(devices))
 
@@ -2913,9 +3133,9 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI),
+         abis.ARM),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       devices = device_utils.DeviceUtils.HealthyDevices(device_arg=())
     self.assertEquals(2, len(devices))
 
@@ -2952,6 +3172,30 @@
     self.assertEquals(mock_sleep.call_args_list, [
         mock.call(2), mock.call(4), mock.call(8), mock.call(16)])
 
+  @mock.patch('time.sleep')
+  @mock.patch('devil.android.device_utils.RestartServer')
+  def testHealthyDevices_EmptyListDeviceArg_no_attached_with_resets(
+      self, mock_restart, mock_sleep):
+    # The reset_usb import fails on windows. Mock the full import here so it can
+    # succeed like it would on linux.
+    mock_reset_import = mock.MagicMock()
+    sys.modules['devil.utils.reset_usb'] = mock_reset_import
+    with self.assertCalls(
+        (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+        (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+        (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+        (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+        (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), [])):
+      with self.assertRaises(device_errors.NoDevicesError):
+        with mock.patch.object(
+            mock_reset_import, 'reset_all_android_devices') as mock_reset:
+          device_utils.DeviceUtils.HealthyDevices(device_arg=[], retries=4,
+                                                  enable_usb_resets=True)
+          self.assertEquals(mock_reset.call_count, 1)
+    self.assertEquals(mock_restart.call_count, 4)
+    self.assertEquals(mock_sleep.call_args_list, [
+        mock.call(2), mock.call(4), mock.call(8), mock.call(16)])
+
   def testHealthyDevices_ListDeviceArg(self):
     device_arg = ['0123456789abcdef', 'fedcba9876543210']
     try:
@@ -2968,12 +3212,12 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI),
+         abis.ARM),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       with self.assertRaises(device_errors.NoDevicesError):
         device_utils.DeviceUtils.HealthyDevices(device_arg=[], retries=0,
-                                                abis=[ARM64_ABI])
+                                                abis=[abis.ARM_64])
 
   def testHealthyDevices_abisArg_filter_on_abi(self):
     test_serials = ['0123456789abcdef', 'fedcba9876543210']
@@ -2981,12 +3225,12 @@
         (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
          [_AdbWrapperMock(s) for s in test_serials]),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM64_ABI),
+         abis.ARM_64),
         (mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
-         ARM32_ABI)):
+         abis.ARM)):
       devices = device_utils.DeviceUtils.HealthyDevices(device_arg=[],
                                                         retries=0,
-                                                        abis=[ARM64_ABI])
+                                                        abis=[abis.ARM_64])
     self.assertEquals(1, len(devices))
 
 
@@ -3200,9 +3444,7 @@
         '  Device ID = 123454321')
     with self.assertCalls(
         (self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
-        (self.call.adb.Shell(
-          'dumpsys iphonesubinfo', ensure_logs_on_timeout=False),
-         dumpsys_output)):
+        (self.call.adb.Shell('dumpsys iphonesubinfo'), dumpsys_output)):
       self.assertEquals(self.device.GetIMEI(), '123454321')
 
   def testSuccessfulServiceCall(self):
@@ -3214,25 +3456,20 @@
     """
     with self.assertCalls(
         (self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
-        (self.call.adb.Shell(
-          'service call iphonesubinfo 1', ensure_logs_on_timeout=False),
-         service_output)):
+        (self.call.adb.Shell('service call iphonesubinfo 1'), service_output)):
       self.assertEquals(self.device.GetIMEI(), '765432101234567')
 
   def testNoIMEI(self):
     with self.assertCalls(
         (self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
-        (self.call.adb.Shell(
-          'dumpsys iphonesubinfo', ensure_logs_on_timeout=False),
-         'no device id')):
+        (self.call.adb.Shell('dumpsys iphonesubinfo'), 'no device id')):
       with self.assertRaises(device_errors.CommandFailedError):
         self.device.GetIMEI()
 
   def testAdbError(self):
     with self.assertCalls(
         (self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
-        (self.call.adb.Shell(
-          'service call iphonesubinfo 1', ensure_logs_on_timeout=False),
+        (self.call.adb.Shell('service call iphonesubinfo 1'),
          self.ShellError())):
       with self.assertRaises(device_errors.CommandFailedError):
         self.device.GetIMEI()
@@ -3250,7 +3487,6 @@
 
 class DeviceUtilsChangeSecurityContext(DeviceUtilsTest):
 
-
   def testChangeSecurityContext(self):
     with self.assertCalls(
         (self.call.device.RunShellCommand(
@@ -3259,6 +3495,49 @@
       self.device.ChangeSecurityContext('u:object_r:system_data_file:s0',
                                         ['/path', '/path2'])
 
+
+class DeviceUtilsLocale(DeviceUtilsTest):
+
+  def testLocaleLegacy(self):
+    with self.assertCalls(
+        (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+        (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+        (self.call.device.GetProp('persist.sys.country', cache=False), 'US')):
+      self.assertEquals(self.device.GetLocale(), ('en', 'US'))
+
+  def testLocale(self):
+    with self.assertCalls(
+        (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US'),
+        (self.call.device.GetProp('persist.sys.locale', cache=False),
+         'en-US-sw')):
+      self.assertEquals(self.device.GetLocale(), ('en', 'US'))
+      self.assertEquals(self.device.GetLocale(), ('en', 'US-sw'))
+
+  def testBadLocale(self):
+    with self.assertCalls(
+        (self.call.device.GetProp('persist.sys.locale', cache=False), 'en')):
+      self.assertEquals(self.device.GetLocale(), ('', ''))
+
+
+  def testLanguageAndCountryLegacy(self):
+    with self.assertCalls(
+        (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+        (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+        (self.call.device.GetProp('persist.sys.country', cache=False), 'US'),
+        (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+        (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+        (self.call.device.GetProp('persist.sys.country', cache=False), 'US')):
+      self.assertEquals(self.device.GetLanguage(), 'en')
+      self.assertEquals(self.device.GetCountry(), 'US')
+
+  def testLanguageAndCountry(self):
+    with self.assertCalls(
+        (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US'),
+        (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US')):
+      self.assertEquals(self.device.GetLanguage(), 'en')
+      self.assertEquals(self.device.GetCountry(), 'US')
+
+
 if __name__ == '__main__':
   logging.getLogger().setLevel(logging.DEBUG)
   unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/fastboot_utils.py b/catapult/devil/devil/android/fastboot_utils.py
index 3bd3ee8..3621d7f 100644
--- a/catapult/devil/devil/android/fastboot_utils.py
+++ b/catapult/devil/devil/android/fastboot_utils.py
@@ -108,7 +108,7 @@
     This waits for the device serial to show up in fastboot devices output.
     """
     def fastboot_mode():
-      return self._serial in self.fastboot.Devices()
+      return any(self._serial == str(d) for d in self.fastboot.Devices())
 
     timeout_retry.WaitFor(fastboot_mode, wait_period=self._FASTBOOT_WAIT_TIME)
 
diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py
index c96dbad..110cf82 100644
--- a/catapult/devil/devil/android/flag_changer.py
+++ b/catapult/devil/devil/android/flag_changer.py
@@ -74,6 +74,8 @@
     if use_legacy_path:
       cmdline_path, alternate_cmdline_path = (
           alternate_cmdline_path, cmdline_path)
+      if not self._device.HasRoot():
+        raise ValueError('use_legacy_path requires a rooted device')
     self._cmdline_path = cmdline_path
 
     if self._device.PathExists(alternate_cmdline_path):
@@ -103,7 +105,7 @@
     self._state_stack[-1] = set(flags)
     return flags
 
-  def ReplaceFlags(self, flags):
+  def ReplaceFlags(self, flags, log_flags=True):
     """Replaces the flags in the command line with the ones provided.
        Saves the current flags state on the stack, so a call to Restore will
        change the state back to the one preceeding the call to ReplaceFlags.
@@ -119,7 +121,7 @@
     new_flags = set(flags)
     self._state_stack.append(new_flags)
     self._SetPermissive()
-    return self._UpdateCommandLineFile()
+    return self._UpdateCommandLineFile(log_flags=log_flags)
 
   def AddFlags(self, flags):
     """Appends flags to the command line if they aren't already there.
@@ -179,10 +181,14 @@
     """Set SELinux to permissive, if needed.
 
     On Android N and above this is needed in order to allow Chrome to read the
-    command line file.
+    legacy command line file.
 
     TODO(crbug.com/699082): Remove when a better solution exists.
     """
+    # TODO(crbug.com/948578): figure out the exact scenarios where the lowered
+    # permissions are needed, and document them in the code.
+    if not self._device.HasRoot():
+      return
     if (self._device.build_version_sdk >= version_codes.NOUGAT and
         self._device.GetEnforce()):
       self._device.SetEnforce(enabled=False)
@@ -209,7 +215,7 @@
       self._ResetEnforce()
     return self._UpdateCommandLineFile()
 
-  def _UpdateCommandLineFile(self):
+  def _UpdateCommandLineFile(self, log_flags=True):
     """Writes out the command line to the file, or removes it if empty.
 
     Returns:
@@ -221,9 +227,11 @@
     else:
       self._device.RemovePath(self._cmdline_path, force=True, as_root=True)
 
-    current_flags = self.GetCurrentFlags()
-    logger.info('Flags now set on the device: %s', current_flags)
-    return current_flags
+    flags = self.GetCurrentFlags()
+    logging.info('Flags now written on the device to %s', self._cmdline_path)
+    if log_flags:
+      logging.info('Flags: %s', flags)
+    return flags
 
 
 def _ParseFlags(line):
diff --git a/catapult/devil/devil/android/logcat_monitor.py b/catapult/devil/devil/android/logcat_monitor.py
index 249320b..b5f796b 100644
--- a/catapult/devil/devil/android/logcat_monitor.py
+++ b/catapult/devil/devil/android/logcat_monitor.py
@@ -31,7 +31,7 @@
       r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
 
   def __init__(self, adb, clear=True, filter_specs=None, output_file=None,
-               transform_func=None):
+               transform_func=None, check_error=True):
     """Create a LogcatMonitor instance.
 
     Args:
@@ -41,11 +41,14 @@
       output_file: File path to save recorded logcat.
       transform_func: An optional unary callable that takes and returns
         a list of lines, possibly transforming them in the process.
+      check_error: Check for and raise an exception on nonzero exit codes
+        from the underlying logcat command.
     """
     if isinstance(adb, adb_wrapper.AdbWrapper):
       self._adb = adb
     else:
       raise ValueError('Unsupported type passed for argument "device"')
+    self._check_error = check_error
     self._clear = clear
     self._filter_specs = filter_specs
     self._output_file = output_file
@@ -168,9 +171,11 @@
     def record_to_file():
       # Write the log with line buffering so the consumer sees each individual
       # line.
-      for data in self._adb.Logcat(filter_specs=self._filter_specs,
-                                   logcat_format='threadtime',
-                                   iter_timeout=self._RECORD_ITER_TIMEOUT):
+      for data in self._adb.Logcat(
+          filter_specs=self._filter_specs,
+          logcat_format='threadtime',
+          iter_timeout=self._RECORD_ITER_TIMEOUT,
+          check_error=self._check_error):
         if self._stop_recording_event.isSet():
           return
 
diff --git a/catapult/devil/devil/android/md5sum.py b/catapult/devil/devil/android/md5sum.py
index 6dece9e..f5b6f3c 100644
--- a/catapult/devil/devil/android/md5sum.py
+++ b/catapult/devil/devil/android/md5sum.py
@@ -89,7 +89,8 @@
   # Note: ":" is equivalent to "true".
   md5sum_script += ';:'
   try:
-    out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
+    out = device.RunShellCommand(
+        md5sum_script, shell=True, check_return=True, large_output=True)
   except device_errors.AdbShellCommandFailedError as e:
     # Push the binary only if it is found to not exist
     # (faster than checking up-front).
@@ -106,7 +107,8 @@
         device.RunShellCommand(mkdir_cmd, shell=True, check_return=True)
         device.adb.Push(md5sum_dist_bin_path, MD5SUM_DEVICE_BIN_PATH)
 
-      out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
+      out = device.RunShellCommand(
+          md5sum_script, shell=True, check_return=True, large_output=True)
     else:
       raise
 
diff --git a/catapult/devil/devil/android/ndk/__init__.py b/catapult/devil/devil/android/ndk/__init__.py
new file mode 100644
index 0000000..edd8dbc
--- /dev/null
+++ b/catapult/devil/devil/android/ndk/__init__.py
@@ -0,0 +1,6 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This package is intended for modules that are very tightly coupled to
+# tools or APIs from the Android NDK.
diff --git a/catapult/devil/devil/android/ndk/abis.py b/catapult/devil/devil/android/ndk/abis.py
new file mode 100644
index 0000000..dd32f7c
--- /dev/null
+++ b/catapult/devil/devil/android/ndk/abis.py
@@ -0,0 +1,16 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Android NDK ABIs.
+
+https://developer.android.com/ndk/guides/abis
+
+These constants can be compared against the value of
+devil.android.DeviceUtils.product_cpu_abi.
+"""
+
+ARM = 'armeabi-v7a'
+ARM_64 = 'arm64-v8a'
+X86 = 'x86'
+X86_64 = 'x86_64'
diff --git a/catapult/devil/devil/android/perf/perf_control.py b/catapult/devil/devil/android/perf/perf_control.py
index 2aa3b2f..1226be8 100644
--- a/catapult/devil/devil/android/perf/perf_control.py
+++ b/catapult/devil/devil/android/perf/perf_control.py
@@ -28,12 +28,40 @@
   'AFTKMST12': {
     'default_mode_governor': 'interactive',
   },
+  # Pixel 3
+  'blueline': {
+     'high_perf_mode': {
+       'bring_cpu_cores_online': True,
+       # The SoC is Arm big.LITTLE. The cores 0..3 are LITTLE, the 4..7 are big.
+       'cpu_max_freq': {'0..3': 1228800, '4..7': 1536000},
+       'gpu_max_freq': 520000000,
+     },
+     'default_mode': {
+       'cpu_max_freq': {'0..3': 1766400, '4..7': 2649600},
+       'gpu_max_freq': 710000000,
+     },
+    'default_mode_governor': 'schedutil',
+  },
   'GT-I9300': {
     'default_mode_governor': 'pegasusq',
   },
   'Galaxy Nexus': {
     'default_mode_governor': 'interactive',
   },
+  # Pixel
+  'msm8996': {
+     'high_perf_mode': {
+       'bring_cpu_cores_online': True,
+       'cpu_max_freq': 1209600,
+       'gpu_max_freq': 315000000,
+     },
+     'default_mode': {
+       # The SoC is Arm big.LITTLE. The cores 0..1 are LITTLE, the 2..3 are big.
+       'cpu_max_freq': {'0..1': 1593600, '2..3': 2150400},
+       'gpu_max_freq': 624000000,
+     },
+    'default_mode_governor': 'sched',
+  },
   'Nexus 7': {
     'default_mode_governor': 'interactive',
   },
@@ -78,6 +106,10 @@
   },
 }
 
+def _GetPerfModeDefinitions(product_model):
+  if product_model.startswith('AOSP on '):
+    product_model = product_model.replace('AOSP on ', '')
+  return _PERFORMANCE_MODE_DEFINITIONS.get(product_model)
 
 def _NoisyWarning(message):
   message += ' Results may be NOISY!!'
@@ -148,8 +180,7 @@
     except device_errors.CommandFailedError:
       _NoisyWarning('Need root for performance mode.')
       return
-    mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
-        self._device.product_model)
+    mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
     if not mode_definitions:
       self.SetScalingGovernor('performance')
       return
@@ -170,8 +201,7 @@
     """Sets the performance mode for the device to its default mode."""
     if not self._device.HasRoot():
       return
-    mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
-        self._device.product_model)
+    mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
     if not mode_definitions:
       self.SetScalingGovernor('ondemand')
     else:
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector.py b/catapult/devil/devil/android/perf/surface_stats_collector.py
index ea46a39..f1140c1 100644
--- a/catapult/devil/devil/android/perf/surface_stats_collector.py
+++ b/catapult/devil/devil/android/perf/surface_stats_collector.py
@@ -2,7 +2,9 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import logging
 import Queue
+import re
 import threading
 
 
@@ -135,61 +137,73 @@
       The return value may be (None, None) if there was no data collected (for
       example, if the app was closed before the collector thread has finished).
     """
-    # adb shell dumpsys SurfaceFlinger --latency <window name>
-    # prints some information about the last 128 frames displayed in
-    # that window.
-    # The data returned looks like this:
-    # 16954612
-    # 7657467895508   7657482691352   7657493499756
-    # 7657484466553   7657499645964   7657511077881
-    # 7657500793457   7657516600576   7657527404785
-    # (...)
-    #
-    # The first line is the refresh period (here 16.95 ms), it is followed
-    # by 128 lines w/ 3 timestamps in nanosecond each:
-    # A) when the app started to draw
-    # B) the vsync immediately preceding SF submitting the frame to the h/w
-    # C) timestamp immediately after SF submitted that frame to the h/w
-    #
-    # The difference between the 1st and 3rd timestamp is the frame-latency.
-    # An interesting data is when the frame latency crosses a refresh period
-    # boundary, this can be calculated this way:
-    #
-    # ceil((C - A) / refresh-period)
-    #
-    # (each time the number above changes, we have a "jank").
-    # If this happens a lot during an animation, the animation appears
-    # janky, even if it runs at 60 fps in average.
     window_name = self._GetSurfaceViewWindowName()
     command = ['dumpsys', 'SurfaceFlinger', '--latency']
     # Even if we don't find the window name, run the command to get the refresh
     # period.
     if window_name:
       command.append(window_name)
-    results = self._device.RunShellCommand(command, check_return=True)
-    if not len(results):
-      return (None, None)
+    output = self._device.RunShellCommand(command, check_return=True)
+    return ParseFrameData(output, parse_timestamps=bool(window_name))
 
-    timestamps = []
-    nanoseconds_per_millisecond = 1e6
-    refresh_period = long(results[0]) / nanoseconds_per_millisecond
-    if not window_name:
-      return (refresh_period, timestamps)
 
-    # If a fence associated with a frame is still pending when we query the
-    # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
-    # Since we only care about completed frames, we will ignore any timestamps
-    # with this value.
-    pending_fence_timestamp = (1 << 63) - 1
+def ParseFrameData(lines, parse_timestamps):
+  # adb shell dumpsys SurfaceFlinger --latency <window name>
+  # prints some information about the last 128 frames displayed in
+  # that window.
+  # The data returned looks like this:
+  # 16954612
+  # 7657467895508   7657482691352   7657493499756
+  # 7657484466553   7657499645964   7657511077881
+  # 7657500793457   7657516600576   7657527404785
+  # (...)
+  #
+  # The first line is the refresh period (here 16.95 ms), it is followed
+  # by 128 lines w/ 3 timestamps in nanosecond each:
+  # A) when the app started to draw
+  # B) the vsync immediately preceding SF submitting the frame to the h/w
+  # C) timestamp immediately after SF submitted that frame to the h/w
+  #
+  # The difference between the 1st and 3rd timestamp is the frame-latency.
+  # An interesting data is when the frame latency crosses a refresh period
+  # boundary, this can be calculated this way:
+  #
+  # ceil((C - A) / refresh-period)
+  #
+  # (each time the number above changes, we have a "jank").
+  # If this happens a lot during an animation, the animation appears
+  # janky, even if it runs at 60 fps in average.
+  results = []
+  for line in lines:
+    # Skip over lines with anything other than digits and whitespace.
+    if re.search(r'[^\d\s]', line):
+      logging.warning('unexpected output: %s', line)
+    else:
+      results.append(line)
+  if not results:
+    return None, None
 
-    for line in results[1:]:
-      fields = line.split()
-      if len(fields) != 3:
-        continue
-      timestamp = long(fields[1])
-      if timestamp == pending_fence_timestamp:
-        continue
-      timestamp /= nanoseconds_per_millisecond
-      timestamps.append(timestamp)
+  timestamps = []
+  nanoseconds_per_millisecond = 1e6
+  refresh_period = long(results[0]) / nanoseconds_per_millisecond
+  if not parse_timestamps:
+    return refresh_period, timestamps
 
-    return (refresh_period, timestamps)
+  # If a fence associated with a frame is still pending when we query the
+  # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
+  # Since we only care about completed frames, we will ignore any timestamps
+  # with this value.
+  pending_fence_timestamp = (1 << 63) - 1
+
+  for line in results[1:]:
+    fields = line.split()
+    if len(fields) != 3:
+      logging.warning('Unexpected line: %s', line)
+      continue
+    timestamp = long(fields[1])
+    if timestamp == pending_fence_timestamp:
+      continue
+    timestamp /= nanoseconds_per_millisecond
+    timestamps.append(timestamp)
+
+  return refresh_period, timestamps
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector_test.py b/catapult/devil/devil/android/perf/surface_stats_collector_test.py
new file mode 100644
index 0000000..13b345c
--- /dev/null
+++ b/catapult/devil/devil/android/perf/surface_stats_collector_test.py
@@ -0,0 +1,40 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+from devil.android.perf import surface_stats_collector
+
+
+class SurfaceStatsCollectorTests(unittest.TestCase):
+  def testParseFrameData_simple(self):
+    actual = surface_stats_collector.ParseFrameData([
+        '16954612',
+        '7657467895508   7657482691352   7657493499756',
+        '7657484466553   7657499645964   7657511077881',
+        '7657500793457   7657516600576   7657527404785',
+    ], parse_timestamps=True)
+    self.assertEqual(
+        actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))
+
+  def testParseFrameData_withoutTimestamps(self):
+    actual = surface_stats_collector.ParseFrameData([
+        '16954612',
+        '7657467895508   7657482691352   7657493499756',
+        '7657484466553   7657499645964   7657511077881',
+        '7657500793457   7657516600576   7657527404785',
+    ], parse_timestamps=False)
+    self.assertEqual(
+        actual, (16.954612, []))
+
+  def testParseFrameData_withWarning(self):
+    actual = surface_stats_collector.ParseFrameData([
+        'SurfaceFlinger appears to be unresponsive, dumping anyways',
+        '16954612',
+        '7657467895508   7657482691352   7657493499756',
+        '7657484466553   7657499645964   7657511077881',
+        '7657500793457   7657516600576   7657527404785',
+    ], parse_timestamps=True)
+    self.assertEqual(
+        actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))
diff --git a/catapult/devil/devil/android/ports.py b/catapult/devil/devil/android/ports.py
index 1d4e5f2..4547a62 100644
--- a/catapult/devil/devil/android/ports.py
+++ b/catapult/devil/devil/android/ports.py
@@ -116,7 +116,7 @@
   """
   base_urls = ('127.0.0.1:%d' % device_port, 'localhost:%d' % device_port)
   netstat_results = device.RunShellCommand(
-      ['netstat', '-a'], check_return=True, large_output=True)
+      ['netstat', '-an'], check_return=True, large_output=True)
   for single_connect in netstat_results:
     # Column 3 is the local address which we want to check with.
     connect_results = single_connect.split()
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper.py b/catapult/devil/devil/android/sdk/adb_wrapper.py
index 2fbe963..13c0f52 100644
--- a/catapult/devil/devil/android/sdk/adb_wrapper.py
+++ b/catapult/devil/devil/android/sdk/adb_wrapper.py
@@ -34,6 +34,7 @@
 
 
 ADB_KEYS_FILE = '/data/misc/adb/adb_keys'
+ADB_HOST_KEYS_DIR = os.path.join(os.path.expanduser('~'), '.android')
 
 DEFAULT_TIMEOUT = 30
 DEFAULT_RETRIES = 2
@@ -262,15 +263,22 @@
   @classmethod
   @decorators.WithTimeoutAndConditionalRetries(_ShouldRetryAdbCmd)
   def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None,
-                 check_error=True, cpu_affinity=None,
-                 ensure_logs_on_timeout=False):
-    timeout = timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()
-    if ensure_logs_on_timeout:
-      timeout = 0.95 * timeout
+                 check_error=True, cpu_affinity=None, additional_env=None):
+    if timeout:
+      remaining = timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()
+      if remaining:
+        # Use a slightly smaller timeout than remaining time to ensure that we
+        # have time to collect output from the command.
+        timeout = 0.95 * remaining
+      else:
+        timeout = None
+    env = cls._ADB_ENV.copy()
+    if additional_env:
+      env.update(additional_env)
     try:
       status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
           cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity),
-          timeout, env=cls._ADB_ENV)
+          timeout, env=env)
     except OSError as e:
       if e.errno in (errno.ENOENT, errno.ENOEXEC):
         raise device_errors.NoAdbError(msg=str(e))
@@ -294,9 +302,7 @@
     return output
   # pylint: enable=unused-argument
 
-  def _RunDeviceAdbCmd(
-      self, args, timeout, retries, check_error=True,
-      ensure_logs_on_timeout=False):
+  def _RunDeviceAdbCmd(self, args, timeout, retries, check_error=True):
     """Runs an adb command on the device associated with this object.
 
     Args:
@@ -304,23 +310,27 @@
       timeout: Timeout in seconds.
       retries: Number of retries.
       check_error: Check that the command doesn't return an error message. This
-        does NOT check the exit status of shell commands.
+        does check the error status of adb but DOES NOT check the exit status
+        of shell commands.
 
     Returns:
       The output of the command.
     """
     return self._RunAdbCmd(args, timeout=timeout, retries=retries,
                            device_serial=self._device_serial,
-                           check_error=check_error,
-                           ensure_logs_on_timeout=ensure_logs_on_timeout)
+                           check_error=check_error)
 
-  def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout):
+  def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout,
+                           check_error=True):
     """Runs an adb command and returns an iterator over its output lines.
 
     Args:
       args: A list of arguments to adb.
       iter_timeout: Timeout for each iteration in seconds.
       timeout: Timeout for the entire command in seconds.
+      check_error: Check that the command succeeded. This does check the
+        error status of the adb command but DOES NOT check the exit status
+        of shell commands.
 
     Yields:
       The output of the command line by line.
@@ -329,7 +339,8 @@
         self._BuildAdbCmd(args, self._device_serial),
         iter_timeout=iter_timeout,
         timeout=timeout,
-        env=self._ADB_ENV)
+        env=self._ADB_ENV,
+        check_status=check_error)
 
   def __eq__(self, other):
     """Consider instances equal if they refer to the same device.
@@ -367,10 +378,21 @@
     cls._RunAdbCmd(['kill-server'], timeout=timeout, retries=retries)
 
   @classmethod
-  def StartServer(cls, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
+  def StartServer(cls, keys=None, timeout=DEFAULT_TIMEOUT,
+                  retries=DEFAULT_RETRIES):
+    """Starts the ADB server.
+
+    Args:
+      keys: (optional) List of local ADB keys to use to auth with devices.
+      timeout: (optional) Timeout per try in seconds.
+      retries: (optional) Number of retries to attempt.
+    """
+    additional_env = {}
+    if keys:
+      additional_env['ADB_VENDOR_KEYS'] = ':'.join(keys)
     # CPU affinity is used to reduce adb instability http://crbug.com/268450
     cls._RunAdbCmd(['start-server'], timeout=timeout, retries=retries,
-                   cpu_affinity=0)
+                   cpu_affinity=0, additional_env=additional_env)
 
   @classmethod
   def GetDevices(cls, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
@@ -507,17 +529,14 @@
     return cmd_helper.StartCmd(
         self._BuildAdbCmd(['shell'] + cmd, self._device_serial))
 
-  def Shell(self, command, expect_status=0, ensure_logs_on_timeout=False,
-            timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
+  def Shell(self, command, expect_status=0, timeout=DEFAULT_TIMEOUT,
+            retries=DEFAULT_RETRIES):
     """Runs a shell command on the device.
 
     Args:
       command: A string with the shell command to run.
       expect_status: (optional) Check that the command's exit status matches
         this value. Default is 0. If set to None the test is skipped.
-      ensure_logs_on_timeout: If True, will use a timeout that is 5% smaller
-        than the remaining time on the thread watchdog for the internal adb
-        command, which allows to retrive logs on timeout.
       timeout: (optional) Timeout per try in seconds.
       retries: (optional) Number of retries to attempt.
 
@@ -532,9 +551,7 @@
       args = ['shell', command]
     else:
       args = ['shell', '( %s );echo %%$?' % command.rstrip()]
-    output = self._RunDeviceAdbCmd(
-        args, timeout, retries, check_error=False,
-        ensure_logs_on_timeout=ensure_logs_on_timeout)
+    output = self._RunDeviceAdbCmd(args, timeout, retries, check_error=False)
     if expect_status is not None:
       output_end = output.rfind('%')
       if output_end < 0:
@@ -607,7 +624,7 @@
 
   def Logcat(self, clear=False, dump=False, filter_specs=None,
              logcat_format=None, ring_buffer=None, iter_timeout=None,
-             timeout=None, retries=DEFAULT_RETRIES):
+             check_error=True, timeout=None, retries=DEFAULT_RETRIES):
     """Get an iterable over the logcat output.
 
     Args:
@@ -623,6 +640,7 @@
       iter_timeout: If set and neither clear nor dump is set, the number of
         seconds to wait between iterations. If no line is found before the
         given number of seconds elapses, the iterable will yield None.
+      check_error: Whether to check the exit status of the logcat command.
       timeout: (optional) If set, timeout per try in seconds. If clear or dump
         is set, defaults to DEFAULT_TIMEOUT.
       retries: (optional) If clear or dump is set, the number of retries to
@@ -648,10 +666,13 @@
       cmd.extend(filter_specs)
 
     if use_iter:
-      return self._IterRunDeviceAdbCmd(cmd, iter_timeout, timeout)
+      return self._IterRunDeviceAdbCmd(
+          cmd, iter_timeout, timeout, check_error=check_error)
     else:
       timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
-      return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines()
+      output = self._RunDeviceAdbCmd(
+          cmd, timeout, retries, check_error=check_error)
+      return output.splitlines()
 
   def Forward(self, local, remote, allow_rebind=False,
               timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
@@ -947,18 +968,28 @@
     return self._RunDeviceAdbCmd(['emu'] + cmd, timeout, retries)
 
   def DisableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
-    """Disable Marshmallow's Verity security feature"""
+    """Disable Marshmallow's Verity security feature.
+
+    Returns:
+      The output of the disable-verity command as a string.
+    """
     output = self._RunDeviceAdbCmd(['disable-verity'], timeout, retries)
     if output and not _VERITY_DISABLE_RE.search(output):
       raise device_errors.AdbCommandFailedError(
           ['disable-verity'], output, device_serial=self._device_serial)
+    return output
 
   def EnableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
-    """Enable Marshmallow's Verity security feature"""
+    """Enable Marshmallow's Verity security feature.
+
+    Returns:
+      The output of the enable-verity command as a string.
+    """
     output = self._RunDeviceAdbCmd(['enable-verity'], timeout, retries)
     if output and not _VERITY_ENABLE_RE.search(output):
       raise device_errors.AdbCommandFailedError(
           ['enable-verity'], output, device_serial=self._device_serial)
+    return output
 
   @property
   def is_emulator(self):
diff --git a/catapult/devil/devil/android/tools/device_recovery.py b/catapult/devil/devil/android/tools/device_recovery.py
index e8b9ba3..8050e6f 100755
--- a/catapult/devil/devil/android/tools/device_recovery.py
+++ b/catapult/devil/devil/android/tools/device_recovery.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env vpython
 # Copyright 2016 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
@@ -6,6 +6,7 @@
 """A script to recover devices in a known bad state."""
 
 import argparse
+import glob
 import logging
 import os
 import signal
@@ -30,14 +31,18 @@
 
 logger = logging.getLogger(__name__)
 
+from py_utils import modules_util
+
+
+# Script depends on features from psutil version 2.0 or higher.
+modules_util.RequireVersion(psutil, '2.0')
+
 
 def KillAllAdb():
   def get_all_adb():
     for p in psutil.process_iter():
       try:
-        # Note: p.as_dict is compatible with both older (v1 and under) as well
-        # as newer (v2 and over) versions of psutil.
-        # See: http://grodola.blogspot.com/2014/01/psutil-20-porting.html
+        # Retrieve all required process infos at once.
         pinfo = p.as_dict(attrs=['pid', 'name', 'cmdline'])
         if pinfo['name'] == 'adb':
           pinfo['cmdline'] = ' '.join(pinfo['cmdline'])
@@ -60,12 +65,58 @@
       pass
 
 
+def TryAuth(device):
+  """Uses anything in ~/.android/ that looks like a key to auth with the device.
+
+  Args:
+    device: The DeviceUtils device to attempt to auth.
+
+  Returns:
+    True if device successfully authed.
+  """
+  possible_keys = glob.glob(os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, '*key'))
+  if len(possible_keys) <= 1:
+    logger.warning(
+        'Only %d ADB keys available. Not forcing auth.', len(possible_keys))
+    return False
+
+  KillAllAdb()
+  adb_wrapper.AdbWrapper.StartServer(keys=possible_keys)
+  new_state = device.adb.GetState()
+  if new_state != 'device':
+    logger.error(
+        'Auth failed. Device %s still stuck in %s.', str(device), new_state)
+    return False
+
+  # It worked! Now register the host's default ADB key on the device so we don't
+  # have to do all that again.
+  pub_key = os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, 'adbkey.pub')
+  if not os.path.exists(pub_key):  # This really shouldn't happen.
+    logger.error('Default ADB key not available at %s.', pub_key)
+    return False
+
+  with open(pub_key) as f:
+    pub_key_contents = f.read()
+  try:
+    device.WriteFile(adb_wrapper.ADB_KEYS_FILE, pub_key_contents, as_root=True)
+  except (device_errors.CommandTimeoutError,
+          device_errors.CommandFailedError,
+          device_errors.DeviceUnreachableError):
+    logger.exception('Unable to write default ADB key to %s.', str(device))
+    return False
+  return True
+
+
 def RecoverDevice(device, blacklist, should_reboot=lambda device: True):
   if device_status.IsBlacklisted(device.adb.GetDeviceSerial(),
                                  blacklist):
     logger.debug('%s is blacklisted, skipping recovery.', str(device))
     return
 
+  if device.adb.GetState() == 'unauthorized' and TryAuth(device):
+    logger.info('Successfully authed device %s!', str(device))
+    return
+
   if should_reboot(device):
     try:
       device.WaitUntilFullyBooted(retries=0)
@@ -136,7 +187,7 @@
   should_restart_adb = should_restart_usb.union(set(
       status['serial'] for status in statuses
       if status['adb_status'] == 'unauthorized'))
-  should_reboot_device = should_restart_adb.union(set(
+  should_reboot_device = should_restart_usb.union(set(
       status['serial'] for status in statuses
       if status['blacklisted']))
 
diff --git a/catapult/devil/devil/android/tools/script_common.py b/catapult/devil/devil/android/tools/script_common.py
index 150e63f..897659b 100644
--- a/catapult/devil/devil/android/tools/script_common.py
+++ b/catapult/devil/devil/android/tools/script_common.py
@@ -11,13 +11,38 @@
 
 
 def AddEnvironmentArguments(parser):
-  """Adds environment-specific arguments to the provided parser."""
+  """Adds environment-specific arguments to the provided parser.
+
+  After adding these arguments, you must pass the user-specified values when
+  initializing devil. See the InitializeEnvironment() to determine how to do so.
+
+  Args:
+    parser: an instance of argparse.ArgumentParser
+  """
   parser.add_argument(
       '--adb-path', type=os.path.realpath,
       help='Path to the adb binary')
 
 
 def InitializeEnvironment(args):
+  """Initializes devil based on the args added by AddEnvironmentArguments().
+
+  This initializes devil, and configures it to use the adb binary specified by
+  the '--adb-path' flag (if provided by the user, otherwise this defaults to
+  devil's copy of adb). Although this is one possible way to initialize devil,
+  you should check if your project has prefered ways to initialize devil (ex.
+  the chromium project uses devil_chromium.Initialize() to have different
+  defaults for dependencies).
+
+  This method requires having previously called AddEnvironmentArguments() on the
+  relevant argparse.ArgumentParser.
+
+  Note: you should only initialize devil once, and subsequent calls to any
+  method wrapping devil_env.config.Initialize() will have no effect.
+
+  Args:
+    args: the parsed args returned by an argparse.ArgumentParser
+  """
   devil_dynamic_config = devil_env.EmptyConfig()
   if args.adb_path:
     devil_dynamic_config['dependencies'].update(
@@ -28,7 +53,11 @@
 
 
 def AddDeviceArguments(parser):
-  """Adds device and blacklist arguments to the provided parser."""
+  """Adds device and blacklist arguments to the provided parser.
+
+  Args:
+    parser: an instance of argparse.ArgumentParser
+  """
   parser.add_argument(
       '-d', '--device', dest='devices', action='append',
       help='Serial number of the Android device to use. (default: use all)')
diff --git a/catapult/devil/devil/android/tools/system_app.py b/catapult/devil/devil/android/tools/system_app.py
index 4fe35e5..8629ae6 100755
--- a/catapult/devil/devil/android/tools/system_app.py
+++ b/catapult/devil/devil/android/tools/system_app.py
@@ -10,6 +10,7 @@
 import logging
 import os
 import posixpath
+import re
 import sys
 
 
@@ -31,6 +32,19 @@
 logger = logging.getLogger(__name__)
 
 
+# Some system apps aren't actually installed in the /system/ directory, so
+# special case them here with the correct install location.
+SPECIAL_SYSTEM_APP_LOCATIONS = {
+  # This also gets installed in /data/app when not a system app, so this script
+  # will remove either version. This doesn't appear to cause any issues, but
+  # will cause a few unnecessary reboots if this is the only package getting
+  # removed and it's already not a system app.
+  'com.google.ar.core': '/data/app/',
+}
+
+# Gets app path and package name pm list packages -f output.
+_PM_LIST_PACKAGE_PATH_RE = re.compile(r'^\s*package:(\S+)=(\S+)\s*$')
+
 def RemoveSystemApps(device, package_names):
   """Removes the given system apps.
 
@@ -46,7 +60,8 @@
 
 
 @contextlib.contextmanager
-def ReplaceSystemApp(device, package_name, replacement_apk):
+def ReplaceSystemApp(device, package_name, replacement_apk,
+                     install_timeout=None):
   """A context manager that replaces the given system app while in scope.
 
   Args:
@@ -57,7 +72,7 @@
   """
   storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb)
   relocate_app = _RelocateApp(device, package_name, storage_dir.name)
-  install_app = _TemporarilyInstallApp(device, replacement_apk)
+  install_app = _TemporarilyInstallApp(device, replacement_apk, install_timeout)
   with storage_dir, relocate_app, install_app:
     yield
 
@@ -66,8 +81,36 @@
   """Finds all system paths for the given packages."""
   found_paths = []
   for system_package in system_package_list:
-    found_paths.extend(device.GetApplicationPaths(system_package))
-  return [p for p in found_paths if p.startswith('/system/')]
+    paths = _GetApplicationPaths(device, system_package)
+    p = _GetSystemPath(system_package, paths)
+    if p:
+      found_paths.append(p)
+  return found_paths
+
+
+# Find all application paths, even those flagged as uninstalled, as these
+# would still block another package with the same name from installation
+# if they differ in signing keys.
+# TODO(aluo): Move this into device_utils.py
+def _GetApplicationPaths(device, package):
+  paths = []
+  lines = device.RunShellCommand(['pm', 'list', 'packages', '-f', '-u',
+                                  package], check_return=True)
+  for line in lines:
+    match = re.match(_PM_LIST_PACKAGE_PATH_RE, line)
+    if match:
+      path = match.group(1)
+      package_name = match.group(2)
+      if package_name == package:
+        paths.append(path)
+  return paths
+
+
+def _GetSystemPath(package, paths):
+  for p in paths:
+    if p.startswith(SPECIAL_SYSTEM_APP_LOCATIONS.get(package, '/system/')):
+      return p
+  return None
 
 
 _ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps'
@@ -84,6 +127,12 @@
     yield
     return
 
+  # All calls that could potentially need root should run with as_root=True, but
+  # it looks like some parts of Telemetry work as-is by implicitly assuming that
+  # root is already granted if it's necessary. The reboot can mess with this, so
+  # as a workaround, check whether we're starting with root already, and if so,
+  # restore the device to that state at the end.
+  should_restore_root = device.HasRoot()
   device.EnableRoot()
   if not device.HasRoot():
     raise device_errors.CommandFailedError(
@@ -107,6 +156,8 @@
     device.SetProp(_ENABLE_MODIFICATION_PROP, '0')
     device.Reboot()
     device.WaitUntilFullyBooted()
+    if should_restore_root:
+      device.EnableRoot()
 
 
 @contextlib.contextmanager
@@ -136,9 +187,13 @@
 
 
 @contextlib.contextmanager
-def _TemporarilyInstallApp(device, apk):
+def _TemporarilyInstallApp(device, apk, install_timeout=None):
   """A context manager that installs an app while in scope."""
-  device.Install(apk, reinstall=True)
+  if install_timeout is None:
+    device.Install(apk, reinstall=True)
+  else:
+    device.Install(apk, reinstall=True, timeout=install_timeout)
+
   try:
     yield
   finally:
diff --git a/catapult/devil/devil/android/tools/system_app_devicetest.py b/catapult/devil/devil/android/tools/system_app_devicetest.py
index 0e8afdc..293bad1 100755
--- a/catapult/devil/devil/android/tools/system_app_devicetest.py
+++ b/catapult/devil/devil/android/tools/system_app_devicetest.py
@@ -39,7 +39,7 @@
     self._cached_apks = {}
     for o in self._original_paths:
       h = os.path.join(self._apk_cache_dir, posixpath.basename(o))
-      self._device.PullFile(o, h)
+      self._device.PullFile(o, h, timeout=60)
       self._cached_apks[h] = o
 
   def tearDown(self):
diff --git a/catapult/devil/devil/android/tools/system_app_test.py b/catapult/devil/devil/android/tools/system_app_test.py
index 1400d7e..44df7ea 100644
--- a/catapult/devil/devil/android/tools/system_app_test.py
+++ b/catapult/devil/devil/android/tools/system_app_test.py
@@ -21,6 +21,16 @@
   import mock
 
 
+_PACKAGE_NAME = 'com.android'
+_PACKAGE_PATH = '/path/to/com.android.apk'
+_PM_LIST_PACKAGES_COMMAND = ['pm', 'list', 'packages', '-f', '-u',
+                             _PACKAGE_NAME]
+_PM_LIST_PACKAGES_OUTPUT_WITH_PATH = ['package:/path/to/other=' + _PACKAGE_NAME
+                                      + '.other', 'package:' + _PACKAGE_PATH +
+                                      '=' + _PACKAGE_NAME]
+_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH = ['package:/path/to/other=' +
+                                         _PACKAGE_NAME + '.other']
+
 class SystemAppTest(unittest.TestCase):
 
   def testDoubleEnableModification(self):
@@ -64,6 +74,62 @@
     mock_device.SetProp.assert_called_once_with(
         system_app._ENABLE_MODIFICATION_PROP, '0')
 
+  def test_GetApplicationPaths_found(self):
+    """Path found in output along with another package having similar name."""
+    # pylint: disable=protected-access
+    mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+    mock_device.RunShellCommand.configure_mock(
+        return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
+    )
+
+    paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+    self.assertEquals([_PACKAGE_PATH], paths)
+    mock_device.RunShellCommand.assert_called_once_with(
+        _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+  def test_GetApplicationPaths_notFound(self):
+    """Path not found in output, only another package with similar name."""
+    # pylint: disable=protected-access
+    mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+    mock_device.RunShellCommand.configure_mock(
+        return_value=_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH
+    )
+
+    paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+    self.assertEquals([], paths)
+    mock_device.RunShellCommand.assert_called_once_with(
+        _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+  def test_GetApplicationPaths_noPaths(self):
+    """Nothing containing text of package name found in output."""
+    # pylint: disable=protected-access
+    mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+    mock_device.RunShellCommand.configure_mock(
+        return_value=[]
+    )
+
+    paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+    self.assertEquals([], paths)
+    mock_device.RunShellCommand.assert_called_once_with(
+        _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+  def test_GetApplicationPaths_emptyName(self):
+    """Called with empty name, should not return any packages."""
+    # pylint: disable=protected-access
+    mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+    mock_device.RunShellCommand.configure_mock(
+        return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
+    )
+
+    paths = system_app._GetApplicationPaths(mock_device, '')
+
+    self.assertEquals([], paths)
+    mock_device.RunShellCommand.assert_called_once_with(
+        _PM_LIST_PACKAGES_COMMAND[:-1] + [''], check_return=True)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/catapult/devil/devil/android/tools/webview_app.py b/catapult/devil/devil/android/tools/webview_app.py
new file mode 100755
index 0000000..36b7039
--- /dev/null
+++ b/catapult/devil/devil/android/tools/webview_app.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A script to use a package as the WebView provider while running a command."""
+
+import argparse
+import contextlib
+import logging
+import os
+import re
+import sys
+
+
+if __name__ == '__main__':
+  sys.path.append(
+      os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                   '..', '..', '..')))
+  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
+      '..', '..', '..', '..', 'common', 'py_utils')))
+
+
+from devil.android import apk_helper
+from devil.android import device_errors
+from devil.android.sdk import version_codes
+from devil.android.tools import script_common
+from devil.android.tools import system_app
+from devil.utils import cmd_helper
+from devil.utils import parallelizer
+from devil.utils import run_tests_helper
+from py_utils import tempfile_ext
+
+logger = logging.getLogger(__name__)
+
+_SYSTEM_PATH_RE = re.compile(r'^\s*\/system\/')
+_WEBVIEW_INSTALL_TIMEOUT = 300
+
+@contextlib.contextmanager
+def UseWebViewProvider(device, apk, expected_package=''):
+  """A context manager that uses the apk as the webview provider while in scope.
+
+  Args:
+    device: (device_utils.DeviceUtils) The device for which the webview apk
+      should be used as the provider.
+    apk: (str) The path to the webview APK to use.
+    expected_package: (str) If non-empty, verify apk's package name matches
+                      this value.
+  """
+  package_name = apk_helper.GetPackageName(apk)
+
+  if expected_package:
+    if package_name != expected_package:
+      raise device_errors.CommandFailedError(
+          'WebView Provider package %s does not match expected %s' %
+          (package_name, expected_package), str(device))
+
+  if (device.build_version_sdk in
+      [version_codes.NOUGAT, version_codes.NOUGAT_MR1]):
+    logger.warning('Due to webviewupdate bug in Nougat, WebView Fallback Logic '
+                   'will be disabled and WebView provider may be changed after '
+                   'exit of UseWebViewProvider context manager scope.')
+
+  webview_update = device.GetWebViewUpdateServiceDump()
+  original_fallback_logic = webview_update.get('FallbackLogicEnabled', None)
+  original_provider = webview_update.get('CurrentWebViewPackage', None)
+
+  # This is only necessary if the provider is a fallback provider, but we can't
+  # generally determine this, so we set this just in case.
+  device.SetWebViewFallbackLogic(False)
+
+  try:
+    # If user installed versions of the package is present, they must be
+    # uninstalled first, so that the system version of the package,
+    # if any, can be found by the ReplaceSystemApp context manager
+    with _UninstallNonSystemApp(device, package_name):
+      all_paths = device.GetApplicationPaths(package_name)
+      system_paths = _FilterPaths(all_paths, True)
+      non_system_paths = _FilterPaths(all_paths, False)
+      if non_system_paths:
+        raise device_errors.CommandFailedError(
+            'Non-System application paths found after uninstallation: ',
+            str(non_system_paths))
+      elif system_paths:
+        # app is system app, use ReplaceSystemApp to install
+        with system_app.ReplaceSystemApp(
+            device,
+            package_name,
+            apk,
+            install_timeout=_WEBVIEW_INSTALL_TIMEOUT):
+          _SetWebViewProvider(device, package_name)
+          yield
+      else:
+        # app is not present on device, can directly install
+        with _InstallApp(device, apk):
+          _SetWebViewProvider(device, package_name)
+          yield
+  finally:
+    # restore the original provider only if it was known and not the current
+    # provider
+    if original_provider is not None:
+      webview_update = device.GetWebViewUpdateServiceDump()
+      new_provider = webview_update.get('CurrentWebViewPackage', None)
+      if new_provider != original_provider:
+        device.SetWebViewImplementation(original_provider)
+
+    # enable the fallback logic only if it was known to be enabled
+    if original_fallback_logic is True:
+      device.SetWebViewFallbackLogic(True)
+
+
+def _SetWebViewProvider(device, package_name):
+  """ Set the WebView provider to the package_name if supported. """
+  if device.build_version_sdk >= version_codes.NOUGAT:
+    device.SetWebViewImplementation(package_name)
+
+
+def _FilterPaths(path_list, is_system):
+  """ Return paths in the path_list that are/aren't system paths. """
+  return [
+      p for p in path_list if is_system == bool(re.match(_SYSTEM_PATH_RE, p))
+  ]
+
+
+def _RebasePath(new_root, old_root):
+  """ Graft old_root onto new_root and return the result. """
+  return os.path.join(new_root, os.path.relpath(old_root, '/'))
+
+
+@contextlib.contextmanager
+def _UninstallNonSystemApp(device, package_name):
+  """ Make package un-installed while in scope. """
+  all_paths = device.GetApplicationPaths(package_name)
+  user_paths = _FilterPaths(all_paths, False)
+  host_paths = []
+  if user_paths:
+    with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
+      for user_path in user_paths:
+        host_path = _RebasePath(temp_dir, user_path)
+        # PullFile takes care of host_path creation if needed.
+        device.PullFile(user_path, host_path)
+        host_paths.append(host_path)
+      device.Uninstall(package_name)
+      try:
+        yield
+      finally:
+        for host_path in reversed(host_paths):
+          device.Install(host_path, reinstall=True,
+                         timeout=_WEBVIEW_INSTALL_TIMEOUT)
+  else:
+    yield
+
+
+@contextlib.contextmanager
+def _InstallApp(device, apk):
+  """ Make apk installed while in scope. """
+  package_name = apk_helper.GetPackageName(apk)
+  device.Install(apk, reinstall=True, timeout=_WEBVIEW_INSTALL_TIMEOUT)
+  try:
+    yield
+  finally:
+    device.Uninstall(package_name)
+
+
+def main(raw_args):
+  parser = argparse.ArgumentParser()
+
+  def add_common_arguments(p):
+    script_common.AddDeviceArguments(p)
+    script_common.AddEnvironmentArguments(p)
+    p.add_argument(
+        '-v', '--verbose', action='count', default=0,
+        help='Print more information.')
+    p.add_argument('command', nargs='*')
+
+  @contextlib.contextmanager
+  def use_webview_provider(device, args):
+    with UseWebViewProvider(device, args.apk, args.expected_package):
+      yield
+
+  parser.add_argument(
+      '--apk', required=True,
+      help='The apk to use as the provider.')
+  parser.add_argument(
+      '--expected-package', default='',
+      help="Verify apk's package name matches value, disabled by default.")
+  add_common_arguments(parser)
+  parser.set_defaults(func=use_webview_provider)
+
+  args = parser.parse_args(raw_args)
+
+  run_tests_helper.SetLogLevel(args.verbose)
+  script_common.InitializeEnvironment(args)
+
+  devices = script_common.GetDevices(args.devices, args.blacklist_file)
+  parallel_devices = parallelizer.SyncParallelizer(
+      [args.func(d, args) for d in devices])
+  with parallel_devices:
+    if args.command:
+      return cmd_helper.Call(args.command)
+    return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))
diff --git a/catapult/devil/devil/devil_dependencies.json b/catapult/devil/devil/devil_dependencies.json
index 8a7943e..8b39788 100644
--- a/catapult/devil/devil/devil_dependencies.json
+++ b/catapult/devil/devil/devil_dependencies.json
@@ -6,7 +6,7 @@
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
         "linux2_x86_64": {
-          "cloud_storage_hash": "16ba3180141a2489d7ec99b39fd6e3434a9a373f",
+          "cloud_storage_hash": "87bd288daab30624e41faa62aa2c1d5bac3e60aa",
           "download_path": "../bin/deps/linux2/x86_64/bin/aapt"
         }
       }
@@ -26,8 +26,8 @@
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
         "linux2_x86_64": {
-          "cloud_storage_hash": "91cdce1e3bd81b2ac1fd380013896d0e2cdb40a0",
-          "download_path": "../bin/deps/linux2/x86_64/lib/libc++.so"
+          "cloud_storage_hash": "9b986774ad27288a6777ebfa9a08fd8a52003008",
+          "download_path": "../bin/deps/linux2/x86_64/lib64/libc++.so"
         }
       }
     },
@@ -46,7 +46,7 @@
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
         "linux2_x86_64": {
-          "cloud_storage_hash": "acfb10f7a868baf9bcf446a2d9f8ed6b5d52c3c6",
+          "cloud_storage_hash": "c3fdf75afe8eb4062d66703cb556ee1e2064b8ae",
           "download_path": "../bin/deps/linux2/x86_64/bin/dexdump"
         }
       }
@@ -55,13 +55,13 @@
       "cloud_storage_base_folder": "binary_dependencies",
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
-        "android_armeabi-v7a": {
-          "cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
-          "download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
-        },
         "android_arm64-v8a": {
           "cloud_storage_hash": "34e583c631a495afbba82ce8a1d4f9b5118a4411",
           "download_path": "../bin/deps/android/arm64-v8a/apks/EmptySystemWebView.apk"
+        },
+        "android_armeabi-v7a": {
+          "cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
+          "download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
         }
       }
     },
@@ -132,7 +132,7 @@
       "cloud_storage_bucket": "chromium-telemetry",
       "file_info": {
         "linux2_x86_64": {
-          "cloud_storage_hash": "abb9753a8d3efeea4144e328933931729e01571c",
+          "cloud_storage_hash": "c116fd0d7ff089561971c078317b75b90f053207",
           "download_path": "../bin/deps/linux2/x86_64/bin/split-select"
         }
       }
diff --git a/catapult/devil/devil/utils/cmd_helper.py b/catapult/devil/devil/utils/cmd_helper.py
index b7b2f0d..3c4a06e 100644
--- a/catapult/devil/devil/utils/cmd_helper.py
+++ b/catapult/devil/devil/utils/cmd_helper.py
@@ -4,6 +4,7 @@
 
 """A wrapper for subprocess to make calling shell commands easier."""
 
+import codecs
 import logging
 import os
 import pipes
@@ -15,11 +16,16 @@
 import sys
 import time
 
+from devil import base_error
 
 logger = logging.getLogger(__name__)
 
 _SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
 
+# Cache the string-escape codec to ensure subprocess can find it
+# later. Return value doesn't matter.
+codecs.lookup('string-escape')
+
 
 def SingleQuote(s):
   """Return an shell-escaped version of the string using single quotes.
@@ -231,11 +237,11 @@
   return (pipe.returncode, stdout, stderr)
 
 
-class TimeoutError(Exception):
+class TimeoutError(base_error.BaseError):
   """Module-specific timeout exception."""
 
   def __init__(self, output=None):
-    super(TimeoutError, self).__init__()
+    super(TimeoutError, self).__init__('Timeout')
     self._output = output
 
   @property
diff --git a/catapult/devil/devil/utils/logging_common.py b/catapult/devil/devil/utils/logging_common.py
index 5aea3c6..ab364a2 100644
--- a/catapult/devil/devil/utils/logging_common.py
+++ b/catapult/devil/devil/utils/logging_common.py
@@ -8,13 +8,32 @@
 
 
 def AddLoggingArguments(parser):
-  parser.add_argument(
+  """Adds standard logging flags to the parser.
+
+  After parsing args, remember to invoke InitializeLogging() with the parsed
+  args, to configure the log level.
+  """
+  group = parser.add_mutually_exclusive_group()
+  group.add_argument(
       '-v', '--verbose', action='count', default=0,
       help='Log more. Use multiple times for even more logging.')
+  group.add_argument(
+      '-q', '--quiet', action='count', default=0,
+      help=('Log less (suppress output). Use multiple times for even less '
+            'output.'))
 
 
 def InitializeLogging(args, handler=None):
-  if args.verbose == 0:
+  """Initialized the log level based on commandline flags.
+
+  This expects to be given an "args" object with the options defined by
+  AddLoggingArguments().
+  """
+  if args.quiet >= 2:
+    log_level = logging.CRITICAL
+  elif args.quiet == 1:
+    log_level = logging.ERROR
+  elif args.verbose == 0:
     log_level = logging.WARNING
   elif args.verbose == 1:
     log_level = logging.INFO
diff --git a/catapult/devil/devil/utils/markdown.py b/catapult/devil/devil/utils/markdown.py
index 6867e9d..ba66664 100755
--- a/catapult/devil/devil/utils/markdown.py
+++ b/catapult/devil/devil/utils/markdown.py
@@ -3,6 +3,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+from __future__ import print_function
+
 import argparse
 import imp
 import os
@@ -221,7 +223,7 @@
   for f in functions_to_doc:
     content += md_function(f)
 
-  print '\n'.join(content)
+  print('\n'.join(content))
 
   return 0
 
diff --git a/catapult/devil/devil/utils/reraiser_thread.py b/catapult/devil/devil/utils/reraiser_thread.py
index cb9764e..6e6c810 100644
--- a/catapult/devil/devil/utils/reraiser_thread.py
+++ b/catapult/devil/devil/utils/reraiser_thread.py
@@ -11,12 +11,14 @@
 import time
 import traceback
 
+from devil import base_error
 from devil.utils import watchdog_timer
 
 
-class TimeoutError(Exception):
+class TimeoutError(base_error.BaseError):
   """Module-specific timeout exception."""
-  pass
+  def __init__(self, message):
+    super(TimeoutError, self).__init__(message)
 
 
 def LogThreadStack(thread, error_log_func=logging.critical):
@@ -67,10 +69,17 @@
     self._exc_info = None
     self._thread_group = None
 
-  def ReraiseIfException(self):
-    """Reraise exception if an exception was raised in the thread."""
-    if self._exc_info:
-      raise self._exc_info[0], self._exc_info[1], self._exc_info[2]
+  if sys.version_info < (3,):
+    # pylint: disable=exec-used
+    exec('''def ReraiseIfException(self):
+  """Reraise exception if an exception was raised in the thread."""
+  if self._exc_info:
+    raise self._exc_info[0], self._exc_info[1], self._exc_info[2]''')
+  else:
+    def ReraiseIfException(self):
+      """Reraise exception if an exception was raised in the thread."""
+      if self._exc_info:
+        raise self._exc_info[1]
 
   def GetReturnValue(self):
     """Reraise exception if present, otherwise get the return value."""
diff --git a/catapult/devil/devil/utils/reset_usb.py b/catapult/devil/devil/utils/reset_usb.py
index 0335227..404a44c 100755
--- a/catapult/devil/devil/utils/reset_usb.py
+++ b/catapult/devil/devil/utils/reset_usb.py
@@ -3,12 +3,15 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import sys
+if sys.platform == 'win32':
+  raise ImportError('devil.utils.reset_usb only supported on unix systems.')
+
 import argparse
 import fcntl
 import logging
 import os
 import re
-import sys
 
 if __name__ == '__main__':
   sys.path.append(
diff --git a/catapult/devil/devil/utils/run_tests_helper.py b/catapult/devil/devil/utils/run_tests_helper.py
index 7f71b65..0b9dd47 100644
--- a/catapult/devil/devil/utils/run_tests_helper.py
+++ b/catapult/devil/devil/utils/run_tests_helper.py
@@ -14,7 +14,7 @@
 
 
 _WrappedLoggingArgs = collections.namedtuple(
-    '_WrappedLoggingArgs', ['verbose'])
+    '_WrappedLoggingArgs', ['verbose', 'quiet'])
 
 
 def SetLogLevel(verbose_count, add_handler=True):
@@ -25,5 +25,5 @@
     add_handler: If true, adds a handler with |CustomFormatter|.
   """
   logging_common.InitializeLogging(
-      _WrappedLoggingArgs(verbose_count),
+      _WrappedLoggingArgs(verbose_count, 0),
       handler=None if add_handler else logging.NullHandler())
diff --git a/catapult/systrace/bin/adb_profile_chrome_startup b/catapult/systrace/bin/adb_profile_chrome_startup
index 6bd3270..42b37df 100755
--- a/catapult/systrace/bin/adb_profile_chrome_startup
+++ b/catapult/systrace/bin/adb_profile_chrome_startup
@@ -53,6 +53,12 @@
   parser.add_option('-t', '--time', help='Stops tracing after N seconds, 0 to '
                     'manually stop (startup trace ends after at most 5s).',
                     default=5, metavar='N', type='int', dest='trace_time')
+  parser.add_option('-c', '--chrome_categories', help='Chrome tracing '
+                    'categories to record.', default=_DEFAULT_CHROME_CATEGORIES,
+                    type='string')
+  parser.add_option('-u', '--atrace-buffer-size', help='Number of bytes to'
+                    ' be used for capturing atrace data', type='int',
+                    default=None, dest='trace_buf_size')
 
   parser.add_option_group(chrome_startup_tracing_agent.add_options(parser))
   parser.add_option_group(atrace_agent.add_options(parser))
@@ -91,7 +97,6 @@
   # removed.
   options.ring_buffer = False
   options.trace_memory = False
-  options.chrome_categories = _DEFAULT_CHROME_CATEGORIES
 
   if options.atrace_categories in ['list', 'help']:
     atrace_agent.list_categories(atrace_agent.get_config(options))
diff --git a/catapult/systrace/profile_chrome/chrome_startup_tracing_agent.py b/catapult/systrace/profile_chrome/chrome_startup_tracing_agent.py
index c1456c1..53be30c 100644
--- a/catapult/systrace/profile_chrome/chrome_startup_tracing_agent.py
+++ b/catapult/systrace/profile_chrome/chrome_startup_tracing_agent.py
@@ -40,7 +40,7 @@
   def _SetupTracing(self):
     # TODO(lizeb): Figure out how to clean up the command-line file when
     # _TearDownTracing() is not executed in StopTracing().
-    flags = ['--trace-startup']
+    flags = ['--trace-startup', '--enable-perfetto']
     if self._trace_time is not None:
       flags.append('--trace-startup-duration={}'.format(self._trace_time))
     self._flag_changer.AddFlags(flags)
diff --git a/catapult/systrace/profile_chrome/perf_tracing_agent.py b/catapult/systrace/profile_chrome/perf_tracing_agent.py
index c7ad946..1414905 100644
--- a/catapult/systrace/profile_chrome/perf_tracing_agent.py
+++ b/catapult/systrace/profile_chrome/perf_tracing_agent.py
@@ -26,7 +26,6 @@
   # pylint: disable=F0401,no-name-in-module,wrong-import-position
   from telemetry.internal.platform.profiler import android_profiling_helper
   from telemetry.internal.util import binary_manager
-  # pylint: enable=wrong-import-position
 except ImportError:
   android_profiling_helper = None
   binary_manager = None
diff --git a/catapult/systrace/profile_chrome/profiler_unittest.py b/catapult/systrace/profile_chrome/profiler_unittest.py
index 9b35753..cd7af95 100644
--- a/catapult/systrace/profile_chrome/profiler_unittest.py
+++ b/catapult/systrace/profile_chrome/profiler_unittest.py
@@ -18,7 +18,7 @@
   def setUp(self):
     ui.EnableTestMode()
     self._tracing_options = tracing_controller.TracingControllerConfig(None,
-        None, None, None, None, None, None, None, None)
+        None, None, None, None, None, None, None, None, None)
 
   @decorators.ClientOnlyTest
   def testCaptureBasicProfile(self):
diff --git a/catapult/systrace/systrace/output_generator.py b/catapult/systrace/systrace/output_generator.py
index 9b2666e..b1dc275 100644
--- a/catapult/systrace/systrace/output_generator.py
+++ b/catapult/systrace/systrace/output_generator.py
@@ -33,12 +33,13 @@
 
 
 def NewGenerateHTMLOutput(trace_results, output_file_name):
-  trace_data_builder = trace_data.TraceDataBuilder()
-  for trace in trace_results:
-    trace_data_part = _SYSTRACE_TO_TRACE_DATA_NAME_MAPPING.get(
-        trace.source_name)
-    trace_data_builder.AddTraceFor(trace_data_part, trace.raw_data)
-  trace_data_builder.AsData().Serialize(output_file_name, _SYSTRACE_HEADER)
+  with trace_data.TraceDataBuilder() as builder:
+    for trace in trace_results:
+      trace_data_part = _SYSTRACE_TO_TRACE_DATA_NAME_MAPPING.get(
+          trace.source_name)
+      builder.AddTraceFor(
+          trace_data_part, trace.raw_data, allow_unstructured=True)
+    builder.Serialize(output_file_name, _SYSTRACE_HEADER)
 
 
 def GenerateHTMLOutput(trace_results, output_file_name):
diff --git a/catapult/systrace/systrace/output_generator_unittest.py b/catapult/systrace/systrace/output_generator_unittest.py
index dba4771..2347316 100644
--- a/catapult/systrace/systrace/output_generator_unittest.py
+++ b/catapult/systrace/systrace/output_generator_unittest.py
@@ -9,11 +9,11 @@
 import os
 import unittest
 
+from py_utils import tempfile_ext
 from systrace import decorators
 from systrace import output_generator
 from systrace import trace_result
 from systrace import update_systrace_trace_viewer
-from systrace import util
 from tracing.trace_data import trace_data as trace_data_module
 
 
@@ -52,18 +52,14 @@
         update_systrace_trace_viewer.SYSTRACE_TRACE_VIEWER_HTML_FILE))
     with open(ATRACE_DATA) as f:
       atrace_data = f.read().replace(" ", "").strip()
-      trace_results = [trace_result.TraceResult('systemTraceEvents',
-                       atrace_data)]
-      output_file_name = util.generate_random_filename_for_test()
-      final_path = output_generator.GenerateHTMLOutput(trace_results,
-                                                       output_file_name)
+    trace_results = [trace_result.TraceResult('systemTraceEvents', atrace_data)]
+    with tempfile_ext.TemporaryFileName() as output_file_name:
+      output_generator.GenerateHTMLOutput(trace_results, output_file_name)
       with open(output_file_name, 'r') as f:
-        output_generator.GenerateHTMLOutput(trace_results, f.name)
         html_output = f.read()
-        trace_data = (html_output.split(
-          '<script class="trace-data" type="application/text">')[1].split(
-          '</script>'))[0].replace(" ", "").strip()
-      os.remove(final_path)
+      trace_data = (html_output.split(
+        '<script class="trace-data" type="application/text">')[1].split(
+        '</script>'))[0].replace(" ", "").strip()
 
     # Ensure the trace data written in HTML is located within the
     # correct place in the HTML document and that the data is not
@@ -74,49 +70,42 @@
   @decorators.HostOnlyTest
   def testHtmlOutputGenerationFormatsMultipleTraces(self):
     trace_results = []
-    trace_data_builder = trace_data_module.TraceDataBuilder()
+    with trace_data_module.TraceDataBuilder() as trace_data_builder:
+      with open(ATRACE_DATA) as fp:
+        atrace_data = fp.read()
+      trace_results.append(
+          trace_result.TraceResult('systemTraceEvents', atrace_data))
+      trace_data_builder.AddTraceFor(trace_data_module.ATRACE_PART, atrace_data,
+                                     allow_unstructured=True)
 
-    with open(ATRACE_DATA) as fp:
-      atrace_data = fp.read()
-    trace_results.append(
-        trace_result.TraceResult('systemTraceEvents', atrace_data))
-    trace_data_builder.AddTraceFor(trace_data_module.ATRACE_PART, atrace_data)
+      with open(ATRACE_PROCESS_DUMP_DATA) as fp:
+        atrace_process_dump_data = fp.read()
+      trace_results.append(trace_result.TraceResult(
+          'atraceProcessDump', atrace_process_dump_data))
+      trace_data_builder.AddTraceFor(trace_data_module.ATRACE_PROCESS_DUMP_PART,
+                                     atrace_process_dump_data,
+                                     allow_unstructured=True)
 
-    with open(ATRACE_PROCESS_DUMP_DATA) as fp:
-      atrace_process_dump_data = fp.read()
-    trace_results.append(
-        trace_result.TraceResult('atraceProcessDump', atrace_process_dump_data))
-    trace_data_builder.AddTraceFor(trace_data_module.ATRACE_PROCESS_DUMP_PART,
-                                   atrace_process_dump_data)
+      with open(COMBINED_PROFILE_CHROME_DATA) as fp:
+        chrome_data = json.load(fp)
+      trace_results.append(
+          trace_result.TraceResult('traceEvents', chrome_data))
+      trace_data_builder.AddTraceFor(
+          trace_data_module.CHROME_TRACE_PART, chrome_data)
 
-    with open(COMBINED_PROFILE_CHROME_DATA) as fp:
-      chrome_data = fp.read()
-    trace_results.append(
-        trace_result.TraceResult('traceEvents', json.loads(chrome_data)))
-    trace_data_builder.AddTraceFor(
-        trace_data_module.CHROME_TRACE_PART, json.loads(chrome_data))
+      trace_results.append(
+          trace_result.TraceResult('systraceController', str({})))
+      trace_data_builder.AddTraceFor(trace_data_module.TELEMETRY_PART, {})
 
-    trace_results.append(
-        trace_result.TraceResult('systraceController', str({})))
-    trace_data_builder.AddTraceFor(trace_data_module.TELEMETRY_PART, {})
+      with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
+        data_builder_out = os.path.join(temp_dir, 'data_builder.html')
+        output_generator_out = os.path.join(temp_dir, 'output_generator.html')
+        output_generator.GenerateHTMLOutput(trace_results, output_generator_out)
+        trace_data_builder.Serialize(data_builder_out, 'Systrace')
 
-    try:
-      data_builder_out = util.generate_random_filename_for_test()
-      output_generator_out = util.generate_random_filename_for_test()
-      output_generator.GenerateHTMLOutput(trace_results, output_generator_out)
-      trace_data_builder.AsData().Serialize(data_builder_out, 'Systrace')
+        output_generator_md5sum = hashlib.md5(
+            open(output_generator_out, 'rb').read()).hexdigest()
+        data_builder_md5sum = hashlib.md5(
+            open(data_builder_out, 'rb').read()).hexdigest()
 
-      output_generator_md5sum = hashlib.md5(
-          open(output_generator_out, 'rb').read()).hexdigest()
-      data_builder_md5sum = hashlib.md5(
-          open(data_builder_out, 'rb').read()).hexdigest()
-
-      self.assertEqual(output_generator_md5sum, data_builder_md5sum)
-    finally:
-      def del_if_exist(path):
-        try:
-          os.remove(path)
-        except IOError:
-          pass
-      del_if_exist(output_generator_out)
-      del_if_exist(data_builder_out)
+        self.assertEqual(output_generator_md5sum, data_builder_md5sum)
diff --git a/catapult/systrace/systrace/run_systrace.py b/catapult/systrace/systrace/run_systrace.py
index 2ca8504..15a39a3 100755
--- a/catapult/systrace/systrace/run_systrace.py
+++ b/catapult/systrace/systrace/run_systrace.py
@@ -47,7 +47,6 @@
 from systrace.tracing_agents import atrace_process_dump
 from systrace.tracing_agents import ftrace_agent
 from systrace.tracing_agents import walt_agent
-# pylint: enable=wrong-import-position
 
 
 ALL_MODULES = [atrace_agent, atrace_from_file_agent, atrace_process_dump,
diff --git a/catapult/systrace/systrace/systrace_trace_viewer.html b/catapult/systrace/systrace/systrace_trace_viewer.html
index bdb0cd9..ad9472e 100644
--- a/catapult/systrace/systrace/systrace_trace_viewer.html
+++ b/catapult/systrace/systrace/systrace_trace_viewer.html
@@ -108,7 +108,7 @@
       text-decoration: underline;
     }
     </style>
-    <a href="{{href}}" on-click="onClicked_" on-mouseenter="onMouseEnter_" on-mouseleave="onMouseLeave_"><content></content></a>
+    <a href="{{href}}" on-click="onClicked_" on-mouseenter="onMouseEnter_" on-mouseleave="onMouseLeave_"><slot></slot></a>
 
   </template>
 </dom-module><dom-module id="tr-ui-b-table">
@@ -956,6 +956,7 @@
     </style>
     <div style="padding-right: 200px">
       <div style="float:right;  border-style: solid; border-width: 1px; padding:20px">
+        X no feedback<br/>
         0 uninitialized<br/>
         . premonomorphic<br/>
         1 monomorphic<br/>
@@ -1113,8 +1114,8 @@
       </template>
     </div>
     <div id="subView"></div>
-    <content>
-    </content>
+    <slot>
+    </slot>
   </template>
 </dom-module><dom-module id="tr-ui-a-memory-dump-heap-details-breakdown-view">
   <template>
@@ -1552,25 +1553,10 @@
     <div id="links"></div>
   </template>
 </dom-module><dom-module id="tr-v-ui-related-event-set-span">
-</dom-module><dom-module id="tr-v-ui-related-histogram-map-span">
-  <template>
-    <tr-ui-b-table id="table"></tr-ui-b-table>
-  </template>
 </dom-module><dom-module id="tr-v-ui-scalar-diagnostic-span">
   <template>
     <tr-v-ui-scalar-span id="scalar"></tr-v-ui-scalar-span>
   </template>
-</dom-module><dom-module id="tr-v-ui-tag-map-span">
-  <template>
-    <style>
-    #hide, #generic {
-      display: none;
-    }
-    </style>
-    <button id="show" on-click="onShow_">Show</button>
-    <button id="hide" on-click="onHide_">Hide</button>
-    <tr-ui-a-generic-object-view id="generic"></tr-ui-a-generic-object-view>
-  </template>
 </dom-module><dom-module id="tr-v-ui-unmergeable-diagnostic-set-span">
 </dom-module><dom-module id="tr-v-ui-diagnostic-map-table">
   <template>
@@ -2052,7 +2038,7 @@
       vertical-align: top;
     }
     </style>
-    <content></content>
+    <slot></slot>
   </template>
 </dom-module><dom-module id="tr-ui-a-power-sample-table">
   <template>
@@ -2204,30 +2190,30 @@
         height: 525px;
       }
     </style>
-    <content></content>
+    <slot></slot>
   </template>
 </dom-module><dom-module id="tr-ui-b-dropdown">
   <template>
     <style>
     button {
-      @apply(--dropdown-button);
+      @apply --dropdown-button;
     }
     button.open {
-      @apply(--dropdown-button-open);
+      @apply --dropdown-button-open;
     }
     dialog {
       position: absolute;
       margin: 0;
       padding: 1em;
       border: 1px solid darkgrey;
-      @apply(--dropdown-dialog);
+      @apply --dropdown-dialog;
     }
     </style>
 
     <button id="button" on-tap="open">[[label]]</button>
 
     <dialog id="dialog" on-cancel="close" on-tap="onDialogTap_">
-      <content></content>
+      <slot></slot>
     </dialog>
   </template>
 </dom-module><dom-module id="tr-ui-b-info-bar-group">
@@ -2268,7 +2254,7 @@
     }
     </style>
     <div id="aligner">
-      <content></content>
+      <slot></slot>
     </div>
   </template>
 </dom-module><style>
@@ -2338,7 +2324,7 @@
       font-size: 8pt;
     }
     </style>
-    <content></content>
+    <slot></slot>
 
     <div id="drag_box"></div>
     <div id="hint_text"></div>
@@ -2881,7 +2867,7 @@
       </tr-ui-b-info-bar-group>
     </div>
     <middle-container>
-      <content></content>
+      <slot></slot>
 
       <tr-ui-side-panel-container id="side_panel_container">
       </tr-ui-side-panel-container>
@@ -3378,14 +3364,16 @@
         padding: 4px;
         font-size: 14px;
       }
+
       .error {
         color: red;
         display: none;
       }
 
-      .child_container {
+      .container {
         position: relative;
         display: inline-block;
+        margin-left: 15px;
       }
 
       #title {
@@ -3395,9 +3383,8 @@
       }
 
       #selectors {
-        display: none;
+        display: block;
         padding-bottom: 10px;
-        display: none;
       }
 
       #search_page {
@@ -3427,27 +3414,157 @@
         stroke: white;
       }
     </style>
-    <div id="title">Metrics Visualization</div>
-    <div class="error" id="data_error">Invalid data provided.</div>
-    <div id="selectors">
-      <div id="percentile_label">Percentile Range:</div>
-      <input class="text_input" id="start" placeholder="0"/>
-      <input class="text_input" id="end" placeholder="100"/>
-      <button id="filter" on-tap="filterByPercentile_">Filter</button>
-      <input class="text_input" id="search_page" placeholder="Page Name"/>
-      <button id="search" on-tap="searchByPage_">Search</button>
-      <div class="error" id="search_error">Sorry, could not find that page!</div>
-    </div>
-    <div id="container">
-    </div>
-    <div id="children">
-    </div>
-    <span id="close">
-      <svg viewBox="0 0 128 128">
-        <line x1="28" x2="100" y1="28" y2="100"></line>
-        <line x1="28" x2="100" y1="100" y2="28"></line>
-      </svg>
+      <span class="container" id="aggregateContainer">
+      </span>
+      <span class="container" id="pageByPageContainer">
+        <span id="selectors">
+          <span id="percentile_label">Percentile Range:</span>
+          <input class="text_input" id="start" placeholder="0"/>
+          <input class="text_input" id="end" placeholder="100"/>
+          <button id="filter" on-tap="filterByPercentile_">Filter</button>
+          <input class="text_input" id="search_page" placeholder="Page Name"/>
+          <button id="search" on-tap="searchByPage_">Search</button>
+          <span class="error" id="search_error">Sorry, could not find that page!</span>
+        </span>
+      </span>
+      <div display="block" id="submetricsContainer">
+        <span id="close">
+          <svg viewBox="0 0 128 128">
+            <line x1="28" x2="100" y1="28" y2="100"></line>
+            <line x1="28" x2="100" y1="100" y2="28"></line>
+          </svg>
+        </span>
+      </div>
+  </template>
+</dom-module><dom-module id="tr-v-ui-raster-visualization">
+  <template>
+    <style>
+      button {
+        padding: 5px;
+        font-size: 14px;
+      }
+      .error {
+        color: red;
+        display: none;
+      }
+
+      .text_input {
+        width: 200px;
+        padding: 4px;
+        font-size: 14px;
+      }
+
+      .selector_container{
+        padding: 5px;
+      }
+
+      #search {
+        display: inline-block;
+        padding-bottom: 10px;
+      }
+
+      #search_page {
+        width: 200px;
+      }
+
+      #pageSelector {
+        display: inline-block;
+        font-size: 12pt;
+      }
+
+      #close {
+        display: none;
+        vertical-align: top;
+      }
+
+      #close svg{
+        height: 1em;
+      }
+
+      #close svg line {
+        stroke-width: 18;
+        stroke: black;
+      }
+
+      #close:hover svg {
+        background: black;
+      }
+
+      #close:hover svg line {
+        stroke: white;
+      }
+    </style>
+    <span id="aggregateContainer">
+      <div>
+        <div class="selector_container">
+          <span id="select_page_label">Individual Page Results:</span>
+          <select id="pageSelector">
+            <option id="select_page" value="">Select a page</option>
+          </select>
+        </div>
+        <div class="selector_container">
+          <div id="search_page_label">Search for a page:</div>
+          <input class="text_input" id="search_page" placeholder="Page Name"/>
+          <button id="search_button">Search</button>
+          <div class="error" id="search_error">Sorry, could not find that page!</div>
+        </div>
+      </div>
     </span>
+    <span id="pageContainer">
+      <span id="close">
+          <svg viewBox="0 0 128 128">
+            <line x1="28" x2="100" y1="28" y2="100"></line>
+            <line x1="28" x2="100" y1="100" y2="28"></line>
+          </svg>
+        </span>
+      </span>
+  </template>
+</dom-module><meta charset="utf-8"/><dom-module id="tr-v-ui-visualizations-data-container">
+  <template>
+    <style>
+      .error {
+        color: red;
+        display: none;
+      }
+
+      .sample{
+        display: none;
+      }
+
+      .subtitle{
+        font-size: 20px;
+        font-weight: bold;
+        padding-bottom: 5px;
+      }
+
+      .description{
+        font-size: 15px;
+        padding-bottom: 5px;
+      }
+
+      #title {
+        font-size: 30px;
+        font-weight: bold;
+        padding-bottom: 5px;
+      }
+    </style>
+    <div id="title">Visualizations</div>
+    <div class="error" id="data_error">Invalid data provided.</div>
+    <div id="pipeline_per_frame_container">
+      <div class="subtitle">Graphics Pipeline and Raster Tasks</div>
+      <div class="description">
+        When raster tasks are completed in comparison to the rest of the graphics pipeline.<br/>
+        Only pages where raster tasks are completed after beginFrame is issued are included.
+      </div>
+      <tr-v-ui-raster-visualization id="rasterVisualization">
+      </tr-v-ui-raster-visualization>
+    </div>
+    <div id="metrics_container">
+      <div class="subtitle">Metrics</div>
+      <div class="description">Total amount of time taken for the indicated metrics.</div>
+      <tr-v-ui-metrics-visualization class="sample" id="metricsVisualization">
+      </tr-v-ui-metrics-visualization>
+    </div>
   </template>
 </dom-module><dom-module id="tr-v-ui-histogram-set-view">
   <template>
@@ -3469,6 +3586,10 @@
     #container {
       display: none;
     }
+
+    #visualizations{
+      display: none;
+    }
     </style>
 
     <div id="zero">zero Histograms</div>
@@ -3477,8 +3598,8 @@
       <tr-v-ui-histogram-set-controls id="controls">
       </tr-v-ui-histogram-set-controls>
 
-      <tr-v-ui-metrics-visualization id="metrics">
-      </tr-v-ui-metrics-visualization>
+      <tr-v-ui-visualizations-data-container id="visualizations">
+      </tr-v-ui-visualizations-data-container>
 
       <tr-v-ui-histogram-set-table id="table"></tr-v-ui-histogram-set-table>
     </div>
@@ -3590,7 +3711,7 @@
 break;case Array:try{value=JSON.parse(value);}catch(x){value=null;console.warn('Polymer::Attributes: couldn`t decode Array as JSON');}
 break;case Date:value=new Date(value);break;case String:default:break;}
 return value;},serialize:function(value){switch(typeof value){case'boolean':return value?'':undefined;case'object':if(value instanceof Date){return value.toString();}else if(value){try{return JSON.stringify(value);}catch(x){return'';}}
-default:return value!=null?value:undefined;}}});Polymer.version="1.10.1";Polymer.Base._addFeature({_registerFeatures:function(){this._prepIs();this._prepBehaviors();this._prepConstructor();this._prepPropertyInfo();},_prepBehavior:function(b){this._addHostAttributes(b.hostAttributes);},_marshalBehavior:function(b){},_initFeatures:function(){this._marshalHostAttributes();this._marshalBehaviors();}});(function(){function resolveCss(cssText,ownerDocument){return cssText.replace(CSS_URL_RX,function(m,pre,url,post){return pre+'\''+resolve(url.replace(/["']/g,''),ownerDocument)+'\''+post;});}
+default:return value!=null?value:undefined;}}});Polymer.version="1.11.3";Polymer.Base._addFeature({_registerFeatures:function(){this._prepIs();this._prepBehaviors();this._prepConstructor();this._prepPropertyInfo();},_prepBehavior:function(b){this._addHostAttributes(b.hostAttributes);},_marshalBehavior:function(b){},_initFeatures:function(){this._marshalHostAttributes();this._marshalBehaviors();}});(function(){function resolveCss(cssText,ownerDocument){return cssText.replace(CSS_URL_RX,function(m,pre,url,post){return pre+'\''+resolve(url.replace(/["']/g,''),ownerDocument)+'\''+post;});}
 function resolveAttrs(element,ownerDocument){for(var name in URL_ATTRS){var a$=URL_ATTRS[name];for(var i=0,l=a$.length,a,at,v;i<l&&(a=a$[i]);i++){if(name==='*'||element.localName===name){at=element.attributes[a];v=at&&at.value;if(v&&v.search(BINDING_RX)<0){at.value=a==='style'?resolveCss(v,ownerDocument):resolve(v,ownerDocument);}}}}}
 function resolve(url,ownerDocument){if(url&&ABS_URL.test(url)){return url;}
 var resolver=getUrlResolver(ownerDocument);resolver.href=url;return resolver.href||url;}
@@ -3793,9 +3914,10 @@
 var key=this._boundListenerKey(eventName,methodName);bl[key]=handler;},_recallEventHandler:function(host,eventName,target,methodName){var hbl=host.__boundListeners;if(!hbl){return;}
 var bl=hbl.get(target);if(!bl){return;}
 var key=this._boundListenerKey(eventName,methodName);return bl[key];},_createEventHandler:function(node,eventName,methodName){var host=this;var handler=function(e){if(host[methodName]){host[methodName](e,e.detail);}else{host._warn(host._logf('_createEventHandler','listener method `'+methodName+'` not defined'));}};handler._listening=false;this._recordEventHandler(host,eventName,node,methodName,handler);return handler;},unlisten:function(node,eventName,methodName){var handler=this._recallEventHandler(this,eventName,node,methodName);if(handler){this._unlisten(node,eventName,handler);handler._listening=false;}},_listen:function(node,eventName,handler){node.addEventListener(eventName,handler);},_unlisten:function(node,eventName,handler){node.removeEventListener(eventName,handler);}});(function(){'use strict';var wrap=Polymer.DomApi.wrap;var HAS_NATIVE_TA=typeof document.head.style.touchAction==='string';var GESTURE_KEY='__polymerGestures';var HANDLED_OBJ='__polymerGesturesHandled';var TOUCH_ACTION='__polymerGesturesTouchAction';var TAP_DISTANCE=25;var TRACK_DISTANCE=5;var TRACK_LENGTH=2;var MOUSE_TIMEOUT=2500;var MOUSE_EVENTS=['mousedown','mousemove','mouseup','click'];var MOUSE_WHICH_TO_BUTTONS=[0,1,4,2];var MOUSE_HAS_BUTTONS=function(){try{return new MouseEvent('test',{buttons:1}).buttons===1;}catch(e){return false;}}();function isMouseEvent(name){return MOUSE_EVENTS.indexOf(name)>-1;}
-var SUPPORTS_PASSIVE=false;(function(){try{var opts=Object.defineProperty({},'passive',{get:function(){SUPPORTS_PASSIVE=true;}});window.addEventListener('test',null,opts);window.removeEventListener('test',null,opts);}catch(e){}}());function PASSIVE_TOUCH(){if(HAS_NATIVE_TA&&SUPPORTS_PASSIVE&&Polymer.Settings.passiveTouchGestures){return{passive:true};}}
+var SUPPORTS_PASSIVE=false;(function(){try{var opts=Object.defineProperty({},'passive',{get:function(){SUPPORTS_PASSIVE=true;}});window.addEventListener('test',null,opts);window.removeEventListener('test',null,opts);}catch(e){}}());function PASSIVE_TOUCH(eventName){if(isMouseEvent(eventName)||eventName==='touchend'){return;}
+if(HAS_NATIVE_TA&&SUPPORTS_PASSIVE&&Polymer.Settings.passiveTouchGestures){return{passive:true};}}
 var IS_TOUCH_ONLY=navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/);var mouseCanceller=function(mouseEvent){var sc=mouseEvent.sourceCapabilities;if(sc&&!sc.firesTouchEvents){return;}
-mouseEvent[HANDLED_OBJ]={skip:true};if(mouseEvent.type==='click'){var path=Polymer.dom(mouseEvent).path;for(var i=0;i<path.length;i++){if(path[i]===POINTERSTATE.mouse.target){return;}}
+mouseEvent[HANDLED_OBJ]={skip:true};if(mouseEvent.type==='click'){var path=Polymer.dom(mouseEvent).path;if(path){for(var i=0;i<path.length;i++){if(path[i]===POINTERSTATE.mouse.target){return;}}}
 mouseEvent.preventDefault();mouseEvent.stopPropagation();}};function setupTeardownMouseCanceller(setup){var events=IS_TOUCH_ONLY?['click']:MOUSE_EVENTS;for(var i=0,en;i<events.length;i++){en=events[i];if(setup){document.addEventListener(en,mouseCanceller,true);}else{document.removeEventListener(en,mouseCanceller,true);}}}
 function ignoreMouse(ev){if(!POINTERSTATE.mouse.mouseIgnoreJob){setupTeardownMouseCanceller(true);}
 var unset=function(){setupTeardownMouseCanceller();POINTERSTATE.mouse.target=null;POINTERSTATE.mouse.mouseIgnoreJob=null;};POINTERSTATE.mouse.target=Polymer.dom(ev).rootTarget;POINTERSTATE.mouse.mouseIgnoreJob=Polymer.Debounce(POINTERSTATE.mouse.mouseIgnoreJob,unset,MOUSE_TIMEOUT);}
@@ -3823,9 +3945,9 @@
 if(prevent){ev.preventDefault();}else{Gestures.prevent('track');}}},add:function(node,evType,handler){node=wrap(node);var recognizer=this.gestures[evType];var deps=recognizer.deps;var name=recognizer.name;var gobj=node[GESTURE_KEY];if(!gobj){node[GESTURE_KEY]=gobj={};}
 for(var i=0,dep,gd;i<deps.length;i++){dep=deps[i];if(IS_TOUCH_ONLY&&isMouseEvent(dep)&&dep!=='click'){continue;}
 gd=gobj[dep];if(!gd){gobj[dep]=gd={_count:0};}
-if(gd._count===0){var options=!isMouseEvent(dep)&&PASSIVE_TOUCH();node.addEventListener(dep,this.handleNative,options);}
+if(gd._count===0){node.addEventListener(dep,this.handleNative,PASSIVE_TOUCH(dep));}
 gd[name]=(gd[name]||0)+1;gd._count=(gd._count||0)+1;}
-node.addEventListener(evType,handler);if(recognizer.touchAction){this.setTouchAction(node,recognizer.touchAction);}},remove:function(node,evType,handler){node=wrap(node);var recognizer=this.gestures[evType];var deps=recognizer.deps;var name=recognizer.name;var gobj=node[GESTURE_KEY];if(gobj){for(var i=0,dep,gd;i<deps.length;i++){dep=deps[i];gd=gobj[dep];if(gd&&gd[name]){gd[name]=(gd[name]||1)-1;gd._count=(gd._count||1)-1;if(gd._count===0){var options=!isMouseEvent(dep)&&PASSIVE_TOUCH();node.removeEventListener(dep,this.handleNative,options);}}}}
+node.addEventListener(evType,handler);if(recognizer.touchAction){this.setTouchAction(node,recognizer.touchAction);}},remove:function(node,evType,handler){node=wrap(node);var recognizer=this.gestures[evType];var deps=recognizer.deps;var name=recognizer.name;var gobj=node[GESTURE_KEY];if(gobj){for(var i=0,dep,gd;i<deps.length;i++){dep=deps[i];gd=gobj[dep];if(gd&&gd[name]){gd[name]=(gd[name]||1)-1;gd._count=(gd._count||1)-1;if(gd._count===0){node.removeEventListener(dep,this.handleNative,PASSIVE_TOUCH(dep));}}}}
 node.removeEventListener(evType,handler);},register:function(recog){this.recognizers.push(recog);for(var i=0;i<recog.emits.length;i++){this.gestures[recog.emits[i]]=recog;}},findRecognizerByEvent:function(evName){for(var i=0,r;i<this.recognizers.length;i++){r=this.recognizers[i];for(var j=0,n;j<r.emits.length;j++){n=r.emits[j];if(n===evName){return r;}}}
 return null;},setTouchAction:function(node,value){if(HAS_NATIVE_TA){node.style.touchAction=value;}
 node[TOUCH_ACTION]=value;},fire:function(target,type,detail){var ev=Polymer.Base.fire(type,detail,{node:target,bubbles:true,cancelable:true});if(ev.defaultPrevented){var preventer=detail.preventer||detail.sourceEvent;if(preventer&&preventer.preventDefault){preventer.preventDefault();}}},prevent:function(evName){var recognizer=this.findRecognizerByEvent(evName);if(recognizer.info){recognizer.info.prevent=true;}},resetMouseCanceller:function(){if(POINTERSTATE.mouse.mouseIgnoreJob){POINTERSTATE.mouse.mouseIgnoreJob.complete();}}};Gestures.register({name:'downup',deps:['mousedown','touchstart','touchend'],flow:{start:['mousedown','touchstart'],end:['mouseup','touchend']},emits:['down','up'],info:{movefn:null,upfn:null},reset:function(){untrackDocument(this.info);},mousedown:function(e){if(!hasLeftMouseButton(e)){return;}
@@ -3917,7 +4039,7 @@
 return'\\'+code;});},stringify:function(node,preserveProperties,text){text=text||'';var cssText='';if(node.cssText||node.rules){var r$=node.rules;if(r$&&!this._hasMixinRules(r$)){for(var i=0,l=r$.length,r;i<l&&(r=r$[i]);i++){cssText=this.stringify(r,preserveProperties,cssText);}}else{cssText=preserveProperties?node.cssText:this.removeCustomProps(node.cssText);cssText=cssText.trim();if(cssText){cssText='  '+cssText+'\n';}}}
 if(cssText){if(node.selector){text+=node.selector+' '+this.OPEN_BRACE+'\n';}
 text+=cssText;if(node.selector){text+=this.CLOSE_BRACE+'\n\n';}}
-return text;},_hasMixinRules:function(rules){return rules[0].selector.indexOf(this.VAR_START)===0;},removeCustomProps:function(cssText){cssText=this.removeCustomPropAssignment(cssText);return this.removeCustomPropApply(cssText);},removeCustomPropAssignment:function(cssText){return cssText.replace(this._rx.customProp,'').replace(this._rx.mixinProp,'');},removeCustomPropApply:function(cssText){return cssText.replace(this._rx.mixinApply,'').replace(this._rx.varApply,'');},types:{STYLE_RULE:1,KEYFRAMES_RULE:7,MEDIA_RULE:4,MIXIN_RULE:1000},OPEN_BRACE:'{',CLOSE_BRACE:'}',_rx:{comments:/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//gim,port:/@import[^;]*;/gim,customProp:/(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim,mixinProp:/(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim,mixinApply:/@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim,varApply:/[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim,keyframesRule:/^@[^\s]*keyframes/,multipleSpaces:/\s+/g},VAR_START:'--',MEDIA_START:'@media',AT_START:'@'};}();Polymer.StyleUtil=function(){var settings=Polymer.Settings;return{NATIVE_VARIABLES:Polymer.Settings.useNativeCSSProperties,MODULE_STYLES_SELECTOR:'style, link[rel=import][type~=css], template',INCLUDE_ATTR:'include',toCssText:function(rules,callback){if(typeof rules==='string'){rules=this.parser.parse(rules);}
+return text;},_hasMixinRules:function(rules){return rules[0].selector.indexOf(this.VAR_START)===0;},removeCustomProps:function(cssText){cssText=this.removeCustomPropAssignment(cssText);return this.removeCustomPropApply(cssText);},removeCustomPropAssignment:function(cssText){return cssText.replace(this._rx.customProp,'').replace(this._rx.mixinProp,'');},removeCustomPropApply:function(cssText){return cssText.replace(this._rx.mixinApply,'').replace(this._rx.varApply,'');},types:{STYLE_RULE:1,KEYFRAMES_RULE:7,MEDIA_RULE:4,MIXIN_RULE:1000},OPEN_BRACE:'{',CLOSE_BRACE:'}',_rx:{comments:/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//gim,port:/@import[^;]*;/gim,customProp:/(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim,mixinProp:/(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim,mixinApply:/@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim,varApply:/[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim,keyframesRule:/^@[^\s]*keyframes/,multipleSpaces:/\s+/g},VAR_START:'--',MEDIA_START:'@media',AT_START:'@'};}();Polymer.StyleUtil=function(){var settings=Polymer.Settings;return{unscopedStyleImports:new WeakMap(),SHADY_UNSCOPED_ATTR:'shady-unscoped',NATIVE_VARIABLES:Polymer.Settings.useNativeCSSProperties,MODULE_STYLES_SELECTOR:'style, link[rel=import][type~=css], template',INCLUDE_ATTR:'include',toCssText:function(rules,callback){if(typeof rules==='string'){rules=this.parser.parse(rules);}
 if(callback){this.forEachRule(rules,callback);}
 return this.parser.stringify(rules,this.NATIVE_VARIABLES);},forRulesInStyles:function(styles,styleRuleCallback,keyframesRuleCallback){if(styles){for(var i=0,l=styles.length,s;i<l&&(s=styles[i]);i++){this.forEachRuleInStyle(s,styleRuleCallback,keyframesRuleCallback);}}},forActiveRulesInStyles:function(styles,styleRuleCallback,keyframesRuleCallback){if(styles){for(var i=0,l=styles.length,s;i<l&&(s=styles[i]);i++){this.forEachRuleInStyle(s,styleRuleCallback,keyframesRuleCallback,true);}}},rulesForStyle:function(style){if(!style.__cssRules&&style.textContent){style.__cssRules=this.parser.parse(style.textContent);}
 return style.__cssRules;},isKeyframesSelector:function(rule){return rule.parent&&rule.parent.type===this.ruleTypes.KEYFRAMES_RULE;},forEachRuleInStyle:function(style,styleRuleCallback,keyframesRuleCallback,onlyActiveRules){var rules=this.rulesForStyle(style);var styleCallback,keyframeCallback;if(styleRuleCallback){styleCallback=function(rule){styleRuleCallback(rule,style);};}
@@ -3926,11 +4048,11 @@
 var skipRules=false;if(onlyActiveRules){if(node.type===this.ruleTypes.MEDIA_RULE){var matchMedia=node.selector.match(this.rx.MEDIA_MATCH);if(matchMedia){if(!window.matchMedia(matchMedia[1]).matches){skipRules=true;}}}}
 if(node.type===this.ruleTypes.STYLE_RULE){styleRuleCallback(node);}else if(keyframesRuleCallback&&node.type===this.ruleTypes.KEYFRAMES_RULE){keyframesRuleCallback(node);}else if(node.type===this.ruleTypes.MIXIN_RULE){skipRules=true;}
 var r$=node.rules;if(r$&&!skipRules){for(var i=0,l=r$.length,r;i<l&&(r=r$[i]);i++){this.forEachRule(r,styleRuleCallback,keyframesRuleCallback,onlyActiveRules);}}},applyCss:function(cssText,moniker,target,contextNode){var style=this.createScopeStyle(cssText,moniker);return this.applyStyle(style,target,contextNode);},applyStyle:function(style,target,contextNode){target=target||document.head;var after=contextNode&&contextNode.nextSibling||target.firstChild;this.__lastHeadApplyNode=style;return target.insertBefore(style,after);},createScopeStyle:function(cssText,moniker){var style=document.createElement('style');if(moniker){style.setAttribute('scope',moniker);}
-style.textContent=cssText;return style;},__lastHeadApplyNode:null,applyStylePlaceHolder:function(moniker){var placeHolder=document.createComment(' Shady DOM styles for '+moniker+' ');var after=this.__lastHeadApplyNode?this.__lastHeadApplyNode.nextSibling:null;var scope=document.head;scope.insertBefore(placeHolder,after||scope.firstChild);this.__lastHeadApplyNode=placeHolder;return placeHolder;},cssFromModules:function(moduleIds,warnIfNotFound){var modules=moduleIds.trim().split(' ');var cssText='';for(var i=0;i<modules.length;i++){cssText+=this.cssFromModule(modules[i],warnIfNotFound);}
+style.textContent=cssText;return style;},__lastHeadApplyNode:null,applyStylePlaceHolder:function(moniker){var placeHolder=document.createComment(' Shady DOM styles for '+moniker+' ');var after=this.__lastHeadApplyNode?this.__lastHeadApplyNode.nextSibling:null;var scope=document.head;scope.insertBefore(placeHolder,after||scope.firstChild);this.__lastHeadApplyNode=placeHolder;return placeHolder;},cssFromModules:function(moduleIds,warnIfNotFound){var modules=moduleIds.trim().split(/\s+/);var cssText='';for(var i=0;i<modules.length;i++){cssText+=this.cssFromModule(modules[i],warnIfNotFound);}
 return cssText;},cssFromModule:function(moduleId,warnIfNotFound){var m=Polymer.DomModule.import(moduleId);if(m&&!m._cssText){m._cssText=this.cssFromElement(m);}
 if(!m&&warnIfNotFound){console.warn('Could not find style data in module named',moduleId);}
 return m&&m._cssText||'';},cssFromElement:function(element){var cssText='';var content=element.content||element;var e$=Polymer.TreeApi.arrayCopy(content.querySelectorAll(this.MODULE_STYLES_SELECTOR));for(var i=0,e;i<e$.length;i++){e=e$[i];if(e.localName==='template'){if(!e.hasAttribute('preserve-content')){cssText+=this.cssFromElement(e);}}else{if(e.localName==='style'){var include=e.getAttribute(this.INCLUDE_ATTR);if(include){cssText+=this.cssFromModules(include,true);}
-e=e.__appliedElement||e;e.parentNode.removeChild(e);cssText+=this.resolveCss(e.textContent,element.ownerDocument);}else if(e.import&&e.import.body){cssText+=this.resolveCss(e.import.body.textContent,e.import);}}}
+e=e.__appliedElement||e;e.parentNode.removeChild(e);var css=this.resolveCss(e.textContent,element.ownerDocument);if(!settings.useNativeShadow&&e.hasAttribute(this.SHADY_UNSCOPED_ATTR)){e.textContent=css;document.head.insertBefore(e,document.head.firstChild);}else{cssText+=css;}}else if(e.import&&e.import.body){var importCss=this.resolveCss(e.import.body.textContent,e.import);if(!settings.useNativeShadow&&e.hasAttribute(this.SHADY_UNSCOPED_ATTR)){if(!this.unscopedStyleImports.has(e.import)){this.unscopedStyleImports.set(e.import,true);var importStyle=document.createElement('style');importStyle.setAttribute(this.SHADY_UNSCOPED_ATTR,'');importStyle.textContent=importCss;document.head.insertBefore(importStyle,document.head.firstChild);}}else{cssText+=importCss;}}}}
 return cssText;},styleIncludesToTemplate:function(targetTemplate){var styles=targetTemplate.content.querySelectorAll('style[include]');for(var i=0,s;i<styles.length;i++){s=styles[i];s.parentNode.insertBefore(this._includesToFragment(s.getAttribute('include')),s);}},_includesToFragment:function(styleIncludes){var includeArray=styleIncludes.trim().split(' ');var frag=document.createDocumentFragment();for(var i=0;i<includeArray.length;i++){var t=Polymer.DomModule.import(includeArray[i],'template');if(t){this._addStylesToFragment(frag,t.content);}}
 return frag;},_addStylesToFragment:function(frag,source){var s$=source.querySelectorAll('style');for(var i=0,s;i<s$.length;i++){s=s$[i];var include=s.getAttribute('include');if(include){frag.appendChild(this._includesToFragment(include));}
 if(s.textContent){frag.appendChild(s.cloneNode(true));}}},isTargetedBuild:function(buildType){return settings.useNativeShadow?buildType==='shadow':buildType==='shady';},cssBuildTypeForModule:function(module){var dm=Polymer.DomModule.import(module);if(dm){return this.getCssBuildType(dm);}},getCssBuildType:function(element){return element.getAttribute('css-build');},_findMatchingParen:function(text,start){var level=0;for(var i=start,l=text.length;i<l;i++){switch(text[i]){case'(':level++;break;case')':if(--level===0){return i;}
@@ -3938,16 +4060,25 @@
 return-1;},processVariableAndFallback:function(str,callback){var start=str.indexOf('var(');if(start===-1){return callback(str,'','','');}
 var end=this._findMatchingParen(str,start+3);var inner=str.substring(start+4,end);var prefix=str.substring(0,start);var suffix=this.processVariableAndFallback(str.substring(end+1),callback);var comma=inner.indexOf(',');if(comma===-1){return callback(prefix,inner.trim(),'',suffix);}
 var value=inner.substring(0,comma).trim();var fallback=inner.substring(comma+1).trim();return callback(prefix,value,fallback,suffix);},rx:{VAR_ASSIGN:/(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:([^;{]*)|{([^}]*)})(?:(?=[;\s}])|$)/gi,MIXIN_MATCH:/(?:^|\W+)@apply\s*\(?([^);\n]*)\)?/gi,VAR_CONSUMED:/(--[\w-]+)\s*([:,;)]|$)/gi,ANIMATION_MATCH:/(animation\s*:)|(animation-name\s*:)/,MEDIA_MATCH:/@media[^(]*(\([^)]*\))/,IS_VAR:/^--/,BRACKETED:/\{[^}]*\}/g,HOST_PREFIX:'(?:^|[^.#[:])',HOST_SUFFIX:'($|[.:[\\s>+~])'},resolveCss:Polymer.ResolveUrl.resolveCss,parser:Polymer.CssParse,ruleTypes:Polymer.CssParse.types};}();Polymer.StyleTransformer=function(){var styleUtil=Polymer.StyleUtil;var settings=Polymer.Settings;var api={dom:function(node,scope,useAttr,shouldRemoveScope){this._transformDom(node,scope||'',useAttr,shouldRemoveScope);},_transformDom:function(node,selector,useAttr,shouldRemoveScope){if(node.setAttribute){this.element(node,selector,useAttr,shouldRemoveScope);}
-var c$=Polymer.dom(node).childNodes;for(var i=0;i<c$.length;i++){this._transformDom(c$[i],selector,useAttr,shouldRemoveScope);}},element:function(element,scope,useAttr,shouldRemoveScope){if(useAttr){if(shouldRemoveScope){element.removeAttribute(SCOPE_NAME);}else{element.setAttribute(SCOPE_NAME,scope);}}else{if(scope){if(element.classList){if(shouldRemoveScope){element.classList.remove(SCOPE_NAME);element.classList.remove(scope);}else{element.classList.add(SCOPE_NAME);element.classList.add(scope);}}else if(element.getAttribute){var c=element.getAttribute(CLASS);if(shouldRemoveScope){if(c){element.setAttribute(CLASS,c.replace(SCOPE_NAME,'').replace(scope,''));}}else{element.setAttribute(CLASS,(c?c+' ':'')+SCOPE_NAME+' '+scope);}}}}},elementStyles:function(element,callback){var styles=element._styles;var cssText='';var cssBuildType=element.__cssBuild;var passthrough=settings.useNativeShadow||cssBuildType==='shady';var cb;if(passthrough){var self=this;cb=function(rule){rule.selector=self._slottedToContent(rule.selector);rule.selector=rule.selector.replace(ROOT,':host > *');if(callback){callback(rule);}};}
+var c$=Polymer.dom(node).childNodes;for(var i=0;i<c$.length;i++){this._transformDom(c$[i],selector,useAttr,shouldRemoveScope);}},element:function(element,scope,useAttr,shouldRemoveScope){if(useAttr){if(shouldRemoveScope){element.removeAttribute(SCOPE_NAME);}else{element.setAttribute(SCOPE_NAME,scope);}}else{if(scope){if(element.classList){if(shouldRemoveScope){element.classList.remove(SCOPE_NAME);element.classList.remove(scope);}else{element.classList.add(SCOPE_NAME);element.classList.add(scope);}}else if(element.getAttribute){var c=element.getAttribute(CLASS);if(shouldRemoveScope){if(c){element.setAttribute(CLASS,c.replace(SCOPE_NAME,'').replace(scope,''));}}else{element.setAttribute(CLASS,(c?c+' ':'')+SCOPE_NAME+' '+scope);}}}}},elementStyles:function(element,callback){var styles=element._styles;var cssText='';var cssBuildType=element.__cssBuild;var passthrough=settings.useNativeShadow||cssBuildType==='shady';var cb;if(passthrough){var self=this;cb=function(rule){rule.selector=self._slottedToContent(rule.selector);rule.selector=rule.selector.replace(ROOT,':host > *');rule.selector=self._dirShadowTransform(rule.selector);if(callback){callback(rule);}};}
 for(var i=0,l=styles.length,s;i<l&&(s=styles[i]);i++){var rules=styleUtil.rulesForStyle(s);cssText+=passthrough?styleUtil.toCssText(rules,cb):this.css(rules,element.is,element.extends,callback,element._scopeCssViaAttr)+'\n\n';}
 return cssText.trim();},css:function(rules,scope,ext,callback,useAttr){var hostScope=this._calcHostScope(scope,ext);scope=this._calcElementScope(scope,useAttr);var self=this;return styleUtil.toCssText(rules,function(rule){if(!rule.isScoped){self.rule(rule,scope,hostScope);rule.isScoped=true;}
-if(callback){callback(rule,scope,hostScope);}});},_calcElementScope:function(scope,useAttr){if(scope){return useAttr?CSS_ATTR_PREFIX+scope+CSS_ATTR_SUFFIX:CSS_CLASS_PREFIX+scope;}else{return'';}},_calcHostScope:function(scope,ext){return ext?'[is='+scope+']':scope;},rule:function(rule,scope,hostScope){this._transformRule(rule,this._transformComplexSelector,scope,hostScope);},_transformRule:function(rule,transformer,scope,hostScope){rule.selector=rule.transformedSelector=this._transformRuleCss(rule,transformer,scope,hostScope);},_transformRuleCss:function(rule,transformer,scope,hostScope){var p$=rule.selector.split(COMPLEX_SELECTOR_SEP);if(!styleUtil.isKeyframesSelector(rule)){for(var i=0,l=p$.length,p;i<l&&(p=p$[i]);i++){p$[i]=transformer.call(this,p,scope,hostScope);}}
-return p$.join(COMPLEX_SELECTOR_SEP);},_transformComplexSelector:function(selector,scope,hostScope){var stop=false;var hostContext=false;var self=this;selector=selector.trim();selector=this._slottedToContent(selector);selector=selector.replace(ROOT,':host > *');selector=selector.replace(CONTENT_START,HOST+' $1');selector=selector.replace(SIMPLE_SELECTOR_SEP,function(m,c,s){if(!stop){var info=self._transformCompoundSelector(s,c,scope,hostScope);stop=stop||info.stop;hostContext=hostContext||info.hostContext;c=info.combinator;s=info.value;}else{s=s.replace(SCOPE_JUMP,' ');}
-return c+s;});if(hostContext){selector=selector.replace(HOST_CONTEXT_PAREN,function(m,pre,paren,post){return pre+paren+' '+hostScope+post+COMPLEX_SELECTOR_SEP+' '+pre+hostScope+paren+post;});}
-return selector;},_transformCompoundSelector:function(selector,combinator,scope,hostScope){var jumpIndex=selector.search(SCOPE_JUMP);var hostContext=false;if(selector.indexOf(HOST_CONTEXT)>=0){hostContext=true;}else if(selector.indexOf(HOST)>=0){selector=this._transformHostSelector(selector,hostScope);}else if(jumpIndex!==0){selector=scope?this._transformSimpleSelector(selector,scope):selector;}
+if(callback){callback(rule,scope,hostScope);}});},_calcElementScope:function(scope,useAttr){if(scope){return useAttr?CSS_ATTR_PREFIX+scope+CSS_ATTR_SUFFIX:CSS_CLASS_PREFIX+scope;}else{return'';}},_calcHostScope:function(scope,ext){return ext?'[is='+scope+']':scope;},rule:function(rule,scope,hostScope){this._transformRule(rule,this._transformComplexSelector,scope,hostScope);},_transformRule:function(rule,transformer,scope,hostScope){rule.selector=rule.transformedSelector=this._transformRuleCss(rule,transformer,scope,hostScope);},_splitSelectorList:function(selector){var parts=[];var part='';for(var i=0;i>=0&&i<selector.length;i++){if(selector[i]==='('){var end=styleUtil._findMatchingParen(selector,i);part+=selector.slice(i,end+1);i=end;}else if(selector[i]===COMPLEX_SELECTOR_SEP){parts.push(part);part='';}else{part+=selector[i];}}
+if(part){parts.push(part);}
+if(parts.length===0){parts.push(selector);}
+return parts;},_transformRuleCss:function(rule,transformer,scope,hostScope){var p$=this._splitSelectorList(rule.selector);if(!styleUtil.isKeyframesSelector(rule)){for(var i=0,l=p$.length,p;i<l&&(p=p$[i]);i++){p$[i]=transformer.call(this,p,scope,hostScope);}}
+return p$.join(COMPLEX_SELECTOR_SEP);},_ensureScopedDir:function(s){var m=s.match(DIR_PAREN);if(m&&m[1]===''&&m[0].length===s.length){s='*'+s;}
+return s;},_additionalDirSelectors:function(dir,after,prefix){if(!dir||!after){return'';}
+prefix=prefix||'';return COMPLEX_SELECTOR_SEP+prefix+' '+dir+' '+after;},_transformComplexSelector:function(selector,scope,hostScope){var stop=false;var hostContext=false;var dir=false;var self=this;selector=selector.trim();selector=this._slottedToContent(selector);selector=selector.replace(ROOT,':host > *');selector=selector.replace(CONTENT_START,HOST+' $1');selector=this._ensureScopedDir(selector);selector=selector.replace(SIMPLE_SELECTOR_SEP,function(m,c,s){if(!stop){var info=self._transformCompoundSelector(s,c,scope,hostScope);stop=stop||info.stop;hostContext=hostContext||info.hostContext;dir=dir||info.dir;c=info.combinator;s=info.value;}else{s=s.replace(SCOPE_JUMP,' ');}
+return c+s;});if(hostContext){selector=selector.replace(HOST_CONTEXT_PAREN,function(m,pre,paren,post){var replacement=pre+paren+' '+hostScope+post+COMPLEX_SELECTOR_SEP+' '+pre+hostScope+paren+post;if(dir){replacement+=self._additionalDirSelectors(paren,post,hostScope);}
+return replacement;});}
+return selector;},_transformDir:function(s){s=s.replace(HOST_DIR,HOST_DIR_REPLACE);s=s.replace(DIR_PAREN,DIR_REPLACE);return s;},_transformCompoundSelector:function(selector,combinator,scope,hostScope){var jumpIndex=selector.search(SCOPE_JUMP);var hostContext=false;var dir=false;if(selector.match(DIR_PAREN)){selector=this._transformDir(selector);dir=true;}
+if(selector.indexOf(HOST_CONTEXT)>=0){hostContext=true;}else if(selector.indexOf(HOST)>=0){selector=this._transformHostSelector(selector,hostScope);}else if(jumpIndex!==0){selector=scope?this._transformSimpleSelector(selector,scope):selector;}
 if(selector.indexOf(CONTENT)>=0){combinator='';}
 var stop;if(jumpIndex>=0){selector=selector.replace(SCOPE_JUMP,' ');stop=true;}
-return{value:selector,combinator:combinator,stop:stop,hostContext:hostContext};},_transformSimpleSelector:function(selector,scope){var p$=selector.split(PSEUDO_PREFIX);p$[0]+=scope;return p$.join(PSEUDO_PREFIX);},_transformHostSelector:function(selector,hostScope){var m=selector.match(HOST_PAREN);var paren=m&&m[2].trim()||'';if(paren){if(!paren[0].match(SIMPLE_SELECTOR_PREFIX)){var typeSelector=paren.split(SIMPLE_SELECTOR_PREFIX)[0];if(typeSelector===hostScope){return paren;}else{return SELECTOR_NO_MATCH;}}else{return selector.replace(HOST_PAREN,function(m,host,paren){return hostScope+paren;});}}else{return selector.replace(HOST,hostScope);}},documentRule:function(rule){rule.selector=rule.parsedSelector;this.normalizeRootSelector(rule);if(!settings.useNativeShadow){this._transformRule(rule,this._transformDocumentSelector);}},normalizeRootSelector:function(rule){rule.selector=rule.selector.replace(ROOT,'html');var parts=rule.selector.split(COMPLEX_SELECTOR_SEP);parts=parts.filter(function(part){return!part.match(HOST_OR_HOST_GT_STAR);});rule.selector=parts.join(COMPLEX_SELECTOR_SEP);},_transformDocumentSelector:function(selector){return selector.match(SCOPE_JUMP)?this._transformComplexSelector(selector,SCOPE_DOC_SELECTOR):this._transformSimpleSelector(selector.trim(),SCOPE_DOC_SELECTOR);},_slottedToContent:function(cssText){return cssText.replace(SLOTTED_PAREN,CONTENT+'> $1');},SCOPE_NAME:'style-scope'};var SCOPE_NAME=api.SCOPE_NAME;var SCOPE_DOC_SELECTOR=':not(['+SCOPE_NAME+'])'+':not(.'+SCOPE_NAME+')';var COMPLEX_SELECTOR_SEP=',';var SIMPLE_SELECTOR_SEP=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=\[])+)/g;var SIMPLE_SELECTOR_PREFIX=/[[.:#*]/;var HOST=':host';var ROOT=':root';var HOST_PAREN=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/;var HOST_CONTEXT=':host-context';var HOST_CONTEXT_PAREN=/(.*)(?::host-context)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))(.*)/;var CONTENT='::content';var SCOPE_JUMP=/::content|::shadow|\/deep\//;var CSS_CLASS_PREFIX='.';var CSS_ATTR_PREFIX='['+SCOPE_NAME+'~=';var CSS_ATTR_SUFFIX=']';var PSEUDO_PREFIX=':';var CLASS='class';var CONTENT_START=new RegExp('^('+CONTENT+')');var SELECTOR_NO_MATCH='should_not_match';var SLOTTED_PAREN=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/g;var HOST_OR_HOST_GT_STAR=/:host(?:\s*>\s*\*)?/;return api;}();Polymer.StyleExtends=function(){var styleUtil=Polymer.StyleUtil;return{hasExtends:function(cssText){return Boolean(cssText.match(this.rx.EXTEND));},transform:function(style){var rules=styleUtil.rulesForStyle(style);var self=this;styleUtil.forEachRule(rules,function(rule){self._mapRuleOntoParent(rule);if(rule.parent){var m;while(m=self.rx.EXTEND.exec(rule.cssText)){var extend=m[1];var extendor=self._findExtendor(extend,rule);if(extendor){self._extendRule(rule,extendor);}}}
+return{value:selector,combinator:combinator,stop:stop,hostContext:hostContext,dir:dir};},_transformSimpleSelector:function(selector,scope){var p$=selector.split(PSEUDO_PREFIX);p$[0]+=scope;return p$.join(PSEUDO_PREFIX);},_transformHostSelector:function(selector,hostScope){var m=selector.match(HOST_PAREN);var paren=m&&m[2].trim()||'';if(paren){if(!paren[0].match(SIMPLE_SELECTOR_PREFIX)){var typeSelector=paren.split(SIMPLE_SELECTOR_PREFIX)[0];if(typeSelector===hostScope){return paren;}else{return SELECTOR_NO_MATCH;}}else{return selector.replace(HOST_PAREN,function(m,host,paren){return hostScope+paren;});}}else{return selector.replace(HOST,hostScope);}},documentRule:function(rule){rule.selector=rule.parsedSelector;this.normalizeRootSelector(rule);if(!settings.useNativeShadow){this._transformRule(rule,this._transformDocumentSelector);}},normalizeRootSelector:function(rule){rule.selector=rule.selector.replace(ROOT,'html');var parts=this._splitSelectorList(rule.selector);parts=parts.filter(function(part){return!part.match(HOST_OR_HOST_GT_STAR);});rule.selector=parts.join(COMPLEX_SELECTOR_SEP);},_transformDocumentSelector:function(selector){return this._transformComplexSelector(selector,SCOPE_DOC_SELECTOR);},_slottedToContent:function(cssText){return cssText.replace(SLOTTED_PAREN,CONTENT+'> $1');},_dirShadowTransform:function(selector){if(!selector.match(/:dir\(/)){return selector;}
+return this._splitSelectorList(selector).map(function(s){s=this._ensureScopedDir(s);s=this._transformDir(s);var m=HOST_CONTEXT_PAREN.exec(s);if(m){s+=this._additionalDirSelectors(m[2],m[3],'');}
+return s;},this).join(COMPLEX_SELECTOR_SEP);},SCOPE_NAME:'style-scope'};var SCOPE_NAME=api.SCOPE_NAME;var SCOPE_DOC_SELECTOR=':not(['+SCOPE_NAME+'])'+':not(.'+SCOPE_NAME+')';var COMPLEX_SELECTOR_SEP=',';var SIMPLE_SELECTOR_SEP=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=\[])+)/g;var SIMPLE_SELECTOR_PREFIX=/[[.:#*]/;var HOST=':host';var ROOT=':root';var HOST_PAREN=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/;var HOST_CONTEXT=':host-context';var HOST_CONTEXT_PAREN=/(.*)(?::host-context)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))(.*)/;var CONTENT='::content';var SCOPE_JUMP=/::content|::shadow|\/deep\//;var CSS_CLASS_PREFIX='.';var CSS_ATTR_PREFIX='['+SCOPE_NAME+'~=';var CSS_ATTR_SUFFIX=']';var PSEUDO_PREFIX=':';var CLASS='class';var CONTENT_START=new RegExp('^('+CONTENT+')');var SELECTOR_NO_MATCH='should_not_match';var SLOTTED_PAREN=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/g;var HOST_OR_HOST_GT_STAR=/:host(?:\s*>\s*\*)?/;var DIR_PAREN=/(.*):dir\((ltr|rtl)\)/;var DIR_REPLACE=':host-context([dir="$2"]) $1';var HOST_DIR=/:host\(:dir\((rtl|ltr)\)\)/g;var HOST_DIR_REPLACE=':host-context([dir="$1"])';return api;}();Polymer.StyleExtends=function(){var styleUtil=Polymer.StyleUtil;return{hasExtends:function(cssText){return Boolean(cssText.match(this.rx.EXTEND));},transform:function(style){var rules=styleUtil.rulesForStyle(style);var self=this;styleUtil.forEachRule(rules,function(rule){self._mapRuleOntoParent(rule);if(rule.parent){var m;while(m=self.rx.EXTEND.exec(rule.cssText)){var extend=m[1];var extendor=self._findExtendor(extend,rule);if(extendor){self._extendRule(rule,extendor);}}}
 rule.cssText=rule.cssText.replace(self.rx.EXTEND,'');});return styleUtil.toCssText(rules,function(rule){if(rule.selector.match(self.rx.STRIP)){rule.cssText='';}},true);},_mapRuleOntoParent:function(rule){if(rule.parent){var map=rule.parent.map||(rule.parent.map={});var parts=rule.selector.split(',');for(var i=0,p;i<parts.length;i++){p=parts[i];map[p.trim()]=rule;}
 return map;}},_findExtendor:function(extend,rule){return rule.parent&&rule.parent.map&&rule.parent.map[extend]||this._findExtendor(extend,rule.parent);},_extendRule:function(target,source){if(target.parent!==source.parent){this._cloneAndAddRuleToParent(source,target.parent);}
 target.extends=target.extends||[];target.extends.push(source);source.selector=source.selector.replace(this.rx.STRIP,'');source.selector=(source.selector&&source.selector+',\n')+target.selector;if(source.extends){source.extends.forEach(function(e){this._extendRule(target,e);},this);}},_cloneAndAddRuleToParent:function(rule,parent){rule=Object.create(rule);rule.parent=parent;if(rule.extends){rule.extends=rule.extends.slice();}
@@ -4013,7 +4144,7 @@
 selectorToMatch=rule.transformedSelector||rule.parsedSelector;}
 if(isRoot&&hostScope==='html'){selectorToMatch=rule.transformedSelector||rule.parsedSelector;}
 callback({selector:selectorToMatch,isHost:isHost,isRoot:isRoot});},hostAndRootPropertiesForScope:function(scope){var hostProps={},rootProps={},self=this;styleUtil.forActiveRulesInStyles(scope._styles,function(rule,style){self.whenHostOrRootRule(scope,rule,style,function(info){var element=scope._element||scope;if(matchesSelector.call(element,info.selector)){if(info.isHost){self.collectProperties(rule,hostProps);}else{self.collectProperties(rule,rootProps);}}});});return{rootProps:rootProps,hostProps:hostProps};},transformStyles:function(element,properties,scopeSelector){var self=this;var hostSelector=styleTransformer._calcHostScope(element.is,element.extends);var rxHostSelector=element.extends?'\\'+hostSelector.slice(0,-1)+'\\]':hostSelector;var hostRx=new RegExp(this.rx.HOST_PREFIX+rxHostSelector+this.rx.HOST_SUFFIX);var keyframeTransforms=this._elementKeyframeTransforms(element,scopeSelector);return styleTransformer.elementStyles(element,function(rule){self.applyProperties(rule,properties);if(!settings.useNativeShadow&&!Polymer.StyleUtil.isKeyframesSelector(rule)&&rule.cssText){self.applyKeyframeTransforms(rule,keyframeTransforms);self._scopeSelector(rule,hostRx,hostSelector,element._scopeCssViaAttr,scopeSelector);}});},_elementKeyframeTransforms:function(element,scopeSelector){var keyframesRules=element._styles._keyframes;var keyframeTransforms={};if(!settings.useNativeShadow&&keyframesRules){for(var i=0,keyframesRule=keyframesRules[i];i<keyframesRules.length;keyframesRule=keyframesRules[++i]){this._scopeKeyframes(keyframesRule,scopeSelector);keyframeTransforms[keyframesRule.keyframesName]=this._keyframesRuleTransformer(keyframesRule);}}
-return keyframeTransforms;},_keyframesRuleTransformer:function(keyframesRule){return function(cssText){return cssText.replace(keyframesRule.keyframesNameRx,keyframesRule.transformedKeyframesName);};},_scopeKeyframes:function(rule,scopeId){rule.keyframesNameRx=new RegExp(rule.keyframesName,'g');rule.transformedKeyframesName=rule.keyframesName+'-'+scopeId;rule.transformedSelector=rule.transformedSelector||rule.selector;rule.selector=rule.transformedSelector.replace(rule.keyframesName,rule.transformedKeyframesName);},_scopeSelector:function(rule,hostRx,hostSelector,viaAttr,scopeId){rule.transformedSelector=rule.transformedSelector||rule.selector;var selector=rule.transformedSelector;var scope=viaAttr?'['+styleTransformer.SCOPE_NAME+'~='+scopeId+']':'.'+scopeId;var parts=selector.split(',');for(var i=0,l=parts.length,p;i<l&&(p=parts[i]);i++){parts[i]=p.match(hostRx)?p.replace(hostSelector,scope):scope+' '+p;}
+return keyframeTransforms;},_keyframesRuleTransformer:function(keyframesRule){return function(cssText){return cssText.replace(keyframesRule.keyframesNameRx,keyframesRule.transformedKeyframesName);};},_scopeKeyframes:function(rule,scopeId){rule.keyframesNameRx=new RegExp('\\b'+rule.keyframesName+'(?!\\B|-)','g');rule.transformedKeyframesName=rule.keyframesName+'-'+scopeId;rule.transformedSelector=rule.transformedSelector||rule.selector;rule.selector=rule.transformedSelector.replace(rule.keyframesName,rule.transformedKeyframesName);},_hasDirOrHostContext:function(parsedSelector){return/:host-context|:dir/.test(parsedSelector);},_scopeSelector:function(rule,hostRx,hostSelector,viaAttr,scopeId){rule.transformedSelector=rule.transformedSelector||rule.selector;var selector=rule.transformedSelector;var scope=styleTransformer._calcElementScope(scopeId,viaAttr);var hostScope=styleTransformer._calcElementScope(hostSelector,viaAttr);var parts=selector.split(',');var isDirOrHostContextSelector=this._hasDirOrHostContext(rule.parsedSelector);for(var i=0,l=parts.length,p;i<l&&(p=parts[i]);i++){parts[i]=p.match(hostRx)?p.replace(hostSelector,scope):isDirOrHostContextSelector?p.replace(hostScope,scope+' '+hostScope):scope+' '+p;}
 rule.selector=parts.join(',');},applyElementScopeSelector:function(element,selector,old,viaAttr){var c=viaAttr?element.getAttribute(styleTransformer.SCOPE_NAME):element.getAttribute('class')||'';var v=old?c.replace(old,selector):(c?c+' ':'')+this.XSCOPE_NAME+' '+selector;if(c!==v){if(viaAttr){element.setAttribute(styleTransformer.SCOPE_NAME,v);}else{element.setAttribute('class',v);}}},applyElementStyle:function(element,properties,selector,style){var cssText=style?style.textContent||'':this.transformStyles(element,properties,selector);var s=element._customStyle;if(s&&!settings.useNativeShadow&&s!==style){s._useCount--;if(s._useCount<=0&&s.parentNode){s.parentNode.removeChild(s);}}
 if(settings.useNativeShadow){if(element._customStyle){element._customStyle.textContent=cssText;style=element._customStyle;}else if(cssText){style=styleUtil.applyCss(cssText,selector,element.root,element._scopeStyle);}}else{if(!style){if(cssText){style=styleUtil.applyCss(cssText,selector,null,element._scopeStyle);}}else if(!style.parentNode){if(IS_IE&&cssText.indexOf('@media')>-1){style.textContent=cssText;}
 styleUtil.applyStyle(style,null,element._scopeStyle);}}
@@ -4058,7 +4189,7 @@
 var archetype=Object.create(Polymer.Base);this._customPrepAnnotations(archetype,template);this._prepParentProperties(archetype,template);archetype._prepEffects();this._customPrepEffects(archetype);archetype._prepBehaviors();archetype._prepPropertyInfo();archetype._prepBindings();archetype._notifyPathUp=this._notifyPathUpImpl;archetype._scopeElementClass=this._scopeElementClassImpl;archetype.listen=this._listenImpl;archetype._showHideChildren=this._showHideChildrenImpl;archetype.__setPropertyOrig=this.__setProperty;archetype.__setProperty=this.__setPropertyImpl;var _constructor=this._constructorImpl;var ctor=function TemplateInstance(model,host){_constructor.call(this,model,host);};ctor.prototype=archetype;archetype.constructor=ctor;template._content._ctor=ctor;this.ctor=ctor;},_getRootDataHost:function(){return this.dataHost&&this.dataHost._rootDataHost||this.dataHost;},_showHideChildrenImpl:function(hide){var c=this._children;for(var i=0;i<c.length;i++){var n=c[i];if(Boolean(hide)!=Boolean(n.__hideTemplateChildren__)){if(n.nodeType===Node.TEXT_NODE){if(hide){n.__polymerTextContent__=n.textContent;n.textContent='';}else{n.textContent=n.__polymerTextContent__;}}else if(n.style){if(hide){n.__polymerDisplay__=n.style.display;n.style.display='none';}else{n.style.display=n.__polymerDisplay__;}}}
 n.__hideTemplateChildren__=hide;}},__setPropertyImpl:function(property,value,fromAbove,node){if(node&&node.__hideTemplateChildren__&&property=='textContent'){property='__polymerTextContent__';}
 this.__setPropertyOrig(property,value,fromAbove,node);},_debounceTemplate:function(fn){Polymer.dom.addDebouncer(this.debounce('_debounceTemplate',fn));},_flushTemplates:function(){Polymer.dom.flush();},_customPrepEffects:function(archetype){var parentProps=archetype._parentProps;for(var prop in parentProps){archetype._addPropertyEffect(prop,'function',this._createHostPropEffector(prop));}
-for(prop in this._instanceProps){archetype._addPropertyEffect(prop,'function',this._createInstancePropEffector(prop));}},_customPrepAnnotations:function(archetype,template){archetype._template=template;var c=template._content;if(!c._notes){var rootDataHost=archetype._rootDataHost;if(rootDataHost){Polymer.Annotations.prepElement=function(){rootDataHost._prepElement();};}
+for(prop in this._instanceProps){archetype._addPropertyEffect(prop,'function',this._createInstancePropEffector(prop));}},_customPrepAnnotations:function(archetype,template){var t=archetype._template=document.createElement('template');var c=t._content=template._content;if(!c._notes){var rootDataHost=archetype._rootDataHost;if(rootDataHost){Polymer.Annotations.prepElement=function(){rootDataHost._prepElement();};}
 c._notes=Polymer.Annotations.parseAnnotations(template);Polymer.Annotations.prepElement=null;this._processAnnotations(c._notes);}
 archetype._notes=c._notes;archetype._parentProps=c._parentProps;},_prepParentProperties:function(archetype,template){var parentProps=this._parentProps=archetype._parentProps;if(this._forwardParentProp&&parentProps){var proto=archetype._parentPropProto;var prop;if(!proto){for(prop in this._instanceProps){delete parentProps[prop];}
 proto=archetype._parentPropProto=Object.create(null);if(template!=this){Polymer.Bind.prepareModel(proto);Polymer.Base.prepareModelNotifyPath(proto);}
@@ -4273,10 +4404,11 @@
 function greaterPower(x,opt_base){const base=opt_base||10;return Math.pow(base,Math.ceil(logOrLog10(x,base)));}
 function lesserWholeNumber(x){if(x===0)return 0;const pow10=(x<0)?-lesserPower(-x):lesserPower(x);return pow10*Math.floor(x/pow10);}
 function greaterWholeNumber(x){if(x===0)return 0;const pow10=(x<0)?-lesserPower(-x):lesserPower(x);return pow10*Math.ceil(x/pow10);}
+function truncate(value,digits){const pow10=Math.pow(10,digits);return Math.round(value*pow10)/pow10;}
 function preferredNumberLargerThanMin(min){const absMin=Math.abs(min);const conservativeGuess=tr.b.math.lesserPower(absMin);let minPreferedNumber=undefined;for(const multiplier of PREFERRED_NUMBER_SERIES_MULTIPLIERS){const tightenedGuess=conservativeGuess*multiplier;if(tightenedGuess>=absMin){minPreferedNumber=tightenedGuess;break;}}
 if(minPreferedNumber===undefined){throw new Error('Could not compute preferred number for '+min);}
 if(min<0)minPreferedNumber*=-1;return minPreferedNumber;}
-return{approximately,clamp,lerp,normalize,deg2rad,erf,lesserPower,greaterPower,lesserWholeNumber,greaterWholeNumber,preferredNumberLargerThanMin,};});'use strict';tr.exportTo('tr.b.math',function(){function Range(){this.isEmpty_=true;this.min_=undefined;this.max_=undefined;}
+return{approximately,clamp,lerp,normalize,deg2rad,erf,lesserPower,greaterPower,lesserWholeNumber,greaterWholeNumber,preferredNumberLargerThanMin,truncate,};});'use strict';tr.exportTo('tr.b.math',function(){function Range(){this.isEmpty_=true;this.min_=undefined;this.max_=undefined;}
 Range.prototype={__proto__:Object.prototype,clone(){if(this.isEmpty)return new Range();return Range.fromExplicitRange(this.min_,this.max_);},reset(){this.isEmpty_=true;this.min_=undefined;this.max_=undefined;},get isEmpty(){return this.isEmpty_;},addRange(range){if(range.isEmpty)return;this.addValue(range.min);this.addValue(range.max);},addValue(value){if(this.isEmpty_){this.max_=value;this.min_=value;this.isEmpty_=false;return;}
 this.max_=Math.max(this.max_,value);this.min_=Math.min(this.min_,value);},set min(min){this.isEmpty_=false;this.min_=min;},get min(){if(this.isEmpty_)return undefined;return this.min_;},get max(){if(this.isEmpty_)return undefined;return this.max_;},set max(max){this.isEmpty_=false;this.max_=max;},get range(){if(this.isEmpty_)return undefined;return this.max_-this.min_;},get center(){return(this.min_+this.max_)*0.5;},get duration(){if(this.isEmpty_)return 0;return this.max_-this.min_;},enclosingPowers(opt_base){if(this.isEmpty)return new Range();return Range.fromExplicitRange(tr.b.math.lesserPower(this.min_,opt_base),tr.b.math.greaterPower(this.max_,opt_base));},normalize(x){return tr.b.math.normalize(x,this.min,this.max);},lerp(x){return tr.b.math.lerp(x,this.min,this.max);},clamp(x){return tr.b.math.clamp(x,this.min,this.max);},equals(that){if(this.isEmpty&&that.isEmpty)return true;if(this.isEmpty!==that.isEmpty)return false;return(tr.b.math.approximately(this.min,that.min)&&tr.b.math.approximately(this.max,that.max));},containsExplicitRangeInclusive(min,max){if(this.isEmpty)return false;return this.min_<=min&&max<=this.max_;},containsExplicitRangeExclusive(min,max){if(this.isEmpty)return false;return this.min_<min&&max<this.max_;},intersectsExplicitRangeInclusive(min,max){if(this.isEmpty)return false;return this.min_<=max&&min<=this.max_;},intersectsExplicitRangeExclusive(min,max){if(this.isEmpty)return false;return this.min_<max&&min<this.max_;},containsRangeInclusive(range){if(range.isEmpty)return false;return this.containsExplicitRangeInclusive(range.min_,range.max_);},containsRangeExclusive(range){if(range.isEmpty)return false;return this.containsExplicitRangeExclusive(range.min_,range.max_);},intersectsRangeInclusive(range){if(range.isEmpty)return false;return this.intersectsExplicitRangeInclusive(range.min_,range.max_);},intersectsRangeExclusive(range){if(range.isEmpty)return false;return this.intersectsExplicitRangeExclusive(range.min_,range.max_);},findExplicitIntersectionDuration(min,max){min=Math.max(this.min,min);max=Math.min(this.max,max);if(max<min)return 0;return max-min;},findIntersection(range){if(this.isEmpty||range.isEmpty)return new Range();const min=Math.max(this.min,range.min);const max=Math.min(this.max,range.max);if(max<min)return new Range();return Range.fromExplicitRange(min,max);},toJSON(){if(this.isEmpty_)return{isEmpty:true};return{isEmpty:false,max:this.max,min:this.min};},filterArray(sortedArray,opt_keyFunc,opt_this){if(this.isEmpty_)return[];const keyFunc=opt_keyFunc||(x=>x);function getValue(obj){return keyFunc.call(opt_this,obj);}
 const first=tr.b.findFirstTrueIndexInSortedArray(sortedArray,obj=>this.min_===undefined||this.min_<=getValue(obj));const last=tr.b.findFirstTrueIndexInSortedArray(sortedArray,obj=>this.max_!==undefined&&this.max_<getValue(obj));return sortedArray.slice(first,last);}};Range.fromDict=function(d){if(d.isEmpty===true)return new Range();if(d.isEmpty===false){const range=new Range();range.min=d.min;range.max=d.max;return range;}
@@ -4379,7 +4511,9 @@
 return formatter;}
 function max(a,b){if(a===undefined)return b;if(b===undefined)return a;return a.scale>b.scale?a:b;}
 const ImprovementDirection={DONT_CARE:0,BIGGER_IS_BETTER:1,SMALLER_IS_BETTER:2};function Unit(unitName,jsonName,scaleBaseUnit,isDelta,improvementDirection,formatSpec){this.unitName=unitName;this.jsonName=jsonName;this.scaleBaseUnit=scaleBaseUnit;this.isDelta=isDelta;this.improvementDirection=improvementDirection;this.formatSpec_=formatSpec;this.baseUnit=undefined;this.correspondingDeltaUnit=undefined;}
-Unit.prototype={asJSON(){return this.jsonName;},getUnitScale_(opt_context){let formatSpec=this.formatSpec_;let formatSpecWasFunction=false;if(typeof formatSpec==='function'){formatSpecWasFunction=true;formatSpec=formatSpec();}
+Unit.prototype={asJSON(){return this.jsonName;},asJSON2(){return this.asJSON().replace('_smallerIsBetter','-').replace('_biggerIsBetter','+');},truncate(value){if(typeof value!=='number')return value;if(0===(value%1))return value;if(typeof this.formatSpec_!=='function'&&(!this.formatSpec_.unitScale||((this.formatSpec_.unitScale.length===1)&&(this.formatSpec_.unitScale[0].value===1)))){const digits=this.formatSpec_.maximumFractionDigits||this.formatSpec_.minimumFractionDigits;return tr.b.math.truncate(value,digits+1);}
+const formatted=this.format(value);let test=Math.round(value);if(formatted===this.format(test))return test;let lo=1;let hi=16;while(lo<hi-1){const digits=parseInt((lo+hi)/2);test=tr.b.math.truncate(value,digits);if(formatted===this.format(test)){hi=digits;}else{lo=digits;}}
+test=tr.b.math.truncate(value,lo);if(formatted===this.format(test))return test;return tr.b.math.truncate(value,hi);},getUnitScale_(opt_context){let formatSpec=this.formatSpec_;let formatSpecWasFunction=false;if(typeof formatSpec==='function'){formatSpecWasFunction=true;formatSpec=formatSpec();}
 const context=opt_context||{};let scale=undefined;if(context.unitScale){scale=context.unitScale;}else if(context.unitPrefix){const symbol=formatSpec.baseSymbol?formatSpec.baseSymbol:this.scaleBaseUnit.baseSymbol;scale=tr.b.UnitScale.defineUnitScaleFromPrefixScale(symbol,symbol,[context.unitPrefix]).AUTO;}else{scale=formatSpec.unitScale;if(!scale){scale=[{value:1,symbol:formatSpec.baseSymbol||'',baseSymbol:formatSpec.baseSymbol||''}];if(!formatSpecWasFunction)formatSpec.unitScale=scale;}}
 if(!(scale instanceof Array)){throw new Error('Unit has a malformed unit scale.');}
 return scale;},get unitString(){const scale=this.getUnitScale_();if(!scale){throw new Error('A UnitScale could not be found for Unit '+this.unitName);}
@@ -4387,7 +4521,8 @@
 const context=opt_context||{};const scale=this.getUnitScale_(context);let deltaValue=context.deltaValue===undefined?value:context.deltaValue;deltaValue=Math.abs(deltaValue)*this.scaleBaseUnit.value;if(deltaValue===0){deltaValue=1;}
 let i=0;while(i<scale.length-1&&deltaValue/scale[i+1].value>=1){i++;}
 const selectedSubUnit=scale[i];let formatSpec=this.formatSpec_;if(typeof formatSpec==='function')formatSpec=formatSpec();let unitString='';if(selectedSubUnit.symbol){if(!formatSpec.avoidSpacePrecedingUnit)unitString=' ';unitString+=selectedSubUnit.symbol;}
-value=tr.b.convertUnit(value,this.scaleBaseUnit,selectedSubUnit);const numberString=getNumberFormatter(formatSpec.minimumFractionDigits,formatSpec.maximumFractionDigits,context.minimumFractionDigits,context.maximumFractionDigits).format(value);return signString+numberString+unitString;}};Unit.reset=function(){Unit.currentTimeDisplayMode=TimeDisplayModes.ms;};Unit.timestampFromUs=function(us){return tr.b.convertUnit(us,tr.b.UnitPrefixScale.METRIC.MICRO,tr.b.UnitPrefixScale.METRIC.MILLI);};Object.defineProperty(Unit,'currentTimeDisplayMode',{get(){return Unit.currentTimeDisplayMode_;},set(value){if(Unit.currentTimeDisplayMode_===value)return;Unit.currentTimeDisplayMode_=value;Unit.dispatchEvent(new tr.b.Event('display-mode-changed'));}});Unit.didPreferredTimeDisplayUnitChange=function(){let largest=undefined;const els=tr.ui.b.findDeepElementsMatching(document.body,'tr-v-ui-preferred-display-unit');els.forEach(function(el){largest=max(largest,el.preferredTimeDisplayMode);});Unit.currentTimeDisplayMode=largest===undefined?TimeDisplayModes.ms:largest;};Unit.byName={};Unit.byJSONName={};Unit.fromJSON=function(object){const u=Unit.byJSONName[object];if(u){return u;}
+value=tr.b.convertUnit(value,this.scaleBaseUnit,selectedSubUnit);const numberString=getNumberFormatter(formatSpec.minimumFractionDigits,formatSpec.maximumFractionDigits,context.minimumFractionDigits,context.maximumFractionDigits).format(value);return signString+numberString+unitString;}};Unit.reset=function(){Unit.currentTimeDisplayMode=TimeDisplayModes.ms;};Unit.timestampFromUs=function(us){return tr.b.convertUnit(us,tr.b.UnitPrefixScale.METRIC.MICRO,tr.b.UnitPrefixScale.METRIC.MILLI);};Object.defineProperty(Unit,'currentTimeDisplayMode',{get(){return Unit.currentTimeDisplayMode_;},set(value){if(Unit.currentTimeDisplayMode_===value)return;Unit.currentTimeDisplayMode_=value;Unit.dispatchEvent(new tr.b.Event('display-mode-changed'));}});Unit.didPreferredTimeDisplayUnitChange=function(){let largest=undefined;const els=tr.ui.b.findDeepElementsMatching(document.body,'tr-v-ui-preferred-display-unit');els.forEach(function(el){largest=max(largest,el.preferredTimeDisplayMode);});Unit.currentTimeDisplayMode=largest===undefined?TimeDisplayModes.ms:largest;};Unit.byName={};Unit.byJSONName={};Unit.fromJSON=function(object){if(typeof(object)==='string'){if(object.endsWith('+')){object=object.slice(0,object.length-1)+'_biggerIsBetter';}else if(object.endsWith('-')){object=object.slice(0,object.length-1)+'_smallerIsBetter';}
+const u=Unit.byJSONName[object];if(u)return u;}
 throw new Error(`Unrecognized unit "${object}"`);};Unit.define=function(params){const definedUnits=[];for(const improvementDirection of Object.values(ImprovementDirection)){const regularUnit=Unit.defineUnitVariant_(params,false,improvementDirection);const deltaUnit=Unit.defineUnitVariant_(params,true,improvementDirection);regularUnit.correspondingDeltaUnit=deltaUnit;deltaUnit.correspondingDeltaUnit=deltaUnit;definedUnits.push(regularUnit,deltaUnit);}
 const baseUnit=Unit.byName[params.baseUnitName];definedUnits.forEach(u=>u.baseUnit=baseUnit);};Unit.nameSuffixForImprovementDirection=function(improvementDirection){switch(improvementDirection){case ImprovementDirection.DONT_CARE:return'';case ImprovementDirection.BIGGER_IS_BETTER:return'_biggerIsBetter';case ImprovementDirection.SMALLER_IS_BETTER:return'_smallerIsBetter';default:throw new Error('Unknown improvement direction: '+improvementDirection);}};Unit.defineUnitVariant_=function(params,isDelta,improvementDirection){let nameSuffix=isDelta?'Delta':'';nameSuffix+=Unit.nameSuffixForImprovementDirection(improvementDirection);const unitName=params.baseUnitName+nameSuffix;const jsonName=params.baseJsonName+nameSuffix;if(Unit.byName[unitName]!==undefined){throw new Error('Unit \''+unitName+'\' already exists');}
 if(Unit.byJSONName[jsonName]!==undefined){throw new Error('JSON unit \''+jsonName+'\' alread exists');}
@@ -4421,7 +4556,7 @@
 this.r+','+this.g+','+
 this.b+','+alpha+')';}};return{Color,};});'use strict';tr.exportTo('tr.b',function(){function SinebowColorGenerator(opt_a,opt_brightness){this.a_=(opt_a===undefined)?1:opt_a;this.brightness_=(opt_brightness===undefined)?1:opt_brightness;this.colorIndex_=0;this.keyToColor={};}
 SinebowColorGenerator.prototype={colorForKey(key){if(!this.keyToColor[key]){this.keyToColor[key]=this.nextColor();}
-return this.keyToColor[key];},nextColor(){const components=SinebowColorGenerator.nthColor(this.colorIndex_++);return tr.b.Color.fromString(SinebowColorGenerator.calculateColor(components[0],components[1],components[2],this.a_,this.brightness_));}};SinebowColorGenerator.PHI=(1+Math.sqrt(5))/2;SinebowColorGenerator.sinebow_=function(h){h+=0.5;h=-h;let r=Math.sin(Math.PI*h);let g=Math.sin(Math.PI*(h+1/3));let b=Math.sin(Math.PI*(h+2/3));r*=r;g*=g;b*=b;const y=2*(0.2989*r+0.5870*g+0.1140*b);r/=y;g/=y;b/=y;return[256*r,256*g,256*b];};SinebowColorGenerator.nthColor=function(n){return SinebowColorGenerator.sinebow_(n*this.PHI);};SinebowColorGenerator.calculateColor=function(r,g,b,a,brightness){if(brightness<=1){r*=brightness;g*=brightness;b*=brightness;}else{r=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),r,255);g=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),g,255);b=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),b,255);}
+return this.keyToColor[key];},nextColor(){const components=SinebowColorGenerator.nthColor(this.colorIndex_++);return tr.b.Color.fromString(SinebowColorGenerator.calculateColor(components[0],components[1],components[2],this.a_,this.brightness_));}};SinebowColorGenerator.PHI=(1+Math.sqrt(5))/2;SinebowColorGenerator.sinebow=function(h){h+=0.5;h=-h;let r=Math.sin(Math.PI*h);let g=Math.sin(Math.PI*(h+1/3));let b=Math.sin(Math.PI*(h+2/3));r*=r;g*=g;b*=b;const y=2*(0.2989*r+0.5870*g+0.1140*b);r/=y;g/=y;b/=y;return[256*r,256*g,256*b];};SinebowColorGenerator.nthColor=function(n){return SinebowColorGenerator.sinebow(n*this.PHI);};SinebowColorGenerator.calculateColor=function(r,g,b,a,brightness){if(brightness<=1){r*=brightness;g*=brightness;b*=brightness;}else{r=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),r,255);g=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),g,255);b=tr.b.math.lerp(tr.b.math.normalize(brightness,1,2),b,255);}
 r=Math.round(r);g=Math.round(g);b=Math.round(b);return'rgba('+r+','+g+','+b+', '+a+')';};return{SinebowColorGenerator,};});'use strict';tr.exportTo('tr.b',function(){const numGeneralPurposeColorIds=23;const generalPurposeColors=new Array(numGeneralPurposeColorIds);const sinebowAlpha=1.0;const sinebowBrightness=1.5;const sinebowColorGenerator=new tr.b.SinebowColorGenerator(sinebowAlpha,sinebowBrightness);for(let i=0;i<numGeneralPurposeColorIds;i++){generalPurposeColors[i]=sinebowColorGenerator.nextColor();}
 const reservedColorsByName={thread_state_uninterruptible:new tr.b.Color(182,125,143),thread_state_iowait:new tr.b.Color(255,140,0),thread_state_running:new tr.b.Color(126,200,148),thread_state_runnable:new tr.b.Color(133,160,210),thread_state_sleeping:new tr.b.Color(240,240,240),thread_state_unknown:new tr.b.Color(199,155,125),background_memory_dump:new tr.b.Color(0,180,180),light_memory_dump:new tr.b.Color(0,0,180),detailed_memory_dump:new tr.b.Color(180,0,180),vsync_highlight_color:new tr.b.Color(0,0,255),generic_work:new tr.b.Color(125,125,125),good:new tr.b.Color(0,125,0),bad:new tr.b.Color(180,125,0),terrible:new tr.b.Color(180,0,0),black:new tr.b.Color(0,0,0),grey:new tr.b.Color(221,221,221),white:new tr.b.Color(255,255,255),yellow:new tr.b.Color(255,255,0),olive:new tr.b.Color(100,100,0),rail_response:new tr.b.Color(67,135,253),rail_animation:new tr.b.Color(244,74,63),rail_idle:new tr.b.Color(238,142,0),rail_load:new tr.b.Color(13,168,97),startup:new tr.b.Color(230,230,0),heap_dump_stack_frame:new tr.b.Color(128,128,128),heap_dump_object_type:new tr.b.Color(0,0,255),heap_dump_child_node_arrow:new tr.b.Color(204,102,0),cq_build_running:new tr.b.Color(255,255,119),cq_build_passed:new tr.b.Color(153,238,102),cq_build_failed:new tr.b.Color(238,136,136),cq_build_abandoned:new tr.b.Color(187,187,187),cq_build_attempt_runnig:new tr.b.Color(222,222,75),cq_build_attempt_passed:new tr.b.Color(103,218,35),cq_build_attempt_failed:new tr.b.Color(197,81,81)};const numReservedColorIds=Object.keys(reservedColorsByName).length;const numColorsPerVariant=numGeneralPurposeColorIds+numReservedColorIds;function ColorScheme(){}
 const paletteBase=[];paletteBase.push.apply(paletteBase,generalPurposeColors);paletteBase.push.apply(paletteBase,Object.values(reservedColorsByName));ColorScheme.colors=[];ColorScheme.properties={};ColorScheme.properties={numColorsPerVariant,};function pushVariant(func){const variantColors=paletteBase.map(func);ColorScheme.colors.push.apply(ColorScheme.colors,variantColors);}
@@ -4627,20 +4762,22 @@
 return process.findAllThreadsNamed('CrGpuMain').length>0;};ChromeGpuHelper.prototype={__proto__:tr.model.helpers.ChromeProcessHelper.prototype};return{ChromeGpuHelper,};});'use strict';tr.exportTo('tr.model.helpers',function(){const NET_CATEGORIES=new Set(['net','netlog','disabled-by-default-netlog','disabled-by-default-network']);class ChromeThreadHelper{constructor(thread){this.thread=thread;}
 getNetworkEvents(){const networkEvents=[];for(const slice of this.thread.asyncSliceGroup.slices){const categories=tr.b.getCategoryParts(slice.category);const isNetEvent=category=>NET_CATEGORIES.has(category);if(categories.filter(isNetEvent).length===0)continue;networkEvents.push(slice);}
 return networkEvents;}}
-return{ChromeThreadHelper,};});'use strict';tr.exportTo('tr.model.helpers',function(){const ChromeThreadHelper=tr.model.helpers.ChromeThreadHelper;function ChromeRendererHelper(modelHelper,process){tr.model.helpers.ChromeProcessHelper.call(this,modelHelper,process);this.mainThread_=process.findAtMostOneThreadNamed('CrRendererMain')||process.findAtMostOneThreadNamed('Chrome_InProcRendererThread');this.compositorThread_=process.findAtMostOneThreadNamed('Compositor');this.rasterWorkerThreads_=process.findAllThreadsMatching(function(t){if(t.name===undefined)return false;if(t.name.indexOf('CompositorTileWorker')===0)return true;if(t.name.indexOf('CompositorRasterWorker')===0)return true;return false;});if(!process.name){process.name=ChromeRendererHelper.PROCESS_NAME;}}
-ChromeRendererHelper.PROCESS_NAME='Renderer';ChromeRendererHelper.isRenderProcess=function(process){if(process.findAtMostOneThreadNamed('CrRendererMain'))return true;if(process.findAtMostOneThreadNamed('Compositor'))return true;return false;};ChromeRendererHelper.isTracingProcess=function(process){return process.labels!==undefined&&process.labels.length===1&&process.labels[0]==='chrome://tracing';};ChromeRendererHelper.prototype={__proto__:tr.model.helpers.ChromeProcessHelper.prototype,get mainThread(){return this.mainThread_;},get compositorThread(){return this.compositorThread_;},get rasterWorkerThreads(){return this.rasterWorkerThreads_;},get isChromeTracingUI(){return ChromeRendererHelper.isTracingProcess(this.process);},};return{ChromeRendererHelper,};});'use strict';tr.exportTo('tr.model.um',function(){class Segment extends tr.model.TimedEvent{constructor(start,duration){super(start);this.duration=duration;this.expectations_=[];}
+return{ChromeThreadHelper,};});'use strict';tr.exportTo('tr.model.helpers',function(){const ChromeThreadHelper=tr.model.helpers.ChromeThreadHelper;function ChromeRendererHelper(modelHelper,process){tr.model.helpers.ChromeProcessHelper.call(this,modelHelper,process);this.mainThread_=process.findAtMostOneThreadNamed('CrRendererMain')||process.findAtMostOneThreadNamed('Chrome_InProcRendererThread');this.compositorThread_=process.findAtMostOneThreadNamed('Compositor');this.rasterWorkerThreads_=process.findAllThreadsMatching(function(t){if(t.name===undefined)return false;if(t.name.startsWith('CompositorTileWorker'))return true;if(t.name.startsWith('CompositorRasterWorker'))return true;return false;});this.dedicatedWorkerThreads_=process.findAllThreadsMatching(function(t){return t.name&&t.name.startsWith('DedicatedWorker');});this.foregroundWorkerThreads_=process.findAllThreadsMatching(function(t){return t.name&&t.name.startsWith('ThreadPoolForegroundWorker');});if(!process.name){process.name=ChromeRendererHelper.PROCESS_NAME;}}
+ChromeRendererHelper.PROCESS_NAME='Renderer';ChromeRendererHelper.isRenderProcess=function(process){if(process.findAtMostOneThreadNamed('CrRendererMain'))return true;if(process.findAtMostOneThreadNamed('Compositor'))return true;return false;};ChromeRendererHelper.isTracingProcess=function(process){return process.labels!==undefined&&process.labels.length===1&&process.labels[0]==='chrome://tracing';};ChromeRendererHelper.prototype={__proto__:tr.model.helpers.ChromeProcessHelper.prototype,get mainThread(){return this.mainThread_;},get compositorThread(){return this.compositorThread_;},get rasterWorkerThreads(){return this.rasterWorkerThreads_;},get dedicatedWorkerThreads(){return this.dedicatedWorkerThreads_;},get foregroundWorkerThreads(){return this.foregroundWorkerThreads_;},get isChromeTracingUI(){return ChromeRendererHelper.isTracingProcess(this.process);},};return{ChromeRendererHelper,};});'use strict';tr.exportTo('tr.model.um',function(){class Segment extends tr.model.TimedEvent{constructor(start,duration){super(start);this.duration=duration;this.expectations_=[];}
 get expectations(){return this.expectations_;}
 clone(){const clone=new Segment(this.start,this.duration);clone.expectations.push(...this.expectations);return clone;}
 addSegment(other){this.duration+=other.duration;this.expectations.push(...other.expectations);}}
-return{Segment,};});'use strict';tr.exportTo('tr.model.helpers',function(){const GESTURE_EVENT='SyntheticGestureController::running';const IR_REG_EXP=/Interaction\.([^/]+)(\/[^/]*)?$/;const ChromeRendererHelper=tr.model.helpers.ChromeRendererHelper;class TelemetryHelper{constructor(modelHelper){this.modelHelper=modelHelper;this.renderersWithIR_=undefined;this.segments_=undefined;this.uiSegments_=undefined;}
+return{Segment,};});'use strict';tr.exportTo('tr.model.helpers',function(){const GESTURE_EVENT='SyntheticGestureController::running';const IR_REG_EXP=/Interaction\.([^/]+)(\/[^/]*)?$/;const ChromeRendererHelper=tr.model.helpers.ChromeRendererHelper;class TelemetryHelper{constructor(modelHelper){this.modelHelper=modelHelper;this.renderersWithIR_=undefined;this.irSegments_=undefined;this.uiSegments_=undefined;this.animationSegments_=undefined;}
 get renderersWithIR(){this.findIRs_();return this.renderersWithIR_;}
-get segments(){this.findIRs_();return this.segments_;}
+get irSegments(){this.findIRs_();return this.irSegments_;}
 get uiSegments(){this.findIRs_();return this.uiSegments_;}
-findIRs_(){if(this.segments_!==undefined)return;this.renderersWithIR_=[];const gestureEvents=[];const interactionRecords=[];const processes=Object.values(this.modelHelper.rendererHelpers).concat(this.modelHelper.browserHelpers).map(processHelper=>processHelper.process);for(const process of processes){let foundIR=false;for(const thread of Object.values(process.threads)){for(const slice of thread.asyncSliceGroup.slices){if(slice.title===GESTURE_EVENT){gestureEvents.push(slice);}else if(IR_REG_EXP.test(slice.title)){interactionRecords.push(slice);foundIR=true;}}}
+get animationSegments(){if(this.animationSegments_===undefined){const model=this.modelHelper.model;this.animationSegments_=model.userModel.segments.filter(segment=>segment.expectations.find(ue=>ue instanceof tr.model.um.AnimationExpectation));this.animationSegments_.sort((x,y)=>x.start-y.start);}
+return this.animationSegments_;}
+findIRs_(){if(this.irSegments_!==undefined)return;this.renderersWithIR_=[];const gestureEvents=[];const interactionRecords=[];const processes=Object.values(this.modelHelper.rendererHelpers).concat(this.modelHelper.browserHelpers).map(processHelper=>processHelper.process);for(const process of processes){let foundIR=false;for(const thread of Object.values(process.threads)){for(const slice of thread.asyncSliceGroup.slices){if(slice.title===GESTURE_EVENT){gestureEvents.push(slice);}else if(IR_REG_EXP.test(slice.title)){interactionRecords.push(slice);foundIR=true;}}}
 if(foundIR&&ChromeRendererHelper.isRenderProcess(process)&&!ChromeRendererHelper.isTracingProcess(process)){this.renderersWithIR_.push(new ChromeRendererHelper(this.modelHelper,process));}}
-this.segments_=[];this.uiSegments_=[];for(const ir of interactionRecords){const parts=IR_REG_EXP.exec(ir.title);let gestureEventFound=false;if(parts[1].startsWith('Gesture_')){for(const gestureEvent of gestureEvents){if(ir.boundsRange.intersectsRangeInclusive(gestureEvent.boundsRange)){this.segments_.push(new tr.model.um.Segment(gestureEvent.start,gestureEvent.duration));gestureEventFound=true;break;}}}else if(parts[1].startsWith('ui_')){this.uiSegments_.push(new tr.model.um.Segment(ir.start,ir.duration));}
-if(!gestureEventFound){this.segments_.push(new tr.model.um.Segment(ir.start,ir.duration));}}
-this.segments_.sort((x,y)=>x.start-y.start);this.uiSegments_.sort((x,y)=>x.start-y.start);}}
+this.irSegments_=[];this.uiSegments_=[];for(const ir of interactionRecords){const parts=IR_REG_EXP.exec(ir.title);let gestureEventFound=false;if(parts[1].startsWith('Gesture_')){for(const gestureEvent of gestureEvents){if(ir.boundsRange.intersectsRangeInclusive(gestureEvent.boundsRange)){this.irSegments_.push(new tr.model.um.Segment(gestureEvent.start,gestureEvent.duration));gestureEventFound=true;break;}}}else if(parts[1].startsWith('ui_')){this.uiSegments_.push(new tr.model.um.Segment(ir.start,ir.duration));}
+if(!gestureEventFound){this.irSegments_.push(new tr.model.um.Segment(ir.start,ir.duration));}}
+this.irSegments_.sort((x,y)=>x.start-y.start);this.uiSegments_.sort((x,y)=>x.start-y.start);}}
 return{TelemetryHelper,};});'use strict';tr.exportTo('tr.model.helpers',function(){function findChromeBrowserProcesses(model){return model.getAllProcesses(tr.model.helpers.ChromeBrowserHelper.isBrowserProcess);}
 function findChromeRenderProcesses(model){return model.getAllProcesses(tr.model.helpers.ChromeRendererHelper.isRenderProcess);}
 function findChromeGpuProcess(model){const gpuProcesses=model.getAllProcesses(tr.model.helpers.ChromeGpuHelper.isGpuProcess);if(gpuProcesses.length!==1)return undefined;return gpuProcesses[0];}
@@ -4681,15 +4818,17 @@
 const INPUT_GSU='InputLatency::GestureScrollUpdate';if(this.title===INPUT_GSU){this.addScrollUpdateEvents(rendererHelper);}},get associatedEvents(){if(this.associatedEvents_.length!==0){return this.associatedEvents_;}
 const modelIndices=this.startThread.parent.model.modelIndices;const flowEvents=modelIndices.getFlowEventsWithId(this.id);if(flowEvents.length===0){return this.associatedEvents_;}
 const sourceSlices=this.addDirectlyAssociatedEvents(flowEvents);const rendererHelper=this.getRendererHelper(sourceSlices);this.addOtherCausallyRelatedEvents(rendererHelper,sourceSlices,flowEvents);return this.associatedEvents_;},get inputLatency(){if(!('data'in this.args))return undefined;const data=this.args.data;const endTimeComp=data[END_COMP_NAME]||data[LEGACY_END_COMP_NAME];if(endTimeComp===undefined)return undefined;let latency=0;const endTime=endTimeComp.time;if(ORIGINAL_COMP_NAME in data){latency=endTime-data[ORIGINAL_COMP_NAME].time;}else if(UI_COMP_NAME in data){latency=endTime-data[UI_COMP_NAME].time;}else if(BEGIN_COMP_NAME in data){latency=endTime-data[BEGIN_COMP_NAME].time;}else{throw new Error('No valid begin latency component');}
-return latency;}};const eventTypeNames=['Char','ContextMenu','GestureClick','GestureFlingCancel','GestureFlingStart','GestureScrollBegin','GestureScrollEnd','GestureScrollUpdate','GestureShowPress','GestureTap','GestureTapCancel','GestureTapDown','GesturePinchBegin','GesturePinchEnd','GesturePinchUpdate','KeyDown','KeyUp','MouseDown','MouseEnter','MouseLeave','MouseMove','MouseUp','MouseWheel','RawKeyDown','ScrollUpdate','TouchCancel','TouchEnd','TouchMove','TouchStart'];const allTypeNames=['InputLatency'];eventTypeNames.forEach(function(eventTypeName){allTypeNames.push('InputLatency:'+eventTypeName);allTypeNames.push('InputLatency::'+eventTypeName);});AsyncSlice.subTypes.register(InputLatencyAsyncSlice,{typeNames:allTypeNames,categoryParts:['latencyInfo']});return{InputLatencyAsyncSlice,INPUT_EVENT_TYPE_NAMES,};});'use strict';tr.exportTo('tr.e.chrome',function(){const SAME_AS_PARENT='same-as-parent';const TITLES_FOR_USER_FRIENDLY_CATEGORY={composite:['CompositingInputsUpdater::update','ThreadProxy::SetNeedsUpdateLayers','LayerTreeHost::DoUpdateLayers','LayerTreeHost::UpdateLayers::BuildPropertyTrees','LocalFrameView::pushPaintArtifactToCompositor','LocalFrameView::updateCompositedSelectionIfNeeded','LocalFrameView::RunCompositingLifecyclePhase','UpdateLayerTree',],gc:['minorGC','majorGC','MajorGC','MinorGC','V8.GCScavenger','V8.GCIncrementalMarking','V8.GCIdleNotification','V8.GCContext','V8.GCCompactor','V8GCController::traceDOMWrappers',],iframe_creation:['WebLocalFrameImpl::createChildframe',],imageDecode:['Decode Image','ImageFrameGenerator::decode','ImageFrameGenerator::decodeAndScale','ImageResourceContent::updateImage',],input:['HitTest','ScrollableArea::scrollPositionChanged','EventHandler::handleMouseMoveEvent',],layout:['IntersectionObserverController::computeTrackedIntersectionObservations','LocalFrameView::invalidateTree','LocalFrameView::layout','LocalFrameView::performLayout','LocalFrameView::performPostLayoutTasks','LocalFrameView::performPreLayoutTasks','LocalFrameView::RunStyleAndLayoutCompositingPhases','Layout','PaintLayer::updateLayerPositionsAfterLayout','ResourceLoadPriorityOptimizer::updateAllImageResourcePriorities','WebViewImpl::updateAllLifecyclePhases','WebViewImpl::beginFrame',],parseHTML:['BackgroundHTMLParser::pumpTokenizer','BackgroundHTMLParser::sendTokensToMainThread','HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser','HTMLDocumentParser::documentElementAvailable','HTMLDocumentParser::notifyPendingTokenizedChunks','HTMLDocumentParser::processParsedChunkFromBackgroundParser','HTMLDocumentParser::processTokenizedChunkFromBackgroundParser','ParseHTML',],raster:['DisplayListRasterSource::PerformSolidColorAnalysis','Picture::Raster','RasterBufferImpl::Playback','RasterTask','RasterizerTaskImpl::RunOnWorkerThread','SkCanvas::drawImageRect()','SkCanvas::drawPicture()','SkCanvas::drawTextBlob()','TileTaskWorkerPool::PlaybackToMemory',],record:['Canvas2DLayerBridge::flushRecordingOnly','CompositingInputsUpdater::update','CompositingRequirementsUpdater::updateRecursive','ContentLayerDelegate::paintContents','DisplayItemList::Finalize','LocalFrameView::RunPaintLifecyclePhase','LocalFrameView::RunPrePaintLifecyclePhase','Paint','PaintController::commitNewDisplayItems','PaintLayerCompositor::updateIfNeededRecursive','Picture::Record','PictureLayer::Update',],style:['CSSParserImpl::parseStyleSheet.parse','CSSParserImpl::parseStyleSheet.tokenize','Document::rebuildLayoutTree','Document::recalcStyle','Document::updateActiveStyle','Document::updateStyle','Document::updateStyleInvalidationIfNeeded','LocalFrameView::updateStyleAndLayoutIfNeededRecursive','ParseAuthorStyleSheet','RuleSet::addRulesFromSheet','StyleElement::processStyleSheet','StyleEngine::createResolver','StyleEngine::updateActiveStyleSheets','StyleSheetContents::parseAuthorStyleSheet','UpdateLayoutTree',],script_parse_and_compile:['V8.CompileFullCode','V8.NewContext','V8.Parse','V8.ParseLazy','V8.RecompileSynchronous','V8.ScriptCompiler','v8.compile','v8.parseOnBackground',],script_execute:['EvaluateScript','FunctionCall','HTMLParserScriptRunner ExecuteScript','V8.Execute','V8.RunMicrotasks','V8.Task','WindowProxy::initialize','v8.callFunction','v8.run',],resource_loading:['RenderFrameImpl::didFinishDocumentLoad','RenderFrameImpl::didFinishLoad','Resource::appendData','ResourceDispatcher::OnReceivedData','ResourceDispatcher::OnReceivedResponse','ResourceDispatcher::OnRequestComplete','ResourceFetcher::requestResource','WebURLLoaderImpl::Context::Cancel','WebURLLoaderImpl::Context::OnCompletedRequest','WebURLLoaderImpl::Context::OnReceivedData','WebURLLoaderImpl::Context::OnReceivedRedirect','WebURLLoaderImpl::Context::OnReceivedResponse','WebURLLoaderImpl::Context::Start','WebURLLoaderImpl::loadAsynchronously','WebURLLoaderImpl::loadSynchronously','content::mojom::URLLoaderClient',],renderer_misc:['DecodeFont','ThreadState::completeSweep',],v8_runtime:[],[SAME_AS_PARENT]:['SyncChannel::Send',]};const COLOR_FOR_USER_FRIENDLY_CATEGORY=new tr.b.SinebowColorGenerator();const USER_FRIENDLY_CATEGORY_FOR_TITLE=new Map();for(const category in TITLES_FOR_USER_FRIENDLY_CATEGORY){TITLES_FOR_USER_FRIENDLY_CATEGORY[category].forEach(function(title){USER_FRIENDLY_CATEGORY_FOR_TITLE.set(title,category);});}
+return latency;}};const eventTypeNames=['Char','ContextMenu','GestureClick','GestureFlingCancel','GestureFlingStart','GestureScrollBegin','GestureScrollEnd','GestureScrollUpdate','GestureShowPress','GestureTap','GestureTapCancel','GestureTapDown','GesturePinchBegin','GesturePinchEnd','GesturePinchUpdate','KeyDown','KeyUp','MouseDown','MouseEnter','MouseLeave','MouseMove','MouseUp','MouseWheel','RawKeyDown','ScrollUpdate','TouchCancel','TouchEnd','TouchMove','TouchStart'];const allTypeNames=['InputLatency'];eventTypeNames.forEach(function(eventTypeName){allTypeNames.push('InputLatency:'+eventTypeName);allTypeNames.push('InputLatency::'+eventTypeName);});AsyncSlice.subTypes.register(InputLatencyAsyncSlice,{typeNames:allTypeNames,categoryParts:['latencyInfo']});return{InputLatencyAsyncSlice,INPUT_EVENT_TYPE_NAMES,};});'use strict';tr.exportTo('tr.e.chrome',function(){const SAME_AS_PARENT='same-as-parent';const TITLES_FOR_USER_FRIENDLY_CATEGORY={composite:['CompositingInputsUpdater::update','ThreadProxy::SetNeedsUpdateLayers','LayerTreeHost::DoUpdateLayers','LayerTreeHost::UpdateLayers::BuildPropertyTrees','LocalFrameView::pushPaintArtifactToCompositor','LocalFrameView::updateCompositedSelectionIfNeeded','LocalFrameView::RunCompositingLifecyclePhase','UpdateLayerTree',],gc:['minorGC','majorGC','MajorGC','MinorGC','V8.GCScavenger','V8.GCIncrementalMarking','V8.GCIdleNotification','V8.GCContext','V8.GCCompactor','V8GCController::traceDOMWrappers',],iframe_creation:['WebLocalFrameImpl::createChildframe',],imageDecode:['Decode Image','ImageFrameGenerator::decode','ImageFrameGenerator::decodeAndScale','ImageFrameGenerator::decodeToYUV','ImageResourceContent::updateImage',],input:['HitTest','ScrollableArea::scrollPositionChanged','EventHandler::handleMouseMoveEvent',],layout:['IntersectionObserverController::computeTrackedIntersectionObservations','LocalFrameView::invalidateTree','LocalFrameView::layout','LocalFrameView::performLayout','LocalFrameView::performPostLayoutTasks','LocalFrameView::performPreLayoutTasks','LocalFrameView::RunStyleAndLayoutCompositingPhases','Layout','PaintLayer::updateLayerPositionsAfterLayout','ResourceLoadPriorityOptimizer::updateAllImageResourcePriorities','WebViewImpl::updateAllLifecyclePhases','WebViewImpl::beginFrame',],parseHTML:['BackgroundHTMLParser::pumpTokenizer','BackgroundHTMLParser::sendTokensToMainThread','HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser','HTMLDocumentParser::documentElementAvailable','HTMLDocumentParser::notifyPendingTokenizedChunks','HTMLDocumentParser::processParsedChunkFromBackgroundParser','HTMLDocumentParser::processTokenizedChunkFromBackgroundParser','ParseHTML',],raster:['DisplayListRasterSource::PerformSolidColorAnalysis','Picture::Raster','RasterBufferImpl::Playback','RasterTask','RasterizerTaskImpl::RunOnWorkerThread','SkCanvas::drawImageRect()','SkCanvas::drawPicture()','SkCanvas::drawTextBlob()','TileTaskWorkerPool::PlaybackToMemory',],record:['Canvas2DLayerBridge::flushRecordingOnly','CompositingInputsUpdater::update','CompositingRequirementsUpdater::updateRecursive','ContentLayerDelegate::paintContents','DisplayItemList::Finalize','LocalFrameView::RunPaintLifecyclePhase','LocalFrameView::RunPrePaintLifecyclePhase','Paint','PaintController::commitNewDisplayItems','PaintLayerCompositor::updateIfNeededRecursive','Picture::Record','PictureLayer::Update',],style:['CSSParserImpl::parseStyleSheet.parse','CSSParserImpl::parseStyleSheet.tokenize','Document::rebuildLayoutTree','Document::recalcStyle','Document::updateActiveStyle','Document::updateStyle','Document::updateStyleInvalidationIfNeeded','LocalFrameView::updateStyleAndLayoutIfNeededRecursive','ParseAuthorStyleSheet','RuleSet::addRulesFromSheet','StyleElement::processStyleSheet','StyleEngine::createResolver','StyleEngine::updateActiveStyleSheets','StyleSheetContents::parseAuthorStyleSheet','UpdateLayoutTree',],script_parse_and_compile:['V8.CompileFullCode','V8.NewContext','V8.Parse','V8.ParseLazy','V8.RecompileSynchronous','V8.ScriptCompiler','v8.compile','v8.parseOnBackground',],script_execute:['EvaluateScript','FunctionCall','HTMLParserScriptRunner ExecuteScript','V8.Execute','V8.RunMicrotasks','V8.Task','WindowProxy::initialize','v8.callFunction','v8.run',],resource_loading:['RenderFrameImpl::didFinishDocumentLoad','RenderFrameImpl::didFinishLoad','Resource::appendData','ResourceDispatcher::OnReceivedData','ResourceDispatcher::OnReceivedResponse','ResourceDispatcher::OnRequestComplete','ResourceFetcher::requestResource','WebURLLoaderImpl::Context::Cancel','WebURLLoaderImpl::Context::OnCompletedRequest','WebURLLoaderImpl::Context::OnReceivedData','WebURLLoaderImpl::Context::OnReceivedRedirect','WebURLLoaderImpl::Context::OnReceivedResponse','WebURLLoaderImpl::Context::Start','WebURLLoaderImpl::loadAsynchronously','WebURLLoaderImpl::loadSynchronously','content::mojom::URLLoaderClient',],renderer_misc:['DecodeFont','ThreadState::completeSweep',],v8_runtime:[],[SAME_AS_PARENT]:['SyncChannel::Send',]};const COLOR_FOR_USER_FRIENDLY_CATEGORY=new tr.b.SinebowColorGenerator();const USER_FRIENDLY_CATEGORY_FOR_TITLE=new Map();for(const category in TITLES_FOR_USER_FRIENDLY_CATEGORY){TITLES_FOR_USER_FRIENDLY_CATEGORY[category].forEach(function(title){USER_FRIENDLY_CATEGORY_FOR_TITLE.set(title,category);});}
 const USER_FRIENDLY_CATEGORY_FOR_EVENT_CATEGORY={netlog:'net',overhead:'overhead',startup:'startup',gpu:'gpu',};function ChromeUserFriendlyCategoryDriver(){}
 ChromeUserFriendlyCategoryDriver.fromEvent=function(event){let userFriendlyCategory=USER_FRIENDLY_CATEGORY_FOR_TITLE.get(event.title);if(userFriendlyCategory){if(userFriendlyCategory===SAME_AS_PARENT){if(event.parentSlice){return ChromeUserFriendlyCategoryDriver.fromEvent(event.parentSlice);}}else{return userFriendlyCategory;}}
 const eventCategoryParts=tr.b.getCategoryParts(event.category);for(let i=0;i<eventCategoryParts.length;++i){const eventCategory=eventCategoryParts[i];userFriendlyCategory=USER_FRIENDLY_CATEGORY_FOR_EVENT_CATEGORY[eventCategory];if(userFriendlyCategory){return userFriendlyCategory;}}
 return'other';};ChromeUserFriendlyCategoryDriver.getColor=function(ufc){return COLOR_FOR_USER_FRIENDLY_CATEGORY.colorForKey(ufc);};ChromeUserFriendlyCategoryDriver.ALL_TITLES=['other'];for(const category in TITLES_FOR_USER_FRIENDLY_CATEGORY){if(category===SAME_AS_PARENT)continue;ChromeUserFriendlyCategoryDriver.ALL_TITLES.push(category);}
 for(const category of Object.values(USER_FRIENDLY_CATEGORY_FOR_EVENT_CATEGORY)){ChromeUserFriendlyCategoryDriver.ALL_TITLES.push(category);}
 ChromeUserFriendlyCategoryDriver.ALL_TITLES.sort();for(const category of ChromeUserFriendlyCategoryDriver.ALL_TITLES){ChromeUserFriendlyCategoryDriver.getColor(category);}
-return{ChromeUserFriendlyCategoryDriver,};});'use strict';tr.exportTo('tr.model',function(){return{BROWSER_PROCESS_PID_REF:-1,OBJECT_DEFAULT_SCOPE:'ptr',LOCAL_ID_PHASES:new Set(['N','D','O','(',')'])};});'use strict';tr.exportTo('tr.e.audits',function(){const Auditor=tr.c.Auditor;function ChromeAuditor(model){Auditor.call(this,model);const modelHelper=this.model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(modelHelper&&modelHelper.browserHelper){this.modelHelper=modelHelper;}else{this.modelHelper=undefined;}}
-ChromeAuditor.prototype={__proto__:Auditor.prototype,runAnnotate(){if(!this.modelHelper)return;for(const pid in this.modelHelper.rendererHelpers){const rendererHelper=this.modelHelper.rendererHelpers[pid];if(rendererHelper.isChromeTracingUI){rendererHelper.process.important=false;}}},installUserFriendlyCategoryDriverIfNeeded(){this.model.addUserFriendlyCategoryDriver(tr.e.chrome.ChromeUserFriendlyCategoryDriver);},runAudit(){if(!this.modelHelper)return;this.model.replacePIDRefsInPatchups(tr.model.BROWSER_PROCESS_PID_REF,this.modelHelper.browserProcess.pid);this.model.applyObjectRefPatchups();}};Auditor.register(ChromeAuditor);return{ChromeAuditor,};});'use strict';tr.exportTo('tr.e.chrome',function(){const KNOWN_PROPERTIES={absX:1,absY:1,address:1,anonymous:1,childNeeds:1,children:1,classNames:1,col:1,colSpan:1,float:1,height:1,htmlId:1,name:1,posChildNeeds:1,positioned:1,positionedMovement:1,relX:1,relY:1,relativePositioned:1,row:1,rowSpan:1,selfNeeds:1,stickyPositioned:1,tag:1,width:1};function LayoutObject(snapshot,args){this.snapshot_=snapshot;this.id_=args.address;this.name_=args.name;this.childLayoutObjects_=[];this.otherProperties_={};this.tag_=args.tag;this.relativeRect_=tr.b.math.Rect.fromXYWH(args.relX,args.relY,args.width,args.height);this.absoluteRect_=tr.b.math.Rect.fromXYWH(args.absX,args.absY,args.width,args.height);this.isFloat_=args.float;this.isStickyPositioned_=args.stickyPositioned;this.isPositioned_=args.positioned;this.isRelativePositioned_=args.relativePositioned;this.isAnonymous_=args.anonymous;this.htmlId_=args.htmlId;this.classNames_=args.classNames;this.needsLayoutReasons_=[];if(args.selfNeeds){this.needsLayoutReasons_.push('self');}
+return{ChromeUserFriendlyCategoryDriver,};});'use strict';tr.exportTo('tr.model',function(){return{BROWSER_PROCESS_PID_REF:-1,OBJECT_DEFAULT_SCOPE:'ptr',LOCAL_ID_PHASES:new Set(['N','D','O','(',')'])};});'use strict';tr.exportTo('tr.e.audits',function(){const Auditor=tr.c.Auditor;const Alert=tr.model.Alert;const EventInfo=tr.model.EventInfo;function ChromeAuditor(model){Auditor.call(this,model);const modelHelper=this.model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(modelHelper&&modelHelper.browserHelper){this.modelHelper=modelHelper;}else{this.modelHelper=undefined;}}
+function getMissedFrameAlerts(rendererHelpers){const alerts=[];for(const rendererHelper of rendererHelpers){if(!rendererHelper.compositorThread)continue;const thread=rendererHelper.compositorThread;const asyncSlices=Object.values(thread.asyncSliceGroup.slices);for(const slice of asyncSlices){if(slice.title!=='PipelineReporter'||!slice.args.termination_status||slice.args.termination_status!=='missed_frame')continue;const alertSlices=[slice].concat(slice.subSlices);alerts.push(new Alert(new EventInfo('Missed Frame','Frame was not submitted before deadline.'),slice.start,alertSlices));}}
+return alerts;}
+ChromeAuditor.prototype={__proto__:Auditor.prototype,runAnnotate(){if(!this.modelHelper)return;for(const pid in this.modelHelper.rendererHelpers){const rendererHelper=this.modelHelper.rendererHelpers[pid];if(rendererHelper.isChromeTracingUI){rendererHelper.process.important=false;}}},installUserFriendlyCategoryDriverIfNeeded(){this.model.addUserFriendlyCategoryDriver(tr.e.chrome.ChromeUserFriendlyCategoryDriver);},runAudit(){if(!this.modelHelper)return;this.model.replacePIDRefsInPatchups(tr.model.BROWSER_PROCESS_PID_REF,this.modelHelper.browserProcess.pid);this.model.applyObjectRefPatchups();const alerts=getMissedFrameAlerts(Object.values(this.modelHelper.rendererHelpers));this.model.alerts=this.model.alerts.concat(alerts);}};Auditor.register(ChromeAuditor);return{ChromeAuditor,};});'use strict';tr.exportTo('tr.e.chrome',function(){const KNOWN_PROPERTIES={absX:1,absY:1,address:1,anonymous:1,childNeeds:1,children:1,classNames:1,col:1,colSpan:1,float:1,height:1,htmlId:1,name:1,posChildNeeds:1,positioned:1,positionedMovement:1,relX:1,relY:1,relativePositioned:1,row:1,rowSpan:1,selfNeeds:1,stickyPositioned:1,tag:1,width:1};function LayoutObject(snapshot,args){this.snapshot_=snapshot;this.id_=args.address;this.name_=args.name;this.childLayoutObjects_=[];this.otherProperties_={};this.tag_=args.tag;this.relativeRect_=tr.b.math.Rect.fromXYWH(args.relX,args.relY,args.width,args.height);this.absoluteRect_=tr.b.math.Rect.fromXYWH(args.absX,args.absY,args.width,args.height);this.isFloat_=args.float;this.isStickyPositioned_=args.stickyPositioned;this.isPositioned_=args.positioned;this.isRelativePositioned_=args.relativePositioned;this.isAnonymous_=args.anonymous;this.htmlId_=args.htmlId;this.classNames_=args.classNames;this.needsLayoutReasons_=[];if(args.selfNeeds){this.needsLayoutReasons_.push('self');}
 if(args.childNeeds){this.needsLayoutReasons_.push('child');}
 if(args.posChildNeeds){this.needsLayoutReasons_.push('positionedChild');}
 if(args.positionedMovement){this.needsLayoutReasons_.push('positionedMovement');}
@@ -4848,7 +4987,8 @@
 const marker=new ClockSyncMarker(domainId,startTs,opt_endTs);if(!this.markersBySyncId_.has(syncId)){this.markersBySyncId_.set(syncId,[marker]);return;}
 const markers=this.markersBySyncId_.get(syncId);if(markers.length===2){throw new Error('Clock sync with ID "'+syncId+'" is already '+'complete - cannot add a third clock sync marker to it.');}
 if(markers[0].domainId===domainId){throw new Error('A clock domain cannot sync with itself.');}
-markers.push(marker);this.onSyncCompleted_(markers[0],marker);},get markersBySyncId(){return this.markersBySyncId_;},get domainsSeen(){return this.domainsSeen_;},getModelTimeTransformer(domainId){this.onDomainSeen_(domainId);if(!this.modelDomainId_){this.selectModelDomainId_();}
+markers.push(marker);this.onSyncCompleted_(markers[0],marker);},get completeSyncIds(){const completeSyncIds=[];for(const[syncId,markers]of this.markersBySyncId){if(markers.length===2)completeSyncIds.push(syncId);}
+return completeSyncIds;},get markersBySyncId(){return this.markersBySyncId_;},get domainsSeen(){return this.domainsSeen_;},getModelTimeTransformer(domainId){this.onDomainSeen_(domainId);if(!this.modelDomainId_){this.selectModelDomainId_();}
 return this.getTimeTransformerRaw_(domainId,this.modelDomainId_).fn;},getTimeTransformerError(fromDomainId,toDomainId){this.onDomainSeen_(fromDomainId);this.onDomainSeen_(toDomainId);return this.getTimeTransformerRaw_(fromDomainId,toDomainId).error;},getTimeTransformerRaw_(fromDomainId,toDomainId){const transformer=this.getTransformerBetween_(fromDomainId,toDomainId);if(!transformer){throw new Error('No clock sync markers exist pairing clock domain "'+
 fromDomainId+'" '+'with target clock domain "'+
 toDomainId+'".');}
@@ -4944,8 +5084,9 @@
 return slice;},pushCompleteSlice(category,title,ts,duration,tts,cpuDuration,opt_args,opt_argsStripped,opt_colorId,opt_bindId){const colorId=opt_colorId||ColorScheme.getColorIdForGeneralPurposeString(title);const sliceConstructorSubTypes=this.sliceConstructorSubTypes;const sliceType=sliceConstructorSubTypes.getConstructor(category,title);const slice=new sliceType(category,title,colorId,ts,opt_args?opt_args:{},duration,tts,cpuDuration,opt_argsStripped,opt_bindId);if(duration===undefined){slice.didNotFinish=true;}
 this.pushSlice(slice);return slice;},autoCloseOpenSlices(){this.updateBounds();const maxTimestamp=this.bounds.max;for(let sI=0;sI<this.slices.length;sI++){const slice=this.slices[sI];if(slice.didNotFinish){slice.duration=maxTimestamp-slice.start;}}
 this.openPartialSlices_=[];},shiftTimestampsForward(amount){for(let sI=0;sI<this.slices.length;sI++){const slice=this.slices[sI];slice.start=(slice.start+amount);}},updateBounds(){this.bounds.reset();for(let i=0;i<this.slices.length;i++){this.bounds.addValue(this.slices[i].start);this.bounds.addValue(this.slices[i].end);}},copySlice(slice){const sliceConstructorSubTypes=this.sliceConstructorSubTypes;const sliceType=sliceConstructorSubTypes.getConstructor(slice.category,slice.title);const newSlice=new sliceType(slice.category,slice.title,slice.colorId,slice.start,slice.args,slice.duration,slice.cpuStart,slice.cpuDuration);newSlice.didNotFinish=slice.didNotFinish;return newSlice;},*findTopmostSlicesInThisContainer(eventPredicate,opt_this){if(!this.haveTopLevelSlicesBeenBuilt){throw new Error('Nope');}
-for(const s of this.topLevelSlices){yield*s.findTopmostSlicesRelativeToThisSlice(eventPredicate);}},*childEvents(){yield*this.slices;},*childEventContainers(){},*getDescendantEventsInSortedRanges(ranges,opt_containerPredicate){if(opt_containerPredicate===undefined||opt_containerPredicate(this)){let rangeIndex=0;let range=ranges[rangeIndex];for(const event of this.childEvents()){while(event.start>range.max){rangeIndex++;if(rangeIndex>=ranges.length)return;range=ranges[rangeIndex];}
-if(event.end>=range.min)yield event;}}},getSlicesOfName(title){const slices=[];for(let i=0;i<this.slices.length;i++){if(this.slices[i].title===title){slices.push(this.slices[i]);}}
+for(const s of this.topLevelSlices){yield*s.findTopmostSlicesRelativeToThisSlice(eventPredicate);}},*childEvents(){yield*this.slices;},*childEventContainers(){},*getDescendantEventsInSortedRanges(ranges,opt_containerPredicate){if(ranges.length===0||(opt_containerPredicate!==undefined&&!opt_containerPredicate(this))){return;}
+let rangeIndex=0;let range=ranges[rangeIndex];for(const event of this.childEvents()){while(event.start>range.max){rangeIndex++;if(rangeIndex>=ranges.length)return;range=ranges[rangeIndex];}
+if(event.end>=range.min)yield event;}},getSlicesOfName(title){const slices=[];for(let i=0;i<this.slices.length;i++){if(this.slices[i].title===title){slices.push(this.slices[i]);}}
 return slices;},iterSlicesInTimeRange(callback,start,end){const ret=[];tr.b.iterateOverIntersectingIntervals(this.topLevelSlices,function(s){return s.start;},function(s){return s.duration;},start,end,function(topLevelSlice){callback(topLevelSlice);for(const slice of topLevelSlice.enumerateAllDescendents()){callback(slice);}});return ret;},findFirstSlice(){if(!this.haveTopLevelSlicesBeenBuilt){throw new Error('Nope');}
 if(0===this.slices.length)return undefined;return this.slices[0];},findSliceAtTs(ts){if(!this.haveTopLevelSlicesBeenBuilt)throw new Error('Nope');let i=tr.b.findIndexInSortedClosedIntervals(this.topLevelSlices,getSliceLo,getSliceHi,ts);if(i===-1||i===this.topLevelSlices.length){return undefined;}
 let curSlice=this.topLevelSlices[i];while(true){i=tr.b.findIndexInSortedClosedIntervals(curSlice.subSlices,getSliceLo,getSliceHi,ts);if(i===-1||i===curSlice.subSlices.length){return curSlice;}
@@ -4989,7 +5130,8 @@
 for(let i=0;i<this.kernelSliceGroup.length;i++){categoriesDict[this.kernelSliceGroup.slices[i].category]=true;}
 for(let i=0;i<this.asyncSliceGroup.length;i++){categoriesDict[this.asyncSliceGroup.slices[i].category]=true;}
 if(this.samples_){for(let i=0;i<this.samples_.length;i++){categoriesDict[this.samples_[i].category]=true;}}},autoCloseOpenSlices(){this.sliceGroup.autoCloseOpenSlices();this.asyncSliceGroup.autoCloseOpenSlices();this.kernelSliceGroup.autoCloseOpenSlices();},mergeKernelWithUserland(){if(this.kernelSliceGroup.length>0){const newSlices=SliceGroup.merge(this.sliceGroup,this.kernelSliceGroup);this.sliceGroup.slices=newSlices.slices;this.kernelSliceGroup=new SliceGroup(this);this.updateBounds();}},createSubSlices(){this.sliceGroup.createSubSlices();this.samples_=this.parent.model.samples.filter(sample=>sample.thread===this);},get userFriendlyName(){return this.name||this.tid;},get userFriendlyDetails(){return'tid: '+this.tid+
-(this.name?', name: '+this.name:'');},getSettingsKey(){if(!this.name)return undefined;const parentKey=this.parent.getSettingsKey();if(!parentKey)return undefined;return parentKey+'.'+this.name;},getProcess(){return this.parent;},indexOfTimeSlice(timeSlice){const i=tr.b.findLowIndexInSortedArray(this.timeSlices,function(slice){return slice.start;},timeSlice.start);if(this.timeSlices[i]!==timeSlice)return undefined;return i;},getCpuTimeForRange(range){let totalCpuTime=0;tr.b.iterateOverIntersectingIntervals(this.sliceGroup.topLevelSlices,slice=>slice.start,slice=>slice.end,range.min,range.max,slice=>{if(slice.duration===0)return;if(!slice.cpuDuration)return;const intersection=range.findIntersection(slice.range);const fractionOfSliceInsideRangeOfInterest=intersection.duration/slice.duration;totalCpuTime+=slice.cpuDuration*fractionOfSliceInsideRangeOfInterest;});return totalCpuTime;},getSchedulingStatsForRange(start,end){const stats={};if(!this.timeSlices)return stats;function addStatsForSlice(threadTimeSlice){const overlapStart=Math.max(threadTimeSlice.start,start);const overlapEnd=Math.min(threadTimeSlice.end,end);const schedulingState=threadTimeSlice.schedulingState;if(!(schedulingState in stats))stats[schedulingState]=0;stats[schedulingState]+=overlapEnd-overlapStart;}
+(this.name?', name: '+this.name:'');},getSettingsKey(){if(!this.name)return undefined;const parentKey=this.parent.getSettingsKey();if(!parentKey)return undefined;return parentKey+'.'+this.name;},getProcess(){return this.parent;},indexOfTimeSlice(timeSlice){const i=tr.b.findLowIndexInSortedArray(this.timeSlices,function(slice){return slice.start;},timeSlice.start);if(this.timeSlices[i]!==timeSlice)return undefined;return i;},sumOverToplevelSlicesInRange(range,func){let sum=0;tr.b.iterateOverIntersectingIntervals(this.sliceGroup.topLevelSlices,slice=>slice.start,slice=>slice.end,range.min,range.max,slice=>{let fractionOfSliceInsideRangeOfInterest=1;if(slice.duration>0){const intersection=range.findIntersection(slice.range);fractionOfSliceInsideRangeOfInterest=intersection.duration/slice.duration;}
+sum+=func(slice)*fractionOfSliceInsideRangeOfInterest;});return sum;},getCpuTimeForRange(range){return this.sumOverToplevelSlicesInRange(range,slice=>slice.cpuDuration||0);},getNumToplevelSlicesForRange(range){return this.sumOverToplevelSlicesInRange(range,slice=>1);},getSchedulingStatsForRange(start,end){const stats={};if(!this.timeSlices)return stats;function addStatsForSlice(threadTimeSlice){const overlapStart=Math.max(threadTimeSlice.start,start);const overlapEnd=Math.min(threadTimeSlice.end,end);const schedulingState=threadTimeSlice.schedulingState;if(!(schedulingState in stats))stats[schedulingState]=0;stats[schedulingState]+=overlapEnd-overlapStart;}
 tr.b.iterateOverIntersectingIntervals(this.timeSlices,function(x){return x.start;},function(x){return x.end;},start,end,addStatsForSlice);return stats;},get samples(){return this.samples_;},get type(){const re=/^[^0-9|\/]+/;const matches=re.exec(this.name);if(matches&&matches[0])return matches[0];throw new Error('Could not determine thread type for thread name '+
 this.name);}};Thread.compare=function(x,y){let tmp=x.parent.compareTo(y.parent);if(tmp)return tmp;tmp=x.sortIndex-y.sortIndex;if(tmp)return tmp;if(x.name!==undefined){if(y.name!==undefined){tmp=x.name.localeCompare(y.name);}else{tmp=-1;}}else if(y.name!==undefined){tmp=1;}
 if(tmp)return tmp;return x.tid-y.tid;};return{Thread,};});'use strict';tr.exportTo('tr.model',function(){const Thread=tr.model.Thread;const Counter=tr.model.Counter;function ProcessBase(model){if(!model){throw new Error('Must provide a model');}
@@ -5049,7 +5191,7 @@
 const SIZE_NUMERIC_NAME=tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME;const EFFECTIVE_SIZE_NUMERIC_NAME=tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME;const MemoryAllocatorDumpInfoType=tr.model.MemoryAllocatorDumpInfoType;const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN=MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN;const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER=MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER;function getSize(dump){const numeric=dump.numerics[SIZE_NUMERIC_NAME];if(numeric===undefined)return 0;return numeric.value;}
 function hasSize(dump){return dump.numerics[SIZE_NUMERIC_NAME]!==undefined;}
 function optional(value,defaultValue){if(value===undefined)return defaultValue;return value;}
-GlobalMemoryDump.prototype={__proto__:tr.model.ContainerMemoryDump.prototype,get userFriendlyName(){return'Global memory dump at '+
+GlobalMemoryDump.prototype={__proto__:tr.model.ContainerMemoryDump.prototype,get stableId(){return'memory.'+this.model.globalMemoryDumps.indexOf(this);},get userFriendlyName(){return'Global memory dump at '+
 tr.b.Unit.byName.timeStampInMs.format(this.start);},get containerName(){return'global space';},finalizeGraph(){this.removeWeakDumps();this.setUpTracingOverheadOwnership();this.aggregateNumerics();this.calculateSizes();this.calculateEffectiveSizes();this.discountTracingOverheadFromVmRegions();this.forceRebuildingMemoryAllocatorDumpByFullNameIndices();},removeWeakDumps(){this.traverseAllocatorDumpsInDepthFirstPreOrder(function(dump){if(dump.weak)return;if((dump.owns!==undefined&&dump.owns.target.weak)||(dump.parent!==undefined&&dump.parent.weak)){dump.weak=true;}});function removeWeakDumpsFromListRecursively(dumps){tr.b.inPlaceFilter(dumps,function(dump){if(dump.weak){return false;}
 removeWeakDumpsFromListRecursively(dump.children);tr.b.inPlaceFilter(dump.ownedBy,function(ownershipLink){return!ownershipLink.source.weak;});return true;});}
 this.iterateContainerDumps(function(containerDump){const memoryAllocatorDumps=containerDump.memoryAllocatorDumps;if(memoryAllocatorDumps!==undefined){removeWeakDumpsFromListRecursively(memoryAllocatorDumps);}});},calculateSizes(){this.traverseAllocatorDumpsInDepthFirstPostOrder(this.calculateMemoryAllocatorDumpSize_.bind(this));},calculateMemoryAllocatorDumpSize_(dump){let shouldDefineSize=false;function getDependencySize(dependencyDump){const numeric=dependencyDump.numerics[SIZE_NUMERIC_NAME];if(numeric===undefined)return 0;shouldDefineSize=true;return numeric.value;}
@@ -5085,12 +5227,13 @@
 this.iterateAllRootAllocatorDumps(visit);},traverseAllocatorDumpsInDepthFirstPreOrder(fn){const visitedDumps=new WeakSet();function visit(dump){if(visitedDumps.has(dump))return;if(dump.owns!==undefined&&!visitedDumps.has(dump.owns.target)){return;}
 if(dump.parent!==undefined&&!visitedDumps.has(dump.parent)){return;}
 fn.call(this,dump);visitedDumps.add(dump);dump.ownedBy.forEach(function(ownershipLink){visit.call(this,ownershipLink.source);},this);dump.children.forEach(visit,this);}
-this.iterateAllRootAllocatorDumps(visit);}};tr.model.EventRegistry.register(GlobalMemoryDump,{name:'globalMemoryDump',pluralName:'globalMemoryDumps'});return{GlobalMemoryDump,};});'use strict';tr.exportTo('tr.model',function(){const InstantEventType={GLOBAL:1,PROCESS:2};function InstantEvent(category,title,colorId,start,args){tr.model.TimedEvent.call(this,start);this.category=category||'';this.title=title;this.colorId=colorId;this.args=args;this.type=undefined;}
-InstantEvent.prototype={__proto__:tr.model.TimedEvent.prototype};function GlobalInstantEvent(category,title,colorId,start,args){InstantEvent.apply(this,arguments);this.type=InstantEventType.GLOBAL;}
+this.iterateAllRootAllocatorDumps(visit);}};tr.model.EventRegistry.register(GlobalMemoryDump,{name:'globalMemoryDump',pluralName:'globalMemoryDumps'});return{GlobalMemoryDump,};});'use strict';tr.exportTo('tr.model',function(){const InstantEventType={GLOBAL:1,PROCESS:2};function InstantEvent(category,title,colorId,start,args,parent){tr.model.TimedEvent.call(this,start);this.category=category||'';this.title=title;this.colorId=colorId;this.args=args;this.parent_=parent;this.type=undefined;}
+InstantEvent.prototype={__proto__:tr.model.TimedEvent.prototype,};function GlobalInstantEvent(category,title,colorId,start,args,parent){InstantEvent.apply(this,arguments);this.type=InstantEventType.GLOBAL;}
 GlobalInstantEvent.prototype={__proto__:InstantEvent.prototype,get userFriendlyName(){return'Global instant event '+this.title+' @ '+
-tr.b.Unit.byName.timeStampInMs.format(start);}};function ProcessInstantEvent(category,title,colorId,start,args){InstantEvent.apply(this,arguments);this.type=InstantEventType.PROCESS;}
+tr.b.Unit.byName.timeStampInMs.format(start);},get stableId(){return'instant.'+this.parent_.instantEvents.indexOf(this);},};function ProcessInstantEvent(category,title,colorId,start,args,parent){InstantEvent.apply(this,arguments);this.type=InstantEventType.PROCESS;}
 ProcessInstantEvent.prototype={__proto__:InstantEvent.prototype,get userFriendlyName(){return'Process-level instant event '+this.title+' @ '+
-tr.b.Unit.byName.timeStampInMs.format(start);}};tr.model.EventRegistry.register(InstantEvent,{name:'instantEvent',pluralName:'instantEvents'});return{GlobalInstantEvent,ProcessInstantEvent,InstantEventType,InstantEvent,};});'use strict';tr.exportTo('tr.model',function(){const Cpu=tr.model.Cpu;const ProcessBase=tr.model.ProcessBase;function Kernel(model){ProcessBase.call(this,model);this.cpus={};this.softwareMeasuredCpuCount_=undefined;}
+tr.b.Unit.byName.timeStampInMs.format(start);},get stableId(){return this.parent_.stableId+'.instant.'+
+this.parent_.instantEvents.indexOf(this);},};tr.model.EventRegistry.register(InstantEvent,{name:'instantEvent',pluralName:'instantEvents'});return{GlobalInstantEvent,ProcessInstantEvent,InstantEventType,InstantEvent,};});'use strict';tr.exportTo('tr.model',function(){const Cpu=tr.model.Cpu;const ProcessBase=tr.model.ProcessBase;function Kernel(model){ProcessBase.call(this,model);this.cpus={};this.softwareMeasuredCpuCount_=undefined;}
 Kernel.compare=function(x,y){return 0;};Kernel.prototype={__proto__:ProcessBase.prototype,compareTo(that){return Kernel.compare(this,that);},get userFriendlyName(){return'Kernel';},get userFriendlyDetails(){return'Kernel';},get stableId(){return'Kernel';},getOrCreateCpu(cpuNumber){if(!this.cpus[cpuNumber]){this.cpus[cpuNumber]=new Cpu(this,cpuNumber);}
 return this.cpus[cpuNumber];},get softwareMeasuredCpuCount(){return this.softwareMeasuredCpuCount_;},set softwareMeasuredCpuCount(softwareMeasuredCpuCount){if(this.softwareMeasuredCpuCount_!==undefined&&this.softwareMeasuredCpuCount_!==softwareMeasuredCpuCount){throw new Error('Cannot change the softwareMeasuredCpuCount once it is set');}
 this.softwareMeasuredCpuCount_=softwareMeasuredCpuCount;},get bestGuessAtCpuCount(){const realCpuCount=Object.keys(this.cpus).length;if(realCpuCount!==0){return realCpuCount;}
@@ -5138,7 +5281,8 @@
 if(region.byteStats.proportionalResident!==undefined){thisByteStats.nativeLibraryProportionalResident=(thisByteStats.nativeLibraryProportionalResident||0)+
 region.byteStats.proportionalResident;}}}};return{VMRegion,VMRegionClassificationNode,};});'use strict';tr.exportTo('tr.model',function(){const DISCOUNTED_ALLOCATOR_NAMES=['winheap','malloc'];const TRACING_OVERHEAD_PATH=['allocated_objects','tracing_overhead'];const SIZE_NUMERIC_NAME=tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME;const RESIDENT_SIZE_NUMERIC_NAME=tr.model.MemoryAllocatorDump.RESIDENT_SIZE_NUMERIC_NAME;function getSizeNumericValue(dump,sizeNumericName){const sizeNumeric=dump.numerics[sizeNumericName];if(sizeNumeric===undefined)return 0;return sizeNumeric.value;}
 function ProcessMemoryDump(globalMemoryDump,process,start){tr.model.ContainerMemoryDump.call(this,start);this.process=process;this.globalMemoryDump=globalMemoryDump;this.totals=undefined;this.vmRegions=undefined;this.heapDumps=undefined;this.tracingOverheadOwnershipSetUp_=false;this.tracingOverheadDiscountedFromVmRegions_=false;}
-ProcessMemoryDump.prototype={__proto__:tr.model.ContainerMemoryDump.prototype,get userFriendlyName(){return'Process memory dump at '+
+ProcessMemoryDump.prototype={__proto__:tr.model.ContainerMemoryDump.prototype,get stableId(){return this.process.stableId+'.memory.'+
+this.process.memoryDumps.indexOf(this);},get userFriendlyName(){return'Process memory dump at '+
 tr.b.Unit.byName.timeStampInMs.format(this.start);},get containerName(){return this.process.userFriendlyName;},get processMemoryDumps(){const dumps={};dumps[this.process.pid]=this;return dumps;},get hasOwnVmRegions(){return this.vmRegions!==undefined;},setUpTracingOverheadOwnership(opt_model){if(this.tracingOverheadOwnershipSetUp_)return;this.tracingOverheadOwnershipSetUp_=true;const tracingDump=this.getMemoryAllocatorDumpByFullName('tracing');if(tracingDump===undefined||tracingDump.owns!==undefined){return;}
 if(tracingDump.owns!==undefined)return;const hasDiscountedFromAllocatorDumps=DISCOUNTED_ALLOCATOR_NAMES.some(function(allocatorName){const allocatorDump=this.getMemoryAllocatorDumpByFullName(allocatorName);if(allocatorDump===undefined){return false;}
 let nextPathIndex=0;let currentDump=allocatorDump;let currentFullName=allocatorName;for(;nextPathIndex<TRACING_OVERHEAD_PATH.length;nextPathIndex++){const childFullName=currentFullName+'/'+
@@ -5234,7 +5378,7 @@
 EtwImporter.canImport=function(events){if(!events.hasOwnProperty('name')||!events.hasOwnProperty('content')||events.name!=='ETW'){return false;}
 return true;};EtwImporter.prototype={__proto__:tr.importer.Importer.prototype,get importerName(){return'EtwImporter';},get model(){return this.model_;},createThreadIfNeeded(pid,tid){this.tidsToPid_[tid]=pid;},removeThreadIfPresent(tid){this.tidsToPid_[tid]=undefined;},getPidFromWindowsTid(tid){if(tid===0)return 0;const pid=this.tidsToPid_[tid];if(pid===undefined){return 0;}
 return pid;},getThreadFromWindowsTid(tid){const pid=this.getPidFromWindowsTid(tid);const process=this.model_.getProcess(pid);if(!process)return undefined;return process.getThread(tid);},getOrCreateCpu(cpuNumber){const cpu=this.model_.kernel.getOrCreateCpu(cpuNumber);return cpu;},importEvents(){this.events_.content.forEach(this.parseInfo.bind(this));if(this.walltime_===undefined||this.ticks_===undefined){throw Error('Cannot find clock sync information in the system trace.');}
-if(this.is64bit_===undefined){throw Error('Cannot determine pointer size of the system trace.');}
+if(this.is64bit_===undefined){throw Error('Cannot determine pointer size of the system trace.'+'Consider deselecting "System tracing" or disabling the "Paging '+'Executive" feature of Windows');}
 this.events_.content.forEach(this.parseEvent.bind(this));},importTimestamp(timestamp){const ts=parseInt(timestamp,16);return(ts-this.walltime_+this.ticks_)/1000.;},parseInfo(event){if(event.hasOwnProperty('guid')&&event.hasOwnProperty('walltime')&&event.hasOwnProperty('tick')&&event.guid==='ClockSync'){this.walltime_=parseInt(event.walltime,16);this.ticks_=parseInt(event.tick,16);}
 if(this.is64bit_===undefined&&event.hasOwnProperty('guid')&&event.hasOwnProperty('op')&&event.hasOwnProperty('ver')&&event.hasOwnProperty('payload')&&event.guid===kThreadGuid&&event.op===kThreadDCStartOpcode){const decodedSize=tr.b.Base64.getDecodedBufferLength(event.payload);if(event.ver===1){if(decodedSize>=52){this.is64bit_=true;}else{this.is64bit_=false;}}else if(event.ver===2){if(decodedSize>=64){this.is64bit_=true;}else{this.is64bit_=false;}}else if(event.ver===3){if(decodedSize>=60){this.is64bit_=true;}else{this.is64bit_=false;}}}
 return true;},parseEvent(event){if(!event.hasOwnProperty('guid')||!event.hasOwnProperty('op')||!event.hasOwnProperty('ver')||!event.hasOwnProperty('cpu')||!event.hasOwnProperty('ts')||!event.hasOwnProperty('payload')){return false;}
@@ -5644,7 +5788,9 @@
 return bank;}};function TraceCodeBank(){this.entries_=[];}
 TraceCodeBank.prototype={removeEntry(address){if(this.entries_.length===0)return undefined;const index=tr.b.findLowIndexInSortedArray(this.entries_,function(entry){return entry.address;},address);const entry=this.entries_[index];if(!entry||entry.address!==address)return undefined;this.entries_.splice(index,1);return entry;},lookupEntry(address){const index=tr.b.findFirstTrueIndexInSortedArray(this.entries_,e=>(address<e.address))-1;const entry=this.entries_[index];return entry&&address<entry.address+entry.size?entry:undefined;},addEntry(newEntry){if(this.entries_.length===0){this.entries_.push(newEntry);}
 const endAddress=newEntry.address+newEntry.size;const lastIndex=tr.b.findLowIndexInSortedArray(this.entries_,function(entry){return entry.address;},endAddress);let index;for(index=lastIndex-1;index>=0;--index){const entry=this.entries_[index];const entryEndAddress=entry.address+entry.size;if(entryEndAddress<=newEntry.address)break;}
-++index;this.entries_.splice(index,lastIndex-index,newEntry);}};return{TraceCodeMap,};});'use strict';tr.exportTo('tr.importer',function(){function ContextProcessor(model){this.model_=model;this.activeContexts_=[];this.stackPerType_={};this.contextCache_={};this.contextSetCache_={};this.cachedEntryForActiveContexts_=undefined;this.seenSnapshots_={};}
+++index;this.entries_.splice(index,lastIndex-index,newEntry);}};return{TraceCodeMap,};});'use strict';tr.exportTo('tr.e.measure',function(){const AsyncSlice=tr.model.AsyncSlice;const MEASURE_NAME_REGEX=/([^\/:]+):(.*?)(?:\/([A-Za-z0-9+/]+=?=?))?$/;function MeasureAsyncSlice(){this.groupTitle_='Ungrouped Measure';const matched=MEASURE_NAME_REGEX.exec(arguments[1]);if(matched!==null){arguments[1]=matched[2];this.groupTitle_=matched[1];}
+AsyncSlice.apply(this,arguments);}
+MeasureAsyncSlice.prototype={__proto__:AsyncSlice.prototype,get viewSubGroupTitle(){return this.groupTitle_;},get title(){return this.title_;},set title(title){this.title_=title;}};AsyncSlice.subTypes.register(MeasureAsyncSlice,{categoryParts:['blink.user_timing']});return{MEASURE_NAME_REGEX,MeasureAsyncSlice,};});'use strict';tr.exportTo('tr.importer',function(){function ContextProcessor(model){this.model_=model;this.activeContexts_=[];this.stackPerType_={};this.contextCache_={};this.contextSetCache_={};this.cachedEntryForActiveContexts_=undefined;this.seenSnapshots_={};}
 ContextProcessor.prototype={enterContext(contextType,scopedId){const newActiveContexts=[this.getOrCreateContext_(contextType,scopedId),];for(const oldContext of this.activeContexts_){if(oldContext.type===contextType){this.pushContext_(oldContext);}else{newActiveContexts.push(oldContext);}}
 this.activeContexts_=newActiveContexts;this.cachedEntryForActiveContexts_=undefined;},leaveContext(contextType,scopedId){this.leaveContextImpl_(context=>context.type===contextType&&context.snapshot.scope===scopedId.scope&&context.snapshot.idRef===scopedId.id);},destroyContext(scopedId){for(const stack of Object.values(this.stackPerType_)){let newLength=0;for(let i=0;i<stack.length;++i){if(stack[i].snapshot.scope!==scopedId.scope||stack[i].snapshot.idRef!==scopedId.id){stack[newLength++]=stack[i];}}
 stack.length=newLength;}
@@ -5686,7 +5832,7 @@
 ScopedId.prototype={toString(){const pidStr=this.pid===undefined?'':'pid: '+this.pid+', ';return'{'+pidStr+'scope: '+this.scope+', id: '+this.id+'}';},toStringWithDelimiter(delim){return(this.pid===undefined?'':this.pid)+delim+
 this.scope+delim+this.id;}};return{ScopedId,};});'use strict';tr.exportTo('tr.ui.annotations',function(){function XMarkerAnnotationView(viewport,annotation){this.viewport_=viewport;this.annotation_=annotation;}
 XMarkerAnnotationView.prototype={__proto__:tr.ui.annotations.AnnotationView.prototype,draw(ctx){const dt=this.viewport_.currentDisplayTransform;const viewX=dt.xWorldToView(this.annotation_.timestamp);ctx.beginPath();tr.ui.b.drawLine(ctx,viewX,0,viewX,ctx.canvas.height);ctx.strokeStyle=this.annotation_.strokeStyle;ctx.stroke();}};return{XMarkerAnnotationView,};});'use strict';tr.exportTo('tr.model',function(){function XMarkerAnnotation(timestamp){tr.model.Annotation.apply(this,arguments);this.timestamp=timestamp;this.strokeStyle='rgba(0, 0, 255, 0.5)';}
-XMarkerAnnotation.fromDict=function(dict){return new XMarkerAnnotation(dict.args.timestamp);};XMarkerAnnotation.prototype={__proto__:tr.model.Annotation.prototype,toDict(){return{typeName:'xmarker',args:{timestamp:this.timestamp}};},createView_(viewport){return new tr.ui.annotations.XMarkerAnnotationView(viewport,this);}};tr.model.Annotation.register(XMarkerAnnotation,{typeName:'xmarker'});return{XMarkerAnnotation,};});'use strict';tr.exportTo('tr.e.importer',function(){const Base64=tr.b.Base64;const deepCopy=tr.b.deepCopy;const ColorScheme=tr.b.ColorScheme;const HeapDumpTraceEventImporter=tr.e.importer.HeapDumpTraceEventImporter;const LegacyHeapDumpTraceEventImporter=tr.e.importer.LegacyHeapDumpTraceEventImporter;const StreamingEventExpander=tr.e.importer.StreamingEventExpander;const ProfilingDictionaryReader=tr.e.importer.ProfilingDictionaryReader;function getEventColor(event,opt_customName){if(event.cname){return ColorScheme.getColorIdForReservedName(event.cname);}else if(opt_customName||event.name){return ColorScheme.getColorIdForGeneralPurposeString(opt_customName||event.name);}}
+XMarkerAnnotation.fromDict=function(dict){return new XMarkerAnnotation(dict.args.timestamp);};XMarkerAnnotation.prototype={__proto__:tr.model.Annotation.prototype,toDict(){return{typeName:'xmarker',args:{timestamp:this.timestamp}};},createView_(viewport){return new tr.ui.annotations.XMarkerAnnotationView(viewport,this);}};tr.model.Annotation.register(XMarkerAnnotation,{typeName:'xmarker'});return{XMarkerAnnotation,};});'use strict';tr.exportTo('tr.e.importer',function(){const Base64=tr.b.Base64;const deepCopy=tr.b.deepCopy;const ColorScheme=tr.b.ColorScheme;const HeapDumpTraceEventImporter=tr.e.importer.HeapDumpTraceEventImporter;const LegacyHeapDumpTraceEventImporter=tr.e.importer.LegacyHeapDumpTraceEventImporter;const StreamingEventExpander=tr.e.importer.StreamingEventExpander;const ProfilingDictionaryReader=tr.e.importer.ProfilingDictionaryReader;const MEASURE_NAME_REGEX=tr.e.measure.MEASURE_NAME_REGEX;function getEventColor(event,opt_customName){if(event.cname){return ColorScheme.getColorIdForReservedName(event.cname);}else if(opt_customName||event.name){return ColorScheme.getColorIdForGeneralPurposeString(opt_customName||event.name);}}
 function isLegacyChromeClockSyncEvent(event){return event.name!==undefined&&event.name.startsWith(LEGACY_CHROME_CLOCK_SYNC_EVENT_NAME_PREFIX)&&((event.ph==='S')||(event.ph==='F'));}
 const PRODUCER='producer';const CONSUMER='consumer';const STEP='step';const BACKGROUND=tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;const LIGHT=tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;const DETAILED=tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;const MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER=[undefined,BACKGROUND,LIGHT,DETAILED];const GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX='global/';const LEGACY_CHROME_CLOCK_SYNC_EVENT_NAME_PREFIX='ClockSyncEvent.';const BYTE_STAT_NAME_MAP={'pc':'privateCleanResident','pd':'privateDirtyResident','sc':'sharedCleanResident','sd':'sharedDirtyResident','pss':'proportionalResident','sw':'swapped'};const WEAK_MEMORY_ALLOCATOR_DUMP_FLAG=1<<0;const OBJECT_TYPE_NAME_PATTERNS=[{prefix:'const char *WTF::getStringWithTypeName() [T = ',suffix:']'},{prefix:'const char* WTF::getStringWithTypeName() [with T = ',suffix:']'},{prefix:'const char *__cdecl WTF::getStringWithTypeName<',suffix:'>(void)'}];const SUBTRACE_FIELDS=new Set(['powerTraceAsString','systemTraceEvents','androidProcessDump',]);const NON_METADATA_FIELDS=new Set(['displayTimeUnit','samples','stackFrames','traceAnnotations','traceEvents',...SUBTRACE_FIELDS]);function TraceEventImporter(model,eventData){this.hasEvents_=undefined;this.importPriority=1;this.model_=model;this.events_=undefined;this.sampleEvents_=undefined;this.stackFrameEvents_=undefined;this.stackFrameTree_=new tr.model.ProfileTree();this.subtraces_=[];this.eventsWereFromString_=false;this.softwareMeasuredCpuCount_=undefined;this.allAsyncEvents_=[];this.allFlowEvents_=[];this.allObjectEvents_=[];this.contextProcessorPerThread={};this.traceEventSampleStackFramesByName_={};this.v8ProcessCodeMaps_={};this.v8ProcessRootStackFrame_={};this.v8SamplingData_=[];this.profileTrees_=new Map();this.profileInfo_=new Map();this.legacyChromeClockSyncStartEvent_=undefined;this.legacyChromeClockSyncFinishEvent_=undefined;this.allMemoryDumpEvents_={};this.heapProfileExpander=new ProfilingDictionaryReader();this.objectTypeNameMap_={};this.clockDomainId_=tr.model.ClockDomainId.UNKNOWN_CHROME_LEGACY;this.toModelTime_=undefined;if(typeof(eventData)==='string'||eventData instanceof String){eventData=eventData.trim();if(eventData[0]==='['){eventData=eventData.replace(/\s*,\s*$/,'');if(eventData[eventData.length-1]!==']'){eventData=eventData+']';}}
 this.events_=JSON.parse(eventData);this.eventsWereFromString_=true;}else{this.events_=eventData;}
@@ -5721,9 +5867,8 @@
 this.softwareMeasuredCpuCount_=n;}else if(event.name==='stackFrames'){const stackFrames=event.args.stackFrames;if(stackFrames===undefined){this.model_.importWarning({type:'metadata_parse_error',message:'No stack frames found in a \''+event.name+'\' metadata event'});}else{this.importStackFrames_(stackFrames,'p'+event.pid+':');}}else if(event.name==='typeNames'){const objectTypeNameMap=event.args.typeNames;if(objectTypeNameMap===undefined){this.model_.importWarning({type:'metadata_parse_error',message:'No mapping from object type IDs to names found in a \''+
 event.name+'\' metadata event'});}else{this.importObjectTypeNameMap_(objectTypeNameMap,event.pid);}}else if(event.name==='TraceConfig'){this.model_.metadata.push({name:'TraceConfig',value:event.args.value});}else{this.model_.importWarning({type:'metadata_parse_error',message:'Unrecognized metadata name: '+event.name});}},processInstantEvent(event){if(event.name==='JitCodeAdded'||event.name==='JitCodeMoved'){this.v8SamplingData_.push(event);return;}
 if(event.s==='t'||event.s===undefined){this.processDurationEvent(event);return;}
-let constructor;switch(event.s){case'g':constructor=tr.model.GlobalInstantEvent;break;case'p':constructor=tr.model.ProcessInstantEvent;break;default:this.model_.importWarning({type:'instant_parse_error',message:'I phase event with unknown "s" field value.'});return;}
-const instantEvent=new constructor(event.cat,event.name,getEventColor(event),this.toModelTimeFromUs_(event.ts),this.deepCopyIfNeeded_(event.args));switch(instantEvent.type){case tr.model.InstantEventType.GLOBAL:this.model_.instantEvents.push(instantEvent);break;case tr.model.InstantEventType.PROCESS:{const process=this.model_.getOrCreateProcess(event.pid);process.instantEvents.push(instantEvent);break;}
-default:throw new Error('Unknown instant event type: '+event.s);}},getOrCreateProfileTree_(sampleType,id){if(!this.profileTrees_.has(sampleType)){this.profileTrees_.set(sampleType,new Map());}
+let constructor;let parent;switch(event.s){case'g':constructor=tr.model.GlobalInstantEvent;parent=this.model_;break;case'p':constructor=tr.model.ProcessInstantEvent;parent=this.model_.getOrCreateProcess(event.pid);break;default:this.model_.importWarning({type:'instant_parse_error',message:'I phase event with unknown "s" field value.'});return;}
+const instantEvent=new constructor(event.cat,event.name,getEventColor(event),this.toModelTimeFromUs_(event.ts),this.deepCopyIfNeeded_(event.args),parent);parent.instantEvents.push(instantEvent);},getOrCreateProfileTree_(sampleType,id){if(!this.profileTrees_.has(sampleType)){this.profileTrees_.set(sampleType,new Map());}
 const profileTreeMap=this.profileTrees_.get(sampleType);if(profileTreeMap.has(id)){return profileTreeMap.get(id);}
 const profileTree=new tr.model.ProfileTree();profileTreeMap.set(id,profileTree);const info=this.profileInfo_.get(id);if(info!==undefined){profileTree.startTime=info.startTime;profileTree.pid=info.pid;profileTree.tid=info.tid;}
 return profileTree;},processSample(event){if(event.args===undefined||event.args.data===undefined){return;}
@@ -5789,7 +5934,8 @@
 if(event.cat===undefined){this.model_.importWarning({type:'async_slice_parse_error',message:'Nestable async events (ph: b, e, or n) require a '+'cat parameter.'});continue;}
 if(event.name===undefined){this.model_.importWarning({type:'async_slice_parse_error',message:'Nestable async events (ph: b, e, or n) require a '+'name parameter.'});continue;}
 const id=TraceEventImporter.scopedIdForEvent_(event);if(id===undefined){this.model_.importWarning({type:'async_slice_parse_error',message:'Nestable async events (ph: b, e, or n) require an '+'id parameter.'});continue;}
-if(event.cat==='blink.user_timing'){const matched=/([^\/:]+):([^\/]+)\/?(.*)/.exec(event.name);if(matched!==null){const key=matched[1]+':'+event.cat;event.args=JSON.parse(Base64.atob(matched[3])||'{}');if(nestableMeasureAsyncEventsByKey[key]===undefined){nestableMeasureAsyncEventsByKey[key]=[];}
+if(event.cat==='blink.user_timing'){const matched=MEASURE_NAME_REGEX.exec(event.name);if(matched!==null){const key=matched[1]+':'+event.cat;try{event.args=JSON.parse(Base64.atob(matched[3])||'{}');}catch(e){}
+if(nestableMeasureAsyncEventsByKey[key]===undefined){nestableMeasureAsyncEventsByKey[key]=[];}
 nestableMeasureAsyncEventsByKey[key].push(asyncEventState);continue;}}
 const key=event.cat+':'+id.toStringWithDelimiter(':');if(nestableAsyncEventsByKey[key]===undefined){nestableAsyncEventsByKey[key]=[];}
 nestableAsyncEventsByKey[key].push(asyncEventState);}
@@ -5850,13 +5996,13 @@
 flowStatus[event.bind_id]=false;if(event.flowPhase===STEP){if(!processFlowProducer(flowIdToEvent,flowStatus,event,slice)){continue;}
 flowStatus[event.bind_id]=true;}}
 continue;}
-let flowEvent;if(event.ph==='s'){if(flowIdToEvent[event.id]){this.model_.importWarning({type:'flow_slice_start_error',message:'event id '+event.id+' already seen when '+'encountering start of flow event.'});continue;}
+const fullFlowId=JSON.stringify({id:event.id,cat:event.cat,name:event.name});let flowEvent;if(event.ph==='s'){if(flowIdToEvent[fullFlowId]){this.model_.importWarning({type:'flow_slice_start_error',message:'event id '+event.id+' already seen when '+'encountering start of flow event.'});continue;}
 flowEvent=createFlowEvent(thread,event);if(!flowEvent){this.model_.importWarning({type:'flow_slice_start_error',message:'event id '+event.id+' does not start '+'at an actual slice, so cannot be created.'});continue;}
-flowIdToEvent[event.id]=flowEvent;}else if(event.ph==='t'||event.ph==='f'){flowEvent=flowIdToEvent[event.id];if(flowEvent===undefined){this.model_.importWarning({type:'flow_slice_ordering_error',message:'Found flow phase '+event.ph+' for id: '+event.id+' but no flow start found.'});continue;}
+flowIdToEvent[fullFlowId]=flowEvent;}else if(event.ph==='t'||event.ph==='f'){flowEvent=flowIdToEvent[fullFlowId];if(flowEvent===undefined){this.model_.importWarning({type:'flow_slice_ordering_error',message:'Found flow phase '+event.ph+' for id: '+event.id+' but no flow start found.'});continue;}
 let bindToParent=event.ph==='t';if(event.ph==='f'){if(event.bp===undefined){if(event.cat.indexOf('input')>-1){bindToParent=true;}else if(event.cat.indexOf('ipc.flow')>-1){bindToParent=true;}}else{if(event.bp!=='e'){this.model_.importWarning({type:'flow_slice_bind_point_error',message:'Flow event with invalid binding point (event.bp).'});continue;}
 bindToParent=true;}}
 const ok=finishFlowEventWith(flowEvent,thread,event,refGuid,bindToParent);if(ok){this.model_.flowEvents.push(flowEvent);}else{this.model_.importWarning({type:'flow_slice_end_error',message:'event id '+event.id+' does not end '+'at an actual slice, so cannot be created.'});}
-flowIdToEvent[event.id]=undefined;if(ok&&event.ph==='t'){flowEvent=createFlowEvent(thread,event);flowIdToEvent[event.id]=flowEvent;}}}},createExplicitObjects_(){if(this.allObjectEvents_.length===0)return;const processEvent=function(objectEventState){const event=objectEventState.event;const scopedId=TraceEventImporter.scopedIdForEvent_(event);const thread=objectEventState.thread;if(event.name===undefined){this.model_.importWarning({type:'object_parse_error',message:'While processing '+JSON.stringify(event)+': '+'Object events require an name parameter.'});}
+flowIdToEvent[fullFlowId]=undefined;if(ok&&event.ph==='t'){flowEvent=createFlowEvent(thread,event);flowIdToEvent[fullFlowId]=flowEvent;}}}},createExplicitObjects_(){if(this.allObjectEvents_.length===0)return;const processEvent=function(objectEventState){const event=objectEventState.event;const scopedId=TraceEventImporter.scopedIdForEvent_(event);const thread=objectEventState.thread;if(event.name===undefined){this.model_.importWarning({type:'object_parse_error',message:'While processing '+JSON.stringify(event)+': '+'Object events require an name parameter.'});}
 if(scopedId===undefined||scopedId.id===undefined){this.model_.importWarning({type:'object_parse_error',message:'While processing '+JSON.stringify(event)+': '+'Object events require an id parameter.'});}
 const process=thread.parent;const ts=this.toModelTimeFromUs_(event.ts);let instance;if(event.ph==='N'){try{instance=process.objects.idWasCreated(scopedId,event.cat,event.name,ts);}catch(e){this.model_.importWarning({type:'object_parse_error',message:'While processing create of '+
 scopedId+' at ts='+ts+': '+e});return;}}else if(event.ph==='O'){if(event.args.snapshot===undefined){this.model_.importWarning({type:'object_parse_error',message:'While processing '+scopedId+' at ts='+ts+': '+'Snapshots must have args: {snapshot: ...}'});return;}
@@ -5947,9 +6093,7 @@
 sourceDump.owns.target.fullName+').'});}else{sourceDump.owns=edge;targetDump.ownedBy.push(edge);}
 break;case'retention':sourceDump.retains.push(edge);targetDump.retainedBy.push(edge);break;default:this.model_.importWarning({type:'memory_dump_parse_error',message:'Invalid edge type: '+rawEdge.type+' (PID='+pid+', dump ID='+dumpId+', source='+sourceGuid+', target='+targetGuid+', importance='+importance+').'});}}}}},toModelTimeFromUs_(ts){if(!this.toModelTime_){this.toModelTime_=this.model_.clockSyncManager.getModelTimeTransformer(this.clockDomainId_);}
 return this.toModelTime_(tr.b.Unit.timestampFromUs(ts));},maybeToModelTimeFromUs_(ts){if(ts===undefined){return undefined;}
-return this.toModelTimeFromUs_(ts);}};tr.importer.Importer.register(TraceEventImporter);return{TraceEventImporter,};});'use strict';tr.exportTo('tr.e.measure',function(){const AsyncSlice=tr.model.AsyncSlice;function MeasureAsyncSlice(){this.groupTitle_='Ungrouped Measure';const matched=/([^\/:]+):([^\/:]+)\/?(.*)/.exec(arguments[1]);if(matched!==null){arguments[1]=matched[2];this.groupTitle_=matched[1];}
-AsyncSlice.apply(this,arguments);}
-MeasureAsyncSlice.prototype={__proto__:AsyncSlice.prototype,get viewSubGroupTitle(){return this.groupTitle_;},get title(){return this.title_;},set title(title){this.title_=title;}};AsyncSlice.subTypes.register(MeasureAsyncSlice,{categoryParts:['blink.user_timing']});return{MeasureAsyncSlice,};});'use strict';tr.exportTo('tr.e.net',function(){const AsyncSlice=tr.model.AsyncSlice;function NetAsyncSlice(){AsyncSlice.apply(this,arguments);this.url_=undefined;this.byteCount_=undefined;this.isTitleComputed_=false;this.isUrlComputed_=false;}
+return this.toModelTimeFromUs_(ts);}};tr.importer.Importer.register(TraceEventImporter);return{TraceEventImporter,};});'use strict';tr.exportTo('tr.e.net',function(){const AsyncSlice=tr.model.AsyncSlice;function NetAsyncSlice(){AsyncSlice.apply(this,arguments);this.url_=undefined;this.byteCount_=undefined;this.isTitleComputed_=false;this.isUrlComputed_=false;}
 NetAsyncSlice.prototype={__proto__:AsyncSlice.prototype,get viewSubGroupTitle(){return'NetLog';},get title(){if(this.isTitleComputed_||!this.isTopLevel){return this.title_;}
 if(this.url!==undefined&&this.url.length>0){this.title_=this.url;}else if(this.args!==undefined&&this.args.source_type!==undefined){this.title_=this.args.source_type;}
 this.isTitleComputed_=true;return this.title_;},set title(title){this.title_=title;},get url(){if(this.isUrlComputed_){return this.url_;}
@@ -6029,23 +6173,25 @@
 if(event[5]==='A'){action+=' ahead';}
 if(event[6]==='S'){action+=' sync';}
 if(event[7]==='M'){action+=' meta';}
-const device=event[1];const sector=parseInt(event[8]);const numSectors=parseInt(event[9]);const key=device+'-'+sector+'-'+numSectors;this.openAsyncSlice(ts,'block',eventBase.threadName,eventBase.pid,key,action);return true;},blockRqCompleteEvent(eventName,cpuNumber,pid,ts,eventBase){const event=new RegExp('(\\d+,\\d+) (F)?([DWRN])(F)?(A)?(S)?(M)? '+'\\(.*\\) (\\d+) \\+ (\\d+) \\[(.*)\\]').exec(eventBase.details);if(!event)return false;const device=event[1];const sector=parseInt(event[8]);const numSectors=parseInt(event[9]);const error=parseInt(event[10]);const key=device+'-'+sector+'-'+numSectors;this.closeAsyncSlice(ts,'block',eventBase.threadName,eventBase.pid,key,{device,sector,numSectors,error});return true;}};Parser.register(DiskParser);return{DiskParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function DrmParser(importer){Parser.call(this,importer);importer.registerEventHandler('drm_vblank_event',DrmParser.prototype.vblankEvent.bind(this));}
+const device=event[1];const sector=parseInt(event[8]);const numSectors=parseInt(event[9]);const key=device+'-'+sector+'-'+numSectors;this.openAsyncSlice(ts,'block',eventBase.threadName,eventBase.pid,key,action);return true;},blockRqCompleteEvent(eventName,cpuNumber,pid,ts,eventBase){const event=new RegExp('(\\d+,\\d+) (F)?([DWRN])(F)?(A)?(S)?(M)? '+'\\(.*\\) (\\d+) \\+ (\\d+) \\[(.*)\\]').exec(eventBase.details);if(!event)return false;const device=event[1];const sector=parseInt(event[8]);const numSectors=parseInt(event[9]);const error=parseInt(event[10]);const key=device+'-'+sector+'-'+numSectors;this.closeAsyncSlice(ts,'block',eventBase.threadName,eventBase.pid,key,{device,sector,numSectors,error});return true;}};Parser.register(DiskParser);return{DiskParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function DmaFenceParser(importer){Parser.call(this,importer);this.model_=importer.model_;importer.registerEventHandler('dma_fence_init',DmaFenceParser.prototype.initEvent.bind(this));importer.registerEventHandler('dma_fence_emit',DmaFenceParser.prototype.initEvent.bind(this));importer.registerEventHandler('dma_fence_destroy',DmaFenceParser.prototype.fenceDestroyEvent.bind(this));importer.registerEventHandler('dma_fence_enable_signal',DmaFenceParser.prototype.fenceEnableSignalEvent.bind(this));importer.registerEventHandler('dma_fence_signaled',DmaFenceParser.prototype.fenceSignaledEvent.bind(this));importer.registerEventHandler('dma_fence_wait_start',DmaFenceParser.prototype.fenceWaitEvent.bind(this));importer.registerEventHandler('dma_fence_wait_end',DmaFenceParser.prototype.fenceWaitEvent.bind(this));importer.registerEventHandler('fence_init',DmaFenceParser.prototype.initEvent.bind(this));importer.registerEventHandler('fence_emit',DmaFenceParser.prototype.initEvent.bind(this));importer.registerEventHandler('fence_destroy',DmaFenceParser.prototype.fenceDestroyEvent.bind(this));importer.registerEventHandler('fence_enable_signal',DmaFenceParser.prototype.fenceEnableSignalEvent.bind(this));importer.registerEventHandler('fence_signaled',DmaFenceParser.prototype.fenceSignaledEvent.bind(this));importer.registerEventHandler('fence_wait_start',DmaFenceParser.prototype.fenceWaitEvent.bind(this));importer.registerEventHandler('fence_wait_end',DmaFenceParser.prototype.fenceWaitEvent.bind(this));this.model_=importer.model_;}
+const fenceRE=/driver=(\S+) timeline=(\S+) context=(\d+) seqno=(\d+)/;DmaFenceParser.prototype={__proto__:Parser.prototype,initEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
+const thread=this.importer.getOrCreatePseudoThread(event[2]);thread.lastActiveTs=ts;return true;},fenceDestroyEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
+const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_destroy('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{driver:event[1],context:event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
+if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
+thread.lastActiveTs=ts;},fenceEnableSignalEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
+const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_enable('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{driver:event[1],context:event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
+if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
+thread.lastActiveTs=ts;},fenceSignaledEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
+const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_signal('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{driver:event[1],context:event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
+if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
+thread.lastActiveTs=ts;return true;},fenceWaitEvent(eventName,cpuNumber,pid,ts,eventBase){if(eventBase.tgid===undefined)return false;const event=fenceRE.exec(eventBase.details);if(!event)return false;const tgid=parseInt(eventBase.tgid);const thread=this.model_.getOrCreateProcess(tgid).getOrCreateThread(pid);thread.name=eventBase.threadName;const slices=thread.kernelSliceGroup;if(!slices.isTimestampValidForBeginOrEnd(ts)){this.model_.importWarning({type:'parse_error',message:'Timestamps are moving backward.'});return false;}
+const name='dma_fence_wait("'+event[2]+'")';if(eventName.endsWith('start')){const slice=slices.beginSlice(null,name,ts,{driver:event[1],context:event[3],seqno:event[4],});}else{if(slices.openSliceCount>0){slices.endSlice(ts);}}
+return true;},};Parser.register(DmaFenceParser);return{DmaFenceParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function DrmParser(importer){Parser.call(this,importer);importer.registerEventHandler('drm_vblank_event',DrmParser.prototype.vblankEvent.bind(this));}
 DrmParser.prototype={__proto__:Parser.prototype,drmVblankSlice(ts,eventName,args){const kthread=this.importer.getOrCreatePseudoThread('drm_vblank');kthread.openSlice=eventName;const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),ts,args,0);kthread.thread.sliceGroup.pushSlice(slice);},vblankEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/crtc=(\d+), seq=(\d+)/.exec(eventBase.details);if(!event)return false;const crtc=parseInt(event[1]);const seq=parseInt(event[2]);this.drmVblankSlice(ts,'vblank:'+crtc,{crtc,seq});return true;}};Parser.register(DrmParser);return{DrmParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function ExynosParser(importer){Parser.call(this,importer);importer.registerEventHandler('exynos_busfreq_target_int',ExynosParser.prototype.busfreqTargetIntEvent.bind(this));importer.registerEventHandler('exynos_busfreq_target_mif',ExynosParser.prototype.busfreqTargetMifEvent.bind(this));importer.registerEventHandler('exynos_page_flip_state',ExynosParser.prototype.pageFlipStateEvent.bind(this));}
 ExynosParser.prototype={__proto__:Parser.prototype,exynosBusfreqSample(name,ts,frequency){const targetCpu=this.importer.getOrCreateCpu(0);const counter=targetCpu.getOrCreateCounter('',name);if(counter.numSeries===0){counter.addSeries(new tr.model.CounterSeries('frequency',ColorScheme.getColorIdForGeneralPurposeString(counter.name+'.'+'frequency')));}
 counter.series.forEach(function(series){series.addCounterSample(ts,frequency);});},busfreqTargetIntEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/frequency=(\d+)/.exec(eventBase.details);if(!event)return false;this.exynosBusfreqSample('INT Frequency',ts,parseInt(event[1]));return true;},busfreqTargetMifEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/frequency=(\d+)/.exec(eventBase.details);if(!event)return false;this.exynosBusfreqSample('MIF Frequency',ts,parseInt(event[1]));return true;},exynosPageFlipStateOpenSlice(ts,pipe,fb,state){const kthread=this.importer.getOrCreatePseudoThread('exynos_flip_state (pipe:'+pipe+', fb:'+fb+')');kthread.openSliceTS=ts;kthread.openSlice=state;},exynosPageFlipStateCloseSlice(ts,pipe,fb,args){const kthread=this.importer.getOrCreatePseudoThread('exynos_flip_state (pipe:'+pipe+', fb:'+fb+')');if(kthread.openSlice){const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),kthread.openSliceTS,args,ts-kthread.openSliceTS);kthread.thread.sliceGroup.pushSlice(slice);}
 kthread.openSlice=undefined;},pageFlipStateEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/pipe=(\d+), fb=(\d+), state=(.*)/.exec(eventBase.details);if(!event)return false;const pipe=parseInt(event[1]);const fb=parseInt(event[2]);const state=event[3];this.exynosPageFlipStateCloseSlice(ts,pipe,fb,{pipe,fb});if(state!=='flipped'){this.exynosPageFlipStateOpenSlice(ts,pipe,fb,state);}
-return true;}};Parser.register(ExynosParser);return{ExynosParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function FenceParser(importer){Parser.call(this,importer);this.model_=importer.model_;importer.registerEventHandler('fence_init',FenceParser.prototype.initEvent.bind(this));importer.registerEventHandler('fence_destroy',FenceParser.prototype.fenceDestroyEvent.bind(this));importer.registerEventHandler('fence_enable_signal',FenceParser.prototype.fenceEnableSignalEvent.bind(this));importer.registerEventHandler('fence_signaled',FenceParser.prototype.fenceSignaledEvent.bind(this));this.model_=importer.model_;}
-const fenceRE=/driver=(\S+) timeline=(\S+) context=(\d+) seqno=(\d+)/;FenceParser.prototype={__proto__:Parser.prototype,initEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
-const thread=this.importer.getOrCreatePseudoThread(event[2]);thread.lastActiveTs=ts;return true;},fenceDestroyEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
-const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_destroy('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{'driver':event[1],'context':event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
-if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
-thread.lastActiveTs=ts;},fenceEnableSignalEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
-const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_enable('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{'driver':event[1],'context':event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
-if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
-thread.lastActiveTs=ts;},fenceSignaledEvent(eventName,cpuNumber,pid,ts,eventBase){const event=fenceRE.exec(eventBase.details);if(!event)return false;if(eventBase.tgid===undefined){return false;}
-const thread=this.importer.getOrCreatePseudoThread(event[2]);const name='fence_signal('+event[4]+')';const colorName='fence('+event[4]+')';if(thread.lastActiveTs!==undefined){const duration=ts-thread.lastActiveTs;const slice=new tr.model.ThreadSlice('',name,ColorScheme.getColorIdForGeneralPurposeString(colorName),thread.lastActiveTs,{'driver':event[1],'context':event[3]},duration);thread.thread.sliceGroup.pushSlice(slice);}
-if(thread.thread.sliceGroup.openSliceCount>0){thread.thread.sliceGroup.endSlice(ts);}
-thread.lastActiveTs=ts;return true;},};Parser.register(FenceParser);return{FenceParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const Parser=tr.e.importer.linux_perf.Parser;function GestureParser(importer){Parser.call(this,importer);importer.registerEventHandler('tracing_mark_write:log',GestureParser.prototype.logEvent.bind(this));importer.registerEventHandler('tracing_mark_write:SyncInterpret',GestureParser.prototype.syncEvent.bind(this));importer.registerEventHandler('tracing_mark_write:HandleTimer',GestureParser.prototype.timerEvent.bind(this));}
+return true;}};Parser.register(ExynosParser);return{ExynosParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const Parser=tr.e.importer.linux_perf.Parser;function GestureParser(importer){Parser.call(this,importer);importer.registerEventHandler('tracing_mark_write:log',GestureParser.prototype.logEvent.bind(this));importer.registerEventHandler('tracing_mark_write:SyncInterpret',GestureParser.prototype.syncEvent.bind(this));importer.registerEventHandler('tracing_mark_write:HandleTimer',GestureParser.prototype.timerEvent.bind(this));}
 GestureParser.prototype={__proto__:Parser.prototype,gestureOpenSlice(title,ts,opt_args){const thread=this.importer.getOrCreatePseudoThread('gesture').thread;thread.sliceGroup.beginSlice('touchpad_gesture',title,ts,opt_args);},gestureCloseSlice(title,ts){const thread=this.importer.getOrCreatePseudoThread('gesture').thread;if(thread.sliceGroup.openSliceCount){const slice=thread.sliceGroup.mostRecentlyOpenedPartialSlice;if(slice.title!==title){this.importer.model.importWarning({type:'title_match_error',message:'Titles do not match. Title is '+
 slice.title+' in openSlice, and is '+
 title+' in endSlice'});}else{thread.sliceGroup.endSlice(ts);}}},logEvent(eventName,cpuNumber,pid,ts,eventBase){const innerEvent=/^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);switch(innerEvent[1]){case'start':this.gestureOpenSlice('GestureLog',ts,{name:innerEvent[2]});break;case'end':this.gestureCloseSlice('GestureLog',ts);}
@@ -6057,7 +6203,10 @@
 Parser.register(I2cParser);return{I2cParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function I915Parser(importer){Parser.call(this,importer);importer.registerEventHandler('i915_gem_object_create',I915Parser.prototype.gemObjectCreateEvent.bind(this));importer.registerEventHandler('i915_gem_object_bind',I915Parser.prototype.gemObjectBindEvent.bind(this));importer.registerEventHandler('i915_gem_object_unbind',I915Parser.prototype.gemObjectBindEvent.bind(this));importer.registerEventHandler('i915_gem_object_change_domain',I915Parser.prototype.gemObjectChangeDomainEvent.bind(this));importer.registerEventHandler('i915_gem_object_pread',I915Parser.prototype.gemObjectPreadWriteEvent.bind(this));importer.registerEventHandler('i915_gem_object_pwrite',I915Parser.prototype.gemObjectPreadWriteEvent.bind(this));importer.registerEventHandler('i915_gem_object_fault',I915Parser.prototype.gemObjectFaultEvent.bind(this));importer.registerEventHandler('i915_gem_object_clflush',I915Parser.prototype.gemObjectDestroyEvent.bind(this));importer.registerEventHandler('i915_gem_object_destroy',I915Parser.prototype.gemObjectDestroyEvent.bind(this));importer.registerEventHandler('i915_gem_ring_dispatch',I915Parser.prototype.gemRingDispatchEvent.bind(this));importer.registerEventHandler('i915_gem_ring_flush',I915Parser.prototype.gemRingFlushEvent.bind(this));importer.registerEventHandler('i915_gem_request',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_request_add',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_request_complete',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_request_retire',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_request_wait_begin',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_request_wait_end',I915Parser.prototype.gemRequestEvent.bind(this));importer.registerEventHandler('i915_gem_ring_wait_begin',I915Parser.prototype.gemRingWaitEvent.bind(this));importer.registerEventHandler('i915_gem_ring_wait_end',I915Parser.prototype.gemRingWaitEvent.bind(this));importer.registerEventHandler('i915_reg_rw',I915Parser.prototype.regRWEvent.bind(this));importer.registerEventHandler('i915_flip_request',I915Parser.prototype.flipEvent.bind(this));importer.registerEventHandler('i915_flip_complete',I915Parser.prototype.flipEvent.bind(this));importer.registerEventHandler('intel_gpu_freq_change',I915Parser.prototype.gpuFrequency.bind(this));}
 I915Parser.prototype={__proto__:Parser.prototype,i915FlipOpenSlice(ts,obj,plane){const kthread=this.importer.getOrCreatePseudoThread('i915_flip');kthread.openSliceTS=ts;kthread.openSlice='flip:'+obj+'/'+plane;},i915FlipCloseSlice(ts,args){const kthread=this.importer.getOrCreatePseudoThread('i915_flip');if(kthread.openSlice){const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),kthread.openSliceTS,args,ts-kthread.openSliceTS);kthread.thread.sliceGroup.pushSlice(slice);}
 kthread.openSlice=undefined;},i915GemObjectSlice(ts,eventName,obj,args){const kthread=this.importer.getOrCreatePseudoThread('i915_gem');kthread.openSlice=eventName+':'+obj;const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),ts,args,0);kthread.thread.sliceGroup.pushSlice(slice);},i915GemRingSlice(ts,eventName,dev,ring,args){const kthread=this.importer.getOrCreatePseudoThread('i915_gem_ring');kthread.openSlice=eventName+':'+dev+'.'+ring;const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),ts,args,0);kthread.thread.sliceGroup.pushSlice(slice);},i915RegSlice(ts,eventName,reg,args){const kthread=this.importer.getOrCreatePseudoThread('i915_reg');kthread.openSlice=eventName+':'+reg;const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),ts,args,0);kthread.thread.sliceGroup.pushSlice(slice);},i915FreqChangeSlice(ts,eventName,args){const kthread=this.importer.getOrCreatePseudoThread('i915_gpu_freq');kthread.openSlice=eventName;const slice=new tr.model.ThreadSlice('',kthread.openSlice,ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),ts,args,0);kthread.thread.sliceGroup.pushSlice(slice);},gemObjectCreateEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+), size=(\d+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];const size=parseInt(event[2]);this.i915GemObjectSlice(ts,eventName,obj,{obj,size});return true;},gemObjectBindEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+), offset=(\w+), size=(\d+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];const offset=event[2];const size=parseInt(event[3]);this.i915ObjectGemSlice(ts,eventName+':'+obj,{obj,offset,size});return true;},gemObjectChangeDomainEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+), read=(\w+=>\w+), write=(\w+=>\w+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];const read=event[2];const write=event[3];this.i915GemObjectSlice(ts,eventName,obj,{obj,read,write});return true;},gemObjectPreadWriteEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+), offset=(\d+), len=(\d+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];const offset=parseInt(event[2]);const len=parseInt(event[3]);this.i915GemObjectSlice(ts,eventName,obj,{obj,offset,len});return true;},gemObjectFaultEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+), (\w+) index=(\d+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];const type=event[2];const index=parseInt(event[3]);this.i915GemObjectSlice(ts,eventName,obj,{obj,type,index});return true;},gemObjectDestroyEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/obj=(\w+)/.exec(eventBase.details);if(!event)return false;const obj=event[1];this.i915GemObjectSlice(ts,eventName,obj,{obj});return true;},gemRingDispatchEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/dev=(\d+), ring=(\d+), seqno=(\d+)/.exec(eventBase.details);if(!event)return false;const dev=parseInt(event[1]);const ring=parseInt(event[2]);const seqno=parseInt(event[3]);this.i915GemRingSlice(ts,eventName,dev,ring,{dev,ring,seqno});return true;},gemRingFlushEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/dev=(\d+), ring=(\w+), invalidate=(\w+), flush=(\w+)/.exec(eventBase.details);if(!event)return false;const dev=parseInt(event[1]);const ring=parseInt(event[2]);const invalidate=event[3];const flush=event[4];this.i915GemRingSlice(ts,eventName,dev,ring,{dev,ring,invalidate,flush});return true;},gemRequestEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/dev=(\d+), ring=(\d+), seqno=(\d+)/.exec(eventBase.details);if(!event)return false;const dev=parseInt(event[1]);const ring=parseInt(event[2]);const seqno=parseInt(event[3]);this.i915GemRingSlice(ts,eventName,dev,ring,{dev,ring,seqno});return true;},gemRingWaitEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/dev=(\d+), ring=(\d+)/.exec(eventBase.details);if(!event)return false;const dev=parseInt(event[1]);const ring=parseInt(event[2]);this.i915GemRingSlice(ts,eventName,dev,ring,{dev,ring});return true;},regRWEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/(\w+) reg=(\w+), len=(\d+), val=(\(\w+, \w+\))/.exec(eventBase.details);if(!event)return false;const rw=event[1];const reg=event[2];const len=event[3];const data=event[3];this.i915RegSlice(ts,rw,reg,{rw,reg,len,data});return true;},flipEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/plane=(\d+), obj=(\w+)/.exec(eventBase.details);if(!event)return false;const plane=parseInt(event[1]);const obj=event[2];if(eventName==='i915_flip_request'){this.i915FlipOpenSlice(ts,obj,plane);}else{this.i915FlipCloseSlice(ts,{obj,plane});}
-return true;},gpuFrequency(eventName,cpuNumver,pid,ts,eventBase){const event=/new_freq=(\d+)/.exec(eventBase.details);if(!event)return false;const freq=parseInt(event[1]);this.i915FreqChangeSlice(ts,eventName,{freq});return true;}};Parser.register(I915Parser);return{I915Parser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function IrqParser(importer){Parser.call(this,importer);importer.registerEventHandler('irq_handler_entry',IrqParser.prototype.irqHandlerEntryEvent.bind(this));importer.registerEventHandler('irq_handler_exit',IrqParser.prototype.irqHandlerExitEvent.bind(this));importer.registerEventHandler('softirq_raise',IrqParser.prototype.softirqRaiseEvent.bind(this));importer.registerEventHandler('softirq_entry',IrqParser.prototype.softirqEntryEvent.bind(this));importer.registerEventHandler('softirq_exit',IrqParser.prototype.softirqExitEvent.bind(this));importer.registerEventHandler('ipi_entry',IrqParser.prototype.ipiEntryEvent.bind(this));importer.registerEventHandler('ipi_exit',IrqParser.prototype.ipiExitEvent.bind(this));importer.registerEventHandler('preempt_disable',IrqParser.prototype.preemptStartEvent.bind(this));importer.registerEventHandler('preempt_enable',IrqParser.prototype.preemptEndEvent.bind(this));importer.registerEventHandler('irq_disable',IrqParser.prototype.irqoffStartEvent.bind(this));importer.registerEventHandler('irq_enable',IrqParser.prototype.irqoffEndEvent.bind(this));}
+return true;},gpuFrequency(eventName,cpuNumver,pid,ts,eventBase){const event=/new_freq=(\d+)/.exec(eventBase.details);if(!event)return false;const freq=parseInt(event[1]);this.i915FreqChangeSlice(ts,eventName,{freq});return true;}};Parser.register(I915Parser);return{I915Parser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function IonHeapParser(importer){Parser.call(this,importer);importer.registerEventHandler('ion_heap_shrink',IonHeapParser.prototype.traceIonHeapShrink.bind(this));importer.registerEventHandler('ion_heap_grow',IonHeapParser.prototype.traceIonHeapGrow.bind(this));this.model_=importer.model_;}
+const TestExports={};const ionHeapRE=new RegExp('heap_name=(\\S+), len=(\\d+), total_allocated=(\\d+)');TestExports.ionHeapRE=ionHeapRE;IonHeapParser.prototype={__proto__:Parser.prototype,traceIonHeapShrink(eventName,cpuNumber,pid,ts,eventBase,threadName){const event=ionHeapRE.exec(eventBase.details);if(!event)return false;const name=event[1];const len=parseInt(event[2]);const totalAllocated=parseInt(event[3]);const ionHeap=totalAllocated+len;const ctr=this.model_.kernel.getOrCreateCounter(null,name+' ion heap');if(ctr.numSeries===0){ctr.addSeries(new tr.model.CounterSeries('value',ColorScheme.getColorIdForGeneralPurposeString(ctr.name+'.'+'value')));}
+ctr.series.forEach(function(series){series.addCounterSample(ts,ionHeap);});return true;},traceIonHeapGrow(eventName,cpuNumber,pid,ts,eventBase,threadName){const event=ionHeapRE.exec(eventBase.details);if(!event)return false;const name=event[1];const len=parseInt(event[2]);const totalAllocated=parseInt(event[3]);const ionHeap=totalAllocated+len;const ctr=this.model_.kernel.getOrCreateCounter(null,name+' ion heap');if(ctr.numSeries===0){ctr.addSeries(new tr.model.CounterSeries('value',ColorScheme.getColorIdForGeneralPurposeString(ctr.name+'.'+'value')));}
+ctr.series.forEach(function(series){series.addCounterSample(ts,ionHeap);});return true;}};Parser.register(IonHeapParser);return{IonHeapParser,_IonHeapParserTestExports:TestExports};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function IrqParser(importer){Parser.call(this,importer);importer.registerEventHandler('irq_handler_entry',IrqParser.prototype.irqHandlerEntryEvent.bind(this));importer.registerEventHandler('irq_handler_exit',IrqParser.prototype.irqHandlerExitEvent.bind(this));importer.registerEventHandler('softirq_raise',IrqParser.prototype.softirqRaiseEvent.bind(this));importer.registerEventHandler('softirq_entry',IrqParser.prototype.softirqEntryEvent.bind(this));importer.registerEventHandler('softirq_exit',IrqParser.prototype.softirqExitEvent.bind(this));importer.registerEventHandler('ipi_entry',IrqParser.prototype.ipiEntryEvent.bind(this));importer.registerEventHandler('ipi_exit',IrqParser.prototype.ipiExitEvent.bind(this));importer.registerEventHandler('preempt_disable',IrqParser.prototype.preemptStartEvent.bind(this));importer.registerEventHandler('preempt_enable',IrqParser.prototype.preemptEndEvent.bind(this));importer.registerEventHandler('irq_disable',IrqParser.prototype.irqoffStartEvent.bind(this));importer.registerEventHandler('irq_enable',IrqParser.prototype.irqoffEndEvent.bind(this));}
 const irqHandlerEntryRE=/irq=(\d+) name=(.+)/;const irqHandlerExitRE=/irq=(\d+) ret=(.+)/;const softirqRE=/vec=(\d+) \[action=(.+)\]/;const ipiHandlerExitRE=/\((.+)\)/;const preemptirqRE=/caller=(.+) parent=(.+)/;IrqParser.prototype={__proto__:Parser.prototype,irqHandlerEntryEvent(eventName,cpuNumber,pid,ts,eventBase){const event=irqHandlerEntryRE.exec(eventBase.details);if(!event)return false;const irq=parseInt(event[1]);const name=event[2];const thread=this.importer.getOrCreatePseudoThread('irqs cpu '+cpuNumber);thread.lastEntryTs=ts;thread.irqName=name;return true;},irqHandlerExitEvent(eventName,cpuNumber,pid,ts,eventBase){const event=irqHandlerExitRE.exec(eventBase.details);if(!event)return false;const irq=parseInt(event[1]);const ret=event[2];const thread=this.importer.getOrCreatePseudoThread('irqs cpu '+cpuNumber);if(thread.lastEntryTs!==undefined){const duration=ts-thread.lastEntryTs;const slice=new tr.model.ThreadSlice('','IRQ ('+thread.irqName+')',ColorScheme.getColorIdForGeneralPurposeString(event[1]),thread.lastEntryTs,{ret},duration);thread.thread.sliceGroup.pushSlice(slice);}
 thread.lastEntryTs=undefined;thread.irqName=undefined;return true;},softirqRaiseEvent(eventName,cpuNumber,pid,ts,eventBase){return true;},softirqEntryEvent(eventName,cpuNumber,pid,ts,eventBase){const event=softirqRE.exec(eventBase.details);if(!event)return false;const action=event[2];const thread=this.importer.getOrCreatePseudoThread('softirq cpu '+cpuNumber);thread.lastEntryTs=ts;return true;},softirqExitEvent(eventName,cpuNumber,pid,ts,eventBase){const event=softirqRE.exec(eventBase.details);if(!event)return false;const vec=parseInt(event[1]);const action=event[2];const thread=this.importer.getOrCreatePseudoThread('softirq cpu '+cpuNumber);if(thread.lastEntryTs!==undefined){const duration=ts-thread.lastEntryTs;const slice=new tr.model.ThreadSlice('',action,ColorScheme.getColorIdForGeneralPurposeString(event[1]),thread.lastEntryTs,{vec},duration);thread.thread.sliceGroup.pushSlice(slice);}
 thread.lastEntryTs=undefined;return true;},ipiEntryEvent(eventName,cpuNumber,pid,ts,eventBase){const thread=this.importer.getOrCreatePseudoThread('irqs cpu '+cpuNumber);thread.lastEntryTs=ts;return true;},ipiExitEvent(eventName,cpuNumber,pid,ts,eventBase){const event=ipiHandlerExitRE.exec(eventBase.details);if(!event)return false;const ipiName=event[1];const thread=this.importer.getOrCreatePseudoThread('irqs cpu '+cpuNumber);if(thread.lastEntryTs!==undefined){const duration=ts-thread.lastEntryTs;const slice=new tr.model.ThreadSlice('','IPI ('+ipiName+')',ColorScheme.getColorIdForGeneralPurposeString(ipiName),thread.lastEntryTs,{},duration);thread.thread.sliceGroup.pushSlice(slice);}
@@ -6115,7 +6264,11 @@
 powerCounter.series.forEach(function(series){if(series.name==='Min Frequency'){series.addCounterSample(ts,minFreq);}
 if(series.name==='Max Frequency'){series.addCounterSample(ts,maxFreq);}});},powerStartEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/type=(\d+) state=(\d) cpu_id=(\d+)/.exec(eventBase.details);if(!event)return false;const targetCpuNumber=parseInt(event[3]);const cpuState=parseInt(event[2]);this.cpuStateSlice(ts,targetCpuNumber,event[1],cpuState);return true;},powerFrequencyEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/type=(\d+) state=(\d+) cpu_id=(\d+)/.exec(eventBase.details);if(!event)return false;const targetCpuNumber=parseInt(event[3]);const powerState=parseInt(event[2]);this.cpuFrequencySlice(ts,targetCpuNumber,powerState);return true;},cpuFrequencyEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/state=(\d+) cpu_id=(\d+)/.exec(eventBase.details);if(!event)return false;const targetCpuNumber=parseInt(event[2]);const powerState=parseInt(event[1]);this.cpuFrequencySlice(ts,targetCpuNumber,powerState);return true;},cpuFrequencyLimitsEvent(eventName,cpu,pid,ts,eventBase){const event=/min=(\d+) max=(\d+) cpu_id=(\d+)/.exec(eventBase.details);if(!event)return false;const targetCpuNumber=parseInt(event[3]);const minFreq=parseInt(event[1]);const maxFreq=parseInt(event[2]);this.cpuFrequencyLimitsSlice(ts,targetCpuNumber,minFreq,maxFreq);return true;},cpuIdleEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/state=(\d+) cpu_id=(\d+)/.exec(eventBase.details);if(!event)return false;const targetCpuNumber=parseInt(event[2]);const cpuState=parseInt(event[1]);this.cpuIdleSlice(ts,targetCpuNumber,cpuState);return true;}};Parser.register(PowerParser);return{PowerParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const ColorScheme=tr.b.ColorScheme;const Parser=tr.e.importer.linux_perf.Parser;function RegulatorParser(importer){Parser.call(this,importer);importer.registerEventHandler('regulator_enable',RegulatorParser.prototype.regulatorEnableEvent.bind(this));importer.registerEventHandler('regulator_enable_delay',RegulatorParser.prototype.regulatorEnableDelayEvent.bind(this));importer.registerEventHandler('regulator_enable_complete',RegulatorParser.prototype.regulatorEnableCompleteEvent.bind(this));importer.registerEventHandler('regulator_disable',RegulatorParser.prototype.regulatorDisableEvent.bind(this));importer.registerEventHandler('regulator_disable_complete',RegulatorParser.prototype.regulatorDisableCompleteEvent.bind(this));importer.registerEventHandler('regulator_set_voltage',RegulatorParser.prototype.regulatorSetVoltageEvent.bind(this));importer.registerEventHandler('regulator_set_voltage_complete',RegulatorParser.prototype.regulatorSetVoltageCompleteEvent.bind(this));this.model_=importer.model_;}
 const regulatorEnableRE=/name=(.+)/;const regulatorDisableRE=/name=(.+)/;const regulatorSetVoltageCompleteRE=/name=(\S+), val=(\d+)/;RegulatorParser.prototype={__proto__:Parser.prototype,getCtr_(ctrName,valueName){const ctr=this.model_.kernel.getOrCreateCounter(null,'vreg '+ctrName+' '+valueName);if(ctr.series[0]===undefined){ctr.addSeries(new tr.model.CounterSeries(valueName,ColorScheme.getColorIdForGeneralPurposeString(ctrName+'.'+valueName)));}
-return ctr;},regulatorEnableEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorEnableRE.exec(eventBase.details);if(!event)return false;const name=event[1];const ctr=this.getCtr_(name,'enabled');ctr.series[0].addCounterSample(ts,1);return true;},regulatorEnableDelayEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorEnableCompleteEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorDisableEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorDisableRE.exec(eventBase.details);if(!event)return false;const name=event[1];const ctr=this.getCtr_(name,'enabled');ctr.series[0].addCounterSample(ts,0);return true;},regulatorDisableCompleteEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorSetVoltageEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorSetVoltageCompleteEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorSetVoltageCompleteRE.exec(eventBase.details);if(!event)return false;const name=event[1];const voltage=parseInt(event[2]);const ctr=this.getCtr_(name,'voltage');ctr.series[0].addCounterSample(ts,voltage);return true;}};Parser.register(RegulatorParser);return{RegulatorParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const Parser=tr.e.importer.linux_perf.Parser;function SchedParser(importer){Parser.call(this,importer);importer.registerEventHandler('sched_switch',SchedParser.prototype.schedSwitchEvent.bind(this));importer.registerEventHandler('sched_wakeup',SchedParser.prototype.schedWakeupEvent.bind(this));importer.registerEventHandler('sched_blocked_reason',SchedParser.prototype.schedBlockedEvent.bind(this));importer.registerEventHandler('sched_cpu_hotplug',SchedParser.prototype.schedCpuHotplugEvent.bind(this));}
+return ctr;},regulatorEnableEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorEnableRE.exec(eventBase.details);if(!event)return false;const name=event[1];const ctr=this.getCtr_(name,'enabled');ctr.series[0].addCounterSample(ts,1);return true;},regulatorEnableDelayEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorEnableCompleteEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorDisableEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorDisableRE.exec(eventBase.details);if(!event)return false;const name=event[1];const ctr=this.getCtr_(name,'enabled');ctr.series[0].addCounterSample(ts,0);return true;},regulatorDisableCompleteEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorSetVoltageEvent(eventName,cpuNum,pid,ts,eventBase){return true;},regulatorSetVoltageCompleteEvent(eventName,cpuNum,pid,ts,eventBase){const event=regulatorSetVoltageCompleteRE.exec(eventBase.details);if(!event)return false;const name=event[1];const voltage=parseInt(event[2]);const ctr=this.getCtr_(name,'voltage');ctr.series[0].addCounterSample(ts,voltage);return true;}};Parser.register(RegulatorParser);return{RegulatorParser,};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const Parser=tr.e.importer.linux_perf.Parser;function RssParser(importer){Parser.call(this,importer);importer.registerEventHandler('rss_stat',RssParser.prototype.rssStat.bind(this));}
+const TestExports={};const rssStatRE=new RegExp('member=(\\d+) size=(\\d+)');TestExports.rssStatRE=rssStatRE;const unknownThreadName='<...>';RssParser.prototype={__proto__:Parser.prototype,rssStat(eventName,cpuNumber,pid,ts,eventBase){const event=rssStatRE.exec(eventBase.details);if(!event)return false;const member=parseInt(event[1]);const size=parseInt(event[2]);if(eventBase.tgid===undefined){return false;}
+const tgid=parseInt(eventBase.tgid);const process=this.importer.model_.getOrCreateProcess(tgid);let subTitle='';if(member===0){subTitle=' (file pages)';}else if(member===1){subTitle=' (anon)';}
+const rssCounter=process.getOrCreateCounter('RSS','RSS '+member+subTitle);if(rssCounter.numSeries===0){rssCounter.addSeries(new tr.model.CounterSeries('RSS',tr.b.ColorScheme.getColorIdForGeneralPurposeString(rssCounter.name)));}
+rssCounter.series.forEach(function(series){series.addCounterSample(ts,size);});return true;},};Parser.register(RssParser);return{RssParser,_RssParserTestExports:TestExports};});'use strict';tr.exportTo('tr.e.importer.linux_perf',function(){const Parser=tr.e.importer.linux_perf.Parser;function SchedParser(importer){Parser.call(this,importer);importer.registerEventHandler('sched_switch',SchedParser.prototype.schedSwitchEvent.bind(this));importer.registerEventHandler('sched_wakeup',SchedParser.prototype.schedWakeupEvent.bind(this));importer.registerEventHandler('sched_blocked_reason',SchedParser.prototype.schedBlockedEvent.bind(this));importer.registerEventHandler('sched_cpu_hotplug',SchedParser.prototype.schedCpuHotplugEvent.bind(this));}
 const TestExports={};const schedSwitchRE=new RegExp('prev_comm=(.+) prev_pid=(\\d+) prev_prio=(\\d+) '+'prev_state=(\\S\\+?|\\S\\|\\S) ==> '+'next_comm=(.+) next_pid=(\\d+) next_prio=(\\d+)');const schedBlockedRE=new RegExp('pid=(\\d+) iowait=(\\d) caller=(.+)');TestExports.schedSwitchRE=schedSwitchRE;const schedWakeupRE=/comm=(.+) pid=(\d+) prio=(\d+)(?: success=\d+)? target_cpu=(\d+)/;TestExports.schedWakeupRE=schedWakeupRE;const unknownThreadName='<...>';SchedParser.prototype={__proto__:Parser.prototype,schedSwitchEvent(eventName,cpuNumber,pid,ts,eventBase){const event=schedSwitchRE.exec(eventBase.details);if(!event)return false;const prevState=event[4];const nextComm=event[5];const nextPid=parseInt(event[6]);const nextPrio=parseInt(event[7]);if(eventBase.tgid!==undefined){const tgid=parseInt(eventBase.tgid);const process=this.importer.model_.getOrCreateProcess(tgid);const storedThread=process.getThread(pid);if(!storedThread){const thread=process.getOrCreateThread(pid);thread.name=eventBase.threadName;}else if(storedThread.name===unknownThreadName){storedThread.name=eventBase.threadName;}}
 const nextThread=this.importer.threadsByLinuxPid[nextPid];let nextName;if(nextThread){nextName=nextThread.userFriendlyName;}else{nextName=nextComm;}
 const cpu=this.importer.getOrCreateCpu(cpuNumber);cpu.switchActiveThread(ts,{stateWhenDescheduled:prevState},nextPid,nextName,{comm:nextComm,tid:nextPid,prio:nextPrio});return true;},schedWakeupEvent(eventName,cpuNumber,pid,ts,eventBase){const event=schedWakeupRE.exec(eventBase.details);if(!event)return false;const fromPid=pid;const comm=event[1];pid=parseInt(event[2]);const prio=parseInt(event[3]);this.importer.markPidRunnable(ts,pid,comm,prio,fromPid);return true;},schedCpuHotplugEvent(eventName,cpuNumber,pid,ts,eventBase){const event=/cpu (\d+) (.+) error=(\d+)/.exec(eventBase.details);if(!event)return false;cpuNumber=event[1];const state=event[2];const targetCpu=this.importer.getOrCreateCpu(cpuNumber);const powerCounter=targetCpu.getOrCreateCounter('','Cpu Hotplug');if(powerCounter.numSeries===0){powerCounter.addSeries(new tr.model.CounterSeries('State',tr.b.ColorScheme.getColorIdForGeneralPurposeString(powerCounter.name+'.'+'State')));}
@@ -6137,7 +6290,7 @@
 let events=[];if(produceResult){for(let i=0;i<rawEvents.length;i++){let event=rawEvents[i];event=stripSuffix(event,'\\n\\');events.push(event);}}else{events=[rawEvents[rawEvents.length-1]];}
 const oldLastEvent=events[events.length-1];const newLastEvent=stripSuffix(oldLastEvent,'\\n";');if(newLastEvent===oldLastEvent)return failure;events[events.length-1]=newLastEvent;return{ok:true,lines:produceResult?events:undefined,eventsBeginAtLine};};FTraceImporter._extractEventsFromSystraceMultiHTML=function(incomingEvents,produceResult){const failure={ok:false};if(produceResult===undefined)produceResult=true;const header=incomingEvents instanceof tr.b.TraceStream?incomingEvents.header:incomingEvents;if(!(new RegExp('^<!DOCTYPE HTML>','i').test(header)))return failure;const r=new tr.importer.SimpleLineReader(incomingEvents);let events=[];let eventsBeginAtLine;while(!/^# tracer:/.test(events)){if(!r.advanceToLineMatching(/^  <script class="trace-data" type="application\/text">$/)){return failure;}
 eventsBeginAtLine=r.curLineNumber+1;r.beginSavingLines();if(!r.advanceToLineMatching(/^  <\/script>$/))return failure;events=r.endSavingLinesAndGetResult();events=events.slice(1,events.length-1);}
-if(!r.advanceToLineMatching(/^<\/body>$/))return failure;if(!r.advanceToLineMatching(/^<\/html>$/))return failure;return{ok:true,lines:produceResult?events:undefined,eventsBeginAtLine,};};FTraceImporter.prototype={__proto__:tr.importer.Importer.prototype,get importerName(){return'FTraceImporter';},get model(){return this.model_;},importClockSyncMarkers(){this.lazyInit_();this.forEachLine_(function(text,eventBase,cpuNumber,pid,ts){const eventName=eventBase.eventName;if(eventName!=='tracing_mark_write'&&eventName!=='0')return;if(traceEventClockSyncRE.exec(eventBase.details)||genericClockSyncRE.exec(eventBase.details)){this.traceClockSyncEvent_(eventName,cpuNumber,pid,ts,eventBase);}else if(realTimeClockSyncRE.exec(eventBase.details)){const match=realTimeClockSyncRE.exec(eventBase.details);this.model_.realtime_to_monotonic_offset_ms=ts-match[1];}}.bind(this));},importEvents(){const modelTimeTransformer=this.model_.clockSyncManager.getModelTimeTransformer(this.clockDomainId_);this.importCpuData_(modelTimeTransformer);this.buildMapFromLinuxPidsToThreads_();this.buildPerThreadCpuSlicesFromCpuState_();},registerEventHandler(eventName,handler){this.eventHandlers_[eventName]=handler;},getOrCreateCpu(cpuNumber){return this.model_.kernel.getOrCreateCpu(cpuNumber);},getOrCreateKernelThread(kernelThreadName,pid,tid){if(!this.kernelThreadStates_[kernelThreadName]){const thread=this.model_.getOrCreateProcess(pid).getOrCreateThread(tid);thread.name=kernelThreadName;this.kernelThreadStates_[kernelThreadName]={pid,thread,openSlice:undefined,openSliceTS:undefined};this.threadsByLinuxPid[tid]=thread;}
+if(!r.advanceToLineMatching(/^<\/body>$/))return failure;if(!r.advanceToLineMatching(/^<\/html>$/))return failure;return{ok:true,lines:produceResult?events:undefined,eventsBeginAtLine,};};FTraceImporter.prototype={__proto__:tr.importer.Importer.prototype,get importerName(){return'FTraceImporter';},get model(){return this.model_;},importClockSyncMarkers(){this.lazyInit_();this.forEachLine_(function(text,eventBase,cpuNumber,pid,ts){const eventName=eventBase.eventName;if(eventName!=='tracing_mark_write'&&eventName!=='0')return;if(traceEventClockSyncRE.exec(eventBase.details)||genericClockSyncRE.exec(eventBase.details)){this.traceClockSyncEvent_(eventName,cpuNumber,pid,ts,eventBase);}else if(realTimeClockSyncRE.exec(eventBase.details)){const match=realTimeClockSyncRE.exec(eventBase.details);this.model_.realtime_to_monotonic_offset_ms=ts-match[1];}}.bind(this));},importEvents(){if(this.lines_.length===0)return;const modelTimeTransformer=this.model_.clockSyncManager.getModelTimeTransformer(this.clockDomainId_);this.importCpuData_(modelTimeTransformer);this.buildMapFromLinuxPidsToThreads_();this.buildPerThreadCpuSlicesFromCpuState_();},registerEventHandler(eventName,handler){this.eventHandlers_[eventName]=handler;},getOrCreateCpu(cpuNumber){return this.model_.kernel.getOrCreateCpu(cpuNumber);},getOrCreateKernelThread(kernelThreadName,pid,tid){if(!this.kernelThreadStates_[kernelThreadName]){const thread=this.model_.getOrCreateProcess(pid).getOrCreateThread(tid);thread.name=kernelThreadName;this.kernelThreadStates_[kernelThreadName]={pid,thread,openSlice:undefined,openSliceTS:undefined};this.threadsByLinuxPid[tid]=thread;}
 return this.kernelThreadStates_[kernelThreadName];},getOrCreateBinderKernelThread(kernelThreadName,pid,tid){const key=kernelThreadName+pid+tid;if(!this.kernelThreadStates_[key]){const thread=this.model_.getOrCreateProcess(pid).getOrCreateThread(tid);thread.name=kernelThreadName;this.kernelThreadStates_[key]={pid,thread,openSlice:undefined,openSliceTS:undefined};this.threadsByLinuxPid[tid]=thread;}
 return this.kernelThreadStates_[key];},getOrCreatePseudoThread(threadName){let thread=this.kernelThreadStates_[threadName];if(!thread){thread=this.getOrCreateKernelThread(threadName,pseudoKernelPID,this.pseudoThreadCounter);this.pseudoThreadCounter++;}
 return thread;},markPidRunnable(ts,pid,comm,prio,fromPid){this.wakeups_.push({ts,tid:pid,fromTid:fromPid});},addPidBlockedReason(ts,pid,iowait,caller){this.blockedReasons_.push({ts,tid:pid,iowait,caller});},buildMapFromLinuxPidsToThreads_(){this.threadsByLinuxPid={};this.model_.getAllThreads().forEach(function(thread){this.threadsByLinuxPid[thread.tid]=thread;}.bind(this));},buildPerThreadCpuSlicesFromCpuState_(){const SCHEDULING_STATE=tr.model.SCHEDULING_STATE;for(const cpuNumber in this.model_.kernel.cpus){const cpu=this.model_.kernel.cpus[cpuNumber];for(let i=0;i<cpu.slices.length;i++){const cpuSlice=cpu.slices[i];const thread=this.threadsByLinuxPid[cpuSlice.args.tid];if(!thread)continue;cpuSlice.threadThatWasRunning=thread;if(!thread.tempCpuSlices){thread.tempCpuSlices=[];}
@@ -6254,7 +6407,8 @@
 ProtoExpectation.prototype={get isValid(){return this.end>this.start;},containsTypeNames(typeNames){return this.associatedEvents.some(x=>typeNames.indexOf(x.typeName)>=0);},containsSliceTitle(title){return this.associatedEvents.some(x=>title===x.title);},createInteractionRecord(model){if(this.type!==ProtoExpectation.IGNORED_TYPE&&!this.isValid){model.importWarning({type:'ProtoExpectation',message:'Please file a bug with this trace. '+this.debug(),showToUser:true});return undefined;}
 const duration=this.end-this.start;let ir=undefined;switch(this.type){case ProtoExpectation.RESPONSE_TYPE:ir=new tr.model.um.ResponseExpectation(model,this.initiatorType,this.start,duration,this.isAnimationBegin);break;case ProtoExpectation.ANIMATION_TYPE:ir=new tr.model.um.AnimationExpectation(model,this.initiatorType,this.start,duration);break;}
 if(!ir)return undefined;ir.sourceEvents.addEventSet(this.associatedEvents);function pushAssociatedEvents(event){ir.associatedEvents.push(event);if(event.associatedEvents){ir.associatedEvents.addEventSet(event.associatedEvents);}}
-this.associatedEvents.forEach(function(event){pushAssociatedEvents(event);if(event.subSlices){event.subSlices.forEach(pushAssociatedEvents);}});return ir;},merge(other){this.initiatorType=combineInitiatorTypes(this.initiatorType,other.initiatorType);this.associatedEvents.addEventSet(other.associatedEvents);this.start=Math.min(this.start,other.start);this.end=Math.max(this.end,other.end);if(other.isAnimationBegin){this.isAnimationBegin=true;}},pushEvent(event){this.start=Math.min(this.start,event.start);this.end=Math.max(this.end,event.end);this.associatedEvents.push(event);},pushSample(sample){this.start=Math.min(this.start,sample.timestamp);this.end=Math.max(this.end,sample.timestamp);this.associatedEvents.push(sample);},containsTimestampInclusive(timestamp){return(this.start<=timestamp)&&(timestamp<=this.end);},intersects(other){return(other.start<this.end)&&(other.end>this.start);},isNear(event,threshold){return(this.end+threshold)>event.start;},debug(){let debugString=this.type+'(';debugString+=parseInt(this.start)+' ';debugString+=parseInt(this.end);this.associatedEvents.forEach(function(event){debugString+=' '+event.typeName;});return debugString+')';}};return{ProtoExpectation,};});'use strict';tr.exportTo('tr.importer',function(){const ProtoExpectation=tr.importer.ProtoExpectation;const INITIATOR_TYPE=tr.model.um.INITIATOR_TYPE;const INPUT_TYPE=tr.e.cc.INPUT_EVENT_TYPE_NAMES;const KEYBOARD_TYPE_NAMES=[INPUT_TYPE.CHAR,INPUT_TYPE.KEY_DOWN_RAW,INPUT_TYPE.KEY_DOWN,INPUT_TYPE.KEY_UP];const MOUSE_RESPONSE_TYPE_NAMES=[INPUT_TYPE.CLICK,INPUT_TYPE.CONTEXT_MENU];const MOUSE_WHEEL_TYPE_NAMES=[INPUT_TYPE.MOUSE_WHEEL];const MOUSE_DRAG_TYPE_NAMES=[INPUT_TYPE.MOUSE_DOWN,INPUT_TYPE.MOUSE_MOVE,INPUT_TYPE.MOUSE_UP];const TAP_TYPE_NAMES=[INPUT_TYPE.TAP,INPUT_TYPE.TAP_CANCEL,INPUT_TYPE.TAP_DOWN];const PINCH_TYPE_NAMES=[INPUT_TYPE.PINCH_BEGIN,INPUT_TYPE.PINCH_END,INPUT_TYPE.PINCH_UPDATE];const FLING_TYPE_NAMES=[INPUT_TYPE.FLING_CANCEL,INPUT_TYPE.FLING_START];const TOUCH_TYPE_NAMES=[INPUT_TYPE.TOUCH_END,INPUT_TYPE.TOUCH_MOVE,INPUT_TYPE.TOUCH_START];const SCROLL_TYPE_NAMES=[INPUT_TYPE.SCROLL_BEGIN,INPUT_TYPE.SCROLL_END,INPUT_TYPE.SCROLL_UPDATE];const ALL_HANDLED_TYPE_NAMES=[].concat(KEYBOARD_TYPE_NAMES,MOUSE_RESPONSE_TYPE_NAMES,MOUSE_WHEEL_TYPE_NAMES,MOUSE_DRAG_TYPE_NAMES,PINCH_TYPE_NAMES,TAP_TYPE_NAMES,FLING_TYPE_NAMES,TOUCH_TYPE_NAMES,SCROLL_TYPE_NAMES);const RENDERER_FLING_TITLE='InputHandlerProxy::HandleGestureFling::started';const PLAYBACK_EVENT_TITLE='VideoPlayback';const CSS_ANIMATION_TITLE='Animation';const VR_COUNTER_NAMES=['gpu.WebVR FPS','gpu.WebVR frame time (ms)','gpu.WebVR pose prediction (ms)',];const VR_EVENT_NAMES=['VrShellGl::AcquireFrame','VrShellGl::DrawFrame','VrShellGl::DrawSubmitFrameWhenReady','VrShellGl::DrawUiView','VrShellGl::UpdateController',];const VR_RESPONSE_MS=1000;const INPUT_MERGE_THRESHOLD_MS=200;const ANIMATION_MERGE_THRESHOLD_MS=32;const MOUSE_WHEEL_THRESHOLD_MS=40;const MOUSE_MOVE_THRESHOLD_MS=40;function compareEvents(x,y){if(x.start!==y.start){return x.start-y.start;}
+this.associatedEvents.forEach(function(event){pushAssociatedEvents(event);if(event.subSlices){event.subSlices.forEach(pushAssociatedEvents);}});return ir;},merge(other){this.initiatorType=combineInitiatorTypes(this.initiatorType,other.initiatorType);this.associatedEvents.addEventSet(other.associatedEvents);this.start=Math.min(this.start,other.start);this.end=Math.max(this.end,other.end);if(other.isAnimationBegin){this.isAnimationBegin=true;}},pushEvent(event){this.start=Math.min(this.start,event.start);this.end=Math.max(this.end,event.end);this.associatedEvents.push(event);},pushSample(sample){this.start=Math.min(this.start,sample.timestamp);this.end=Math.max(this.end,sample.timestamp);this.associatedEvents.push(sample);},containsTimestampInclusive(timestamp){return(this.start<=timestamp)&&(timestamp<=this.end);},intersects(other){return(other.start<this.end)&&(other.end>this.start);},isNear(event,threshold){return(this.end+threshold)>event.start;},debug(){let debugString=this.type+'(';debugString+=parseInt(this.start)+' ';debugString+=parseInt(this.end);this.associatedEvents.forEach(function(event){debugString+=' '+event.typeName;});return debugString+')';}};return{ProtoExpectation,};});'use strict';tr.exportTo('tr.importer',function(){const ProtoExpectation=tr.importer.ProtoExpectation;const INITIATOR_TYPE=tr.model.um.INITIATOR_TYPE;const INPUT_TYPE=tr.e.cc.INPUT_EVENT_TYPE_NAMES;const KEYBOARD_TYPE_NAMES=[INPUT_TYPE.CHAR,INPUT_TYPE.KEY_DOWN_RAW,INPUT_TYPE.KEY_DOWN,INPUT_TYPE.KEY_UP];const MOUSE_RESPONSE_TYPE_NAMES=[INPUT_TYPE.CLICK,INPUT_TYPE.CONTEXT_MENU];const MOUSE_WHEEL_TYPE_NAMES=[INPUT_TYPE.MOUSE_WHEEL];const MOUSE_DRAG_TYPE_NAMES=[INPUT_TYPE.MOUSE_DOWN,INPUT_TYPE.MOUSE_MOVE,INPUT_TYPE.MOUSE_UP];const TAP_TYPE_NAMES=[INPUT_TYPE.TAP,INPUT_TYPE.TAP_CANCEL,INPUT_TYPE.TAP_DOWN];const PINCH_TYPE_NAMES=[INPUT_TYPE.PINCH_BEGIN,INPUT_TYPE.PINCH_END,INPUT_TYPE.PINCH_UPDATE];const FLING_TYPE_NAMES=[INPUT_TYPE.FLING_CANCEL,INPUT_TYPE.FLING_START];const TOUCH_TYPE_NAMES=[INPUT_TYPE.TOUCH_END,INPUT_TYPE.TOUCH_MOVE,INPUT_TYPE.TOUCH_START];const SCROLL_TYPE_NAMES=[INPUT_TYPE.SCROLL_BEGIN,INPUT_TYPE.SCROLL_END,INPUT_TYPE.SCROLL_UPDATE];const ALL_HANDLED_TYPE_NAMES=[].concat(KEYBOARD_TYPE_NAMES,MOUSE_RESPONSE_TYPE_NAMES,MOUSE_WHEEL_TYPE_NAMES,MOUSE_DRAG_TYPE_NAMES,PINCH_TYPE_NAMES,TAP_TYPE_NAMES,FLING_TYPE_NAMES,TOUCH_TYPE_NAMES,SCROLL_TYPE_NAMES);const RENDERER_FLING_TITLE='InputHandlerProxy::HandleGestureFling::started';const PLAYBACK_EVENT_TITLE='VideoPlayback';const CSS_ANIMATION_TITLE='Animation';const VR_COUNTER_NAMES=['gpu.WebVR FPS','gpu.WebVR frame time (ms)','gpu.WebVR pose prediction (ms)','gpu.WebXR FPS',];const VR_EXPECTATION_EVENTS={'Vr.AcquireGvrFrame':{'histogramName':'acquire_frame','description':'Duration acquire a frame from GVR','hasCpuTime':true,},'Vr.DrawFrame':{'histogramName':'draw_frame','description':'Duration to render one frame','hasCpuTime':true,},'Vr.PostSubmitDrawOnGpu':{'histogramName':'post_submit_draw_on_gpu','description':'Duration to draw a frame on GPU post submit to '+'GVR. Note this duration may include time spent on '+'reprojection','hasCpuTime':false,},'Vr.ProcessControllerInput':{'histogramName':'update_controller','description':'Duration to query input from the controller','hasCpuTime':true,},'Vr.ProcessControllerInputForWebXr':{'histogramName':'update_controller_webxr','description':'Duration to query input from the controller for WebXR','hasCpuTime':true,},'Vr.SubmitFrameNow':{'histogramName':'submit_frame','description':'Duration to submit a frame to GVR','hasCpuTime':true,}};const WEBXR_INSTANT_EVENTS={'WebXR frame time (ms)':{'javascript':{'histogramName':'webxr_frame_time_javascript','description':'WebXR frame time spent on JavaScript',},'rendering':{'histogramName':'webxr_frame_time_rendering','description':'WebXR frame time spent on rendering'}},'WebXR pose prediction':{'milliseconds':{'histogramName':'webxr_pose_prediction','description':'WebXR pose prediction in ms',},},};const XR_DEVICE_SERVICE_PROCESS='Service: xr_device_service';function isXrDeviceServiceProcess(process){if(process.name===XR_DEVICE_SERVICE_PROCESS)return true;return false;}
+const VR_RESPONSE_MS=1000;const INPUT_MERGE_THRESHOLD_MS=200;const ANIMATION_MERGE_THRESHOLD_MS=32;const MOUSE_WHEEL_THRESHOLD_MS=40;const MOUSE_MOVE_THRESHOLD_MS=40;function compareEvents(x,y){if(x.start!==y.start){return x.start-y.start;}
 if(x.end!==y.end){return x.end-y.end;}
 if(x.guid&&y.guid){return x.guid-y.guid;}
 return 0;}
@@ -6311,9 +6465,10 @@
 function handleVrAnimations(modelHelper,sortedInputEvents,warn){const events=[];const processes=[];if(typeof modelHelper.gpuHelper!=='undefined'){processes.push(modelHelper.gpuHelper.process);}
 for(const helper of Object.values(modelHelper.rendererHelpers)){processes.push(helper.process);}
 for(const helper of Object.values(modelHelper.browserHelpers)){processes.push(helper.process);}
+for(const service of modelHelper.model.getAllProcesses(isXrDeviceServiceProcess)){processes.push(service);}
 let vrCounterStart=Number.MAX_SAFE_INTEGER;let vrEventStart=Number.MAX_SAFE_INTEGER;for(const proc of processes){for(const[counterName,counterSeries]of
 Object.entries(proc.counters)){if(VR_COUNTER_NAMES.includes(counterName)){for(const series of counterSeries.series){for(const sample of series.samples){events.push(sample);vrCounterStart=Math.min(vrCounterStart,sample.timestamp);}}}}
-for(const thread of Object.values(proc.threads)){for(const container of thread.childEventContainers()){for(const slice of container.slices){if(VR_EVENT_NAMES.includes(slice.title)){events.push(slice);vrEventStart=Math.min(vrEventStart,slice.start);}}}}}
+for(const thread of Object.values(proc.threads)){for(const container of thread.childEventContainers()){for(const slice of container.slices){if(slice.title in VR_EXPECTATION_EVENTS||slice.title in WEBXR_INSTANT_EVENTS){events.push(slice);vrEventStart=Math.min(vrEventStart,slice.start);}}}}}
 if(events.length===0){return[];}
 events.sort(function(x,y){if(x.range.min!==y.range.min){return x.range.min-y.range.min;}
 return x.guid-y.guid;});vrCounterStart=(vrCounterStart===Number.MAX_SAFE_INTEGER)?0:vrCounterStart;vrEventStart=(vrEventStart===Number.MAX_SAFE_INTEGER)?0:vrEventStart;const vrAnimationStart=Math.max(vrCounterStart,vrEventStart)+
@@ -6359,7 +6514,7 @@
 handledEvents.push(event);});});sortedInputEvents.forEach(function(event){if(handledEvents.indexOf(event)<0){warn({type:'UserModelBuilder',message:`double-handled event: ${event.typeName} @ ${event.start}`,showToUser:false,});}});}
 function findInputExpectations(modelHelper){let warning;function warn(w){if(warning)return;warning=w;}
 const sortedInputEvents=getSortedInputEvents(modelHelper);let protoExpectations=findProtoExpectations(modelHelper,sortedInputEvents,warn);protoExpectations=postProcessProtoExpectations(modelHelper,protoExpectations);checkAllInputEventsHandled(modelHelper,sortedInputEvents,protoExpectations,warn);if(warning)modelHelper.model.importWarning(warning);const expectations=[];protoExpectations.forEach(function(protoExpectation){const ir=protoExpectation.createInteractionRecord(modelHelper.model);if(ir){expectations.push(ir);}});return expectations;}
-return{findInputExpectations,compareEvents,CSS_ANIMATION_TITLE,};});'use strict';tr.exportTo('tr.b',function(){class FixedColorScheme{constructor(namesToColors){this.namesToColors_=namesToColors;}
+return{findInputExpectations,compareEvents,CSS_ANIMATION_TITLE,VR_EXPECTATION_EVENTS,WEBXR_INSTANT_EVENTS,};});'use strict';tr.exportTo('tr.b',function(){class FixedColorScheme{constructor(namesToColors){this.namesToColors_=namesToColors;}
 static fromNames(names){const namesToColors=new Map();const generator=new tr.b.SinebowColorGenerator();for(const name of names){namesToColors.set(name,generator.colorForKey(name));}
 return new FixedColorScheme(namesToColors);}
 getColor(name){const color=this.namesToColors_.get(name);if(color===undefined)throw new Error('Unknown color: '+name);return color;}}
@@ -6378,9 +6533,11 @@
 map.set(uniqueKey,value);}
 function skipDumpsThatDoNotIntersectRange(dumps,opt_range){if(!opt_range)return dumps;return dumps.filter(d=>opt_range.intersectsExplicitRangeInclusive(d.start,d.end));}
 function hasCategoryAndName(event,category,title){return event.title===title&&event.category&&tr.b.getCategoryParts(event.category).includes(category);}
-return{hasCategoryAndName,filterExpectationsByRange,perceptualBlend,splitGlobalDumpsByBrowserName};});'use strict';tr.exportTo('tr.e.chrome',function(){const CHROME_INTERNAL_URLS=['','about:blank','data:text/html,pluginplaceholderdata','chrome-error://chromewebdata/'];const SCHEDULER_TOP_LEVEL_TASK_TITLE='ThreadControllerImpl::RunTask';const SCHEDULOER_TOP_LEVEL_TASKS=new Set([SCHEDULER_TOP_LEVEL_TASK_TITLE,'ThreadControllerImpl::DoWork','TaskQueueManager::ProcessTaskFromWorkQueue']);class EventFinderUtils{static hasCategoryAndName(event,category,title){return event.title===title&&event.category&&tr.b.getCategoryParts(event.category).includes(category);}
+return{hasCategoryAndName,filterExpectationsByRange,perceptualBlend,splitGlobalDumpsByBrowserName};});'use strict';tr.exportTo('tr.e.chrome',function(){const CHROME_INTERNAL_URLS=['','about:blank','data:text/html,pluginplaceholderdata','chrome-error://chromewebdata/'];const SCHEDULER_TOP_LEVEL_TASK_TITLE='ThreadControllerImpl::RunTask';const SCHEDULER_TOP_LEVEL_TASKS=new Set([SCHEDULER_TOP_LEVEL_TASK_TITLE,'ThreadControllerImpl::DoWork','TaskQueueManager::ProcessTaskFromWorkQueue']);class EventFinderUtils{static hasCategoryAndName(event,category,title){return event.title===title&&event.category&&tr.b.getCategoryParts(event.category).includes(category);}
 static*getMainThreadEvents(rendererHelper,eventTitle,eventCategory){if(!rendererHelper.mainThread)return;for(const ev of rendererHelper.mainThread.sliceGroup.childEvents()){if(rendererHelper.isTelemetryInternalEvent(ev))continue;if(!this.hasCategoryAndName(ev,eventCategory,eventTitle)){continue;}
 yield ev;}}
+static getNetworkEventsInRange(process,range){const networkEvents=[];for(const thread of Object.values(process.threads)){const threadHelper=new tr.model.helpers.ChromeThreadHelper(thread);const events=threadHelper.getNetworkEvents();for(const event of events){if(range.intersectsExplicitRangeInclusive(event.start,event.end)){networkEvents.push(event);}}}
+return networkEvents;}
 static getSortedMainThreadEventsByFrame(rendererHelper,eventTitle,eventCategory){const eventsByFrame=new Map();const events=this.getMainThreadEvents(rendererHelper,eventTitle,eventCategory);for(const ev of events){const frameIdRef=ev.args.frame;if(frameIdRef===undefined)continue;if(!eventsByFrame.has(frameIdRef)){eventsByFrame.set(frameIdRef,[]);}
 eventsByFrame.get(frameIdRef).push(ev);}
 return eventsByFrame;}
@@ -6392,7 +6549,8 @@
 return sortedEvents[firstIndexOnOrAfterTimestamp];}
 static findNextEventStartingAfterTimestamp(sortedEvents,timestamp){const firstIndexOnOrAfterTimestamp=tr.b.findFirstTrueIndexInSortedArray(sortedEvents,e=>e.start>timestamp);if(firstIndexOnOrAfterTimestamp===sortedEvents.length){return undefined;}
 return sortedEvents[firstIndexOnOrAfterTimestamp];}
-static findToplevelSchedulerTasks(mainThread){const tasks=[];tasks.push(...mainThread.findTopmostSlices(slice=>slice.category==='toplevel'&&SCHEDULOER_TOP_LEVEL_TASKS.has(slice.title)));return tasks;}}
+static findToplevelSchedulerTasks(mainThread){const tasks=[];for(const task of mainThread.findTopmostSlices(slice=>slice.category==='toplevel'&&SCHEDULER_TOP_LEVEL_TASKS.has(slice.title))){tasks.push(task);}
+return tasks;}}
 return{EventFinderUtils,CHROME_INTERNAL_URLS,SCHEDULER_TOP_LEVEL_TASK_TITLE,};});'use strict';tr.exportTo('tr.e.chrome',function(){const TIME_TO_INTERACTIVE_WINDOW_SIZE_MS=5000;const ACTIVE_REQUEST_TOLERANCE=2;const FCI_MIN_CLUSTER_SEPARATION_MS=1000;const TASK_CLUSTER_HEAVINESS_THRESHOLD_MS=250;const ENDPOINT_TYPES={LONG_TASK_START:'LONG_TASK_START',LONG_TASK_END:'LONG_TASK_END',REQUEST_START:'REQUEST_START',REQUEST_END:'REQUEST_END'};function getEndpoints_(events,startType,endType){const endpoints=[];for(const event of events){endpoints.push({time:event.start,type:startType});endpoints.push({time:event.end,type:endType});}
 return endpoints;}
 function reachedTTIQuiscence_(timestamp,networkQuietWindowStart,mainThreadQuietWindowStart){if(networkQuietWindowStart===undefined||mainThreadQuietWindowStart===undefined){return false;}
@@ -6422,16 +6580,14 @@
 return{findInteractiveTime,findFirstCpuIdleTime,requiredFCIWindowSizeMs,findFCITaskClusters,};});'use strict';tr.exportTo('tr.model.um',function(){const LOAD_SUBTYPE_NAMES={SUCCESSFUL:'Successful',FAILED:'Failed',};const DOES_LOAD_SUBTYPE_NAME_EXIST={};for(const key in LOAD_SUBTYPE_NAMES){DOES_LOAD_SUBTYPE_NAME_EXIST[LOAD_SUBTYPE_NAMES[key]]=true;}
 function LoadExpectation(parentModel,initiatorTitle,start,duration,renderer,navigationStart,fmpEvent,dclEndEvent,cpuIdleTime,timeToInteractive,url,frameId){if(!DOES_LOAD_SUBTYPE_NAME_EXIST[initiatorTitle]){throw new Error(initiatorTitle+' is not in LOAD_SUBTYPE_NAMES');}
 tr.model.um.UserExpectation.call(this,parentModel,initiatorTitle,start,duration);this.renderProcess=renderer;this.renderMainThread=undefined;this.routingId=undefined;this.parentRoutingId=undefined;this.loadFinishedEvent=undefined;this.navigationStart=navigationStart;this.fmpEvent=fmpEvent;this.domContentLoadedEndEvent=dclEndEvent;this.firstCpuIdleTime=cpuIdleTime;this.timeToInteractive=timeToInteractive;this.url=url;this.frameId=frameId;}
-LoadExpectation.prototype={__proto__:tr.model.um.UserExpectation.prototype,constructor:LoadExpectation};tr.model.um.UserExpectation.subTypes.register(LoadExpectation,{stageTitle:'Load',colorId:tr.b.ColorScheme.getColorIdForReservedName('rail_load')});return{LOAD_SUBTYPE_NAMES,LoadExpectation,};});'use strict';tr.exportTo('tr.importer',function(){const LONG_TASK_THRESHOLD_MS=50;const IGNORE_URLS=['','about:blank',];function getNetworkEventsInRange(process,range){const networkEvents=[];for(const thread of Object.values(process.threads)){const threadHelper=new tr.model.helpers.ChromeThreadHelper(thread);const events=threadHelper.getNetworkEvents();for(const event of events){if(range.intersectsExplicitRangeInclusive(event.start,event.end)){networkEvents.push(event);}}}
-return networkEvents;}
-function findFrameLoaderSnapshotAt(rendererHelper,frameIdRef,ts){const objects=rendererHelper.process.objects;const frameLoaderInstances=objects.instancesByTypeName_.FrameLoader;if(frameLoaderInstances===undefined)return undefined;let snapshot;for(const instance of frameLoaderInstances){if(!instance.isAliveAt(ts))continue;const maybeSnapshot=instance.getSnapshotAt(ts);if(frameIdRef!==maybeSnapshot.args.frame.id_ref)continue;snapshot=maybeSnapshot;}
+LoadExpectation.prototype={__proto__:tr.model.um.UserExpectation.prototype,constructor:LoadExpectation};tr.model.um.UserExpectation.subTypes.register(LoadExpectation,{stageTitle:'Load',colorId:tr.b.ColorScheme.getColorIdForReservedName('rail_load')});return{LOAD_SUBTYPE_NAMES,LoadExpectation,};});'use strict';tr.exportTo('tr.importer',function(){const LONG_TASK_THRESHOLD_MS=50;const IGNORE_URLS=['','about:blank',];function findFrameLoaderSnapshotAt(rendererHelper,frameIdRef,ts){const objects=rendererHelper.process.objects;const frameLoaderInstances=objects.instancesByTypeName_.FrameLoader;if(frameLoaderInstances===undefined)return undefined;let snapshot;for(const instance of frameLoaderInstances){if(!instance.isAliveAt(ts))continue;const maybeSnapshot=instance.getSnapshotAt(ts);if(frameIdRef!==maybeSnapshot.args.frame.id_ref)continue;snapshot=maybeSnapshot;}
 return snapshot;}
 function findFirstMeaningfulPaintCandidates(rendererHelper){const candidatesForFrameId={};for(const ev of rendererHelper.process.getDescendantEvents()){if(!tr.e.chrome.EventFinderUtils.hasCategoryAndName(ev,'loading','firstMeaningfulPaintCandidate')){continue;}
 if(rendererHelper.isTelemetryInternalEvent(ev))continue;const frameIdRef=ev.args.frame;if(frameIdRef===undefined)continue;let list=candidatesForFrameId[frameIdRef];if(list===undefined){candidatesForFrameId[frameIdRef]=list=[];}
 list.push(ev);}
 return candidatesForFrameId;}
 function computeInteractivityMetricSample_(rendererHelper,navigationStart,fmpEvent,domContentLoadedEndEvent,searchWindowEnd){if(domContentLoadedEndEvent===undefined||fmpEvent===undefined){return{interactiveTime:undefined,firstCpuIdleTime:undefined};}
-const firstMeaningfulPaintTime=fmpEvent.start;const mainThreadTasks=tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks(rendererHelper.mainThread);const longTasks=mainThreadTasks.filter(task=>task.duration>=LONG_TASK_THRESHOLD_MS);const longTasksInWindow=longTasks.filter(task=>task.range.intersectsExplicitRangeInclusive(firstMeaningfulPaintTime,searchWindowEnd));const resourceLoadEvents=getNetworkEventsInRange(rendererHelper.process,tr.b.math.Range.fromExplicitRange(navigationStart.start,searchWindowEnd));const firstCpuIdleTime=tr.e.chrome.findFirstCpuIdleTime(firstMeaningfulPaintTime,searchWindowEnd,domContentLoadedEndEvent.start,longTasksInWindow);const interactiveTime=resourceLoadEvents.length>0?tr.e.chrome.findInteractiveTime(firstMeaningfulPaintTime,searchWindowEnd,domContentLoadedEndEvent.start,longTasksInWindow,resourceLoadEvents):undefined;return{interactiveTime,firstCpuIdleTime};}
+const firstMeaningfulPaintTime=fmpEvent.start;const mainThreadTasks=tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks(rendererHelper.mainThread);const longTasks=mainThreadTasks.filter(task=>task.duration>=LONG_TASK_THRESHOLD_MS);const longTasksInWindow=longTasks.filter(task=>task.range.intersectsExplicitRangeInclusive(firstMeaningfulPaintTime,searchWindowEnd));const resourceLoadEvents=tr.e.chrome.EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,tr.b.math.Range.fromExplicitRange(navigationStart.start,searchWindowEnd));const firstCpuIdleTime=tr.e.chrome.findFirstCpuIdleTime(firstMeaningfulPaintTime,searchWindowEnd,domContentLoadedEndEvent.start,longTasksInWindow);const interactiveTime=resourceLoadEvents.length>0?tr.e.chrome.findInteractiveTime(firstMeaningfulPaintTime,searchWindowEnd,domContentLoadedEndEvent.start,longTasksInWindow,resourceLoadEvents):undefined;return{interactiveTime,firstCpuIdleTime};}
 function constructLoadingExpectation_(rendererHelper,frameToDomContentLoadedEndEvents,navigationStart,fmpEvent,searchWindowEnd,url,frameId){const dclTimesForFrame=frameToDomContentLoadedEndEvents.get(frameId)||[];const dclSearchRange=tr.b.math.Range.fromExplicitRange(navigationStart.start,searchWindowEnd);const dclTimesInWindow=dclSearchRange.filterArray(dclTimesForFrame,event=>event.start);let domContentLoadedEndEvent=undefined;if(dclTimesInWindow.length!==0){domContentLoadedEndEvent=dclTimesInWindow[dclTimesInWindow.length-1];}
 const{interactiveTime,firstCpuIdleTime}=computeInteractivityMetricSample_(rendererHelper,navigationStart,fmpEvent,domContentLoadedEndEvent,searchWindowEnd);const duration=(interactiveTime===undefined)?searchWindowEnd-navigationStart.start:interactiveTime-navigationStart.start;return new tr.model.um.LoadExpectation(rendererHelper.modelHelper.model,tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL,navigationStart.start,duration,rendererHelper.process,navigationStart,fmpEvent,domContentLoadedEndEvent,firstCpuIdleTime,interactiveTime,url,frameId);}
 function collectLoadExpectationsForRenderer(rendererHelper){const samples=[];const frameToNavStartEvents=tr.e.chrome.EventFinderUtils.getSortedMainThreadEventsByFrame(rendererHelper,'navigationStart','blink.user_timing');const frameToDomContentLoadedEndEvents=tr.e.chrome.EventFinderUtils.getSortedMainThreadEventsByFrame(rendererHelper,'domContentLoadedEventEnd','blink.user_timing');function addSamples(frameIdRef,navigationStart,fmpCandidateEvents,searchWindowEnd,url){let fmpMarkerEvent=tr.e.chrome.EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp(fmpCandidateEvents,searchWindowEnd);if(fmpMarkerEvent!==undefined&&navigationStart.start>fmpMarkerEvent.start){fmpMarkerEvent=undefined;}
@@ -6494,7 +6650,7 @@
 return{decorate,define,elementIsChildOf,};});'use strict';tr.exportTo('tr.b.math',function(){function Rect(){this.x=0;this.y=0;this.width=0;this.height=0;}
 Rect.fromXYWH=function(x,y,w,h){const rect=new Rect();rect.x=x;rect.y=y;rect.width=w;rect.height=h;return rect;};Rect.fromArray=function(ary){if(ary.length!==4){throw new Error('ary.length must be 4');}
 const rect=new Rect();rect.x=ary[0];rect.y=ary[1];rect.width=ary[2];rect.height=ary[3];return rect;};Rect.prototype={__proto__:Object.prototype,get left(){return this.x;},get top(){return this.y;},get right(){return this.x+this.width;},get bottom(){return this.y+this.height;},toString(){return'Rect('+this.x+', '+this.y+', '+
-this.width+', '+this.height+')';},toArray(){return[this.x,this.y,this.width,this.height];},clone(){const rect=new Rect();rect.x=this.x;rect.y=this.y;rect.width=this.width;rect.height=this.height;return rect;},enlarge(pad){const rect=new Rect();this.enlargeFast(rect,pad);return rect;},enlargeFast(out,pad){out.x=this.x-pad;out.y=this.y-pad;out.width=this.width+2*pad;out.height=this.height+2*pad;return out;},size(){return{width:this.width,height:this.height};},scale(s){const rect=new Rect();this.scaleFast(rect,s);return rect;},scaleSize(s){return Rect.fromXYWH(this.x,this.y,this.width*s,this.height*s);},scaleFast(out,s){out.x=this.x*s;out.y=this.y*s;out.width=this.width*s;out.height=this.height*s;return out;},translate(v){const rect=new Rect();this.translateFast(rect,v);return rect;},translateFast(out,v){out.x=this.x+v[0];out.y=this.x+v[1];out.width=this.width;out.height=this.height;return out;},asUVRectInside(containingRect){const rect=new Rect();rect.x=(this.x-containingRect.x)/containingRect.width;rect.y=(this.y-containingRect.y)/containingRect.height;rect.width=this.width/containingRect.width;rect.height=this.height/containingRect.height;return rect;},intersects(that){let ok=true;ok&=this.x<that.right;ok&=this.right>that.x;ok&=this.y<that.bottom;ok&=this.bottom>that.y;return ok;},equalTo(rect){return rect&&(this.x===rect.x)&&(this.y===rect.y)&&(this.width===rect.width)&&(this.height===rect.height);}};return{Rect,};});'use strict';tr.exportTo('tr.ui.b',function(){function instantiateTemplate(selector,doc){doc=doc||document;const el=Polymer.dom(doc).querySelector(selector);if(!el){throw new Error('Element not found');}
+this.width+', '+this.height+')';},toArray(){return[this.x,this.y,this.width,this.height];},clone(){const rect=new Rect();rect.x=this.x;rect.y=this.y;rect.width=this.width;rect.height=this.height;return rect;},enlarge(pad){const rect=new Rect();this.enlargeFast(rect,pad);return rect;},enlargeFast(out,pad){out.x=this.x-pad;out.y=this.y-pad;out.width=this.width+2*pad;out.height=this.height+2*pad;return out;},size(){return{width:this.width,height:this.height};},scale(s){const rect=new Rect();this.scaleFast(rect,s);return rect;},scaleSize(s){return Rect.fromXYWH(this.x,this.y,this.width*s,this.height*s);},scaleFast(out,s){out.x=this.x*s;out.y=this.y*s;out.width=this.width*s;out.height=this.height*s;return out;},translate(v){const rect=new Rect();this.translateFast(rect,v);return rect;},translateFast(out,v){out.x=this.x+v[0];out.y=this.x+v[1];out.width=this.width;out.height=this.height;return out;},asUVRectInside(containingRect){const rect=new Rect();rect.x=(this.x-containingRect.x)/containingRect.width;rect.y=(this.y-containingRect.y)/containingRect.height;rect.width=this.width/containingRect.width;rect.height=this.height/containingRect.height;return rect;},intersects(that){let ok=true;ok&=this.x<that.right;ok&=this.right>that.x;ok&=this.y<that.bottom;ok&=this.bottom>that.y;return ok;},equalTo(rect){return rect&&(this.x===rect.x)&&(this.y===rect.y)&&(this.width===rect.width)&&(this.height===rect.height);}};return{Rect,};});'use strict';tr.exportTo('tr.ui.b',function(){function instantiateTemplate(selector,doc){doc=doc||document;const el=Polymer.dom(doc).querySelector(selector);if(!el){throw new Error('Element not found: '+selector);}
 return doc.importNode(el.content,true);}
 function windowRectForElement(element){const position=[element.offsetLeft,element.offsetTop];const size=[element.offsetWidth,element.offsetHeight];let node=element.offsetParent;while(node){position[0]+=node.offsetLeft;position[1]+=node.offsetTop;node=node.offsetParent;}
 return tr.b.math.Rect.fromXYWH(position[0],position[1],size[0],size[1]);}
@@ -6580,7 +6736,7 @@
 return true;};PictureSnapshot.CanGetInfo=function(){if(!PictureSnapshot.HasSkiaBenchmarking()){return false;}
 if(!window.chrome.skiaBenchmarking.getInfo){return false;}
 return true;};PictureSnapshot.HowToEnablePictureDebugging=function(){if(tr.isHeadless){return'Pictures only work in chrome';}
-const usualReason=['For pictures to show up, you need to have Chrome running with ','--enable-skia-benchmarking. Please restart chrome with this flag ','and try again.'].join('');if(!PictureSnapshot.HasSkiaBenchmarking()){return usualReason;}
+const usualReason=['For pictures to show up, the Chrome browser displaying the trace ','needs to be running with --enable-skia-benchmarking. Please restart ','chrome with this flag and try loading the trace again.'].join('');if(!PictureSnapshot.HasSkiaBenchmarking()){return usualReason;}
 if(!PictureSnapshot.CanRasterize()){return'Your chrome is old: chrome.skipBenchmarking.rasterize not found';}
 if(!PictureSnapshot.CanGetOps()){return'Your chrome is old: chrome.skiaBenchmarking.getOps not found';}
 if(!PictureSnapshot.CanGetOpTimings()){return'Your chrome is old: '+'chrome.skiaBenchmarking.getOpTimings not found';}
@@ -6670,16 +6826,13 @@
 if(!(analysisViewRelatedEvents instanceof EventSet)){analysisViewRelatedEvents=new EventSet();}
 this.analysisViewRelatedEvents_=analysisViewRelatedEvents;},get analysisLinkHoveredEvents(){return this.analysisLinkHoveredEvents_;},set analysisLinkHoveredEvents(analysisLinkHoveredEvents){if(this.appliedToModel_){throw new Error('Cannot mutate this state right now');}
 if(!(analysisLinkHoveredEvents instanceof EventSet)){analysisLinkHoveredEvents=new EventSet();}
-this.analysisLinkHoveredEvents_=analysisLinkHoveredEvents;},get isAppliedToModel(){return this.appliedToModel_!==undefined;},get viewSpecificBrushingStates(){return this.viewSpecificBrushingStates_;},set viewSpecificBrushingStates(viewSpecificBrushingStates){this.viewSpecificBrushingStates_=viewSpecificBrushingStates;},get dimmedEvents_(){const dimmedEvents=new EventSet();dimmedEvents.addEventSet(this.findMatches);dimmedEvents.addEventSet(this.analysisViewRelatedEvents_);return dimmedEvents;},get brightenedEvents_(){const brightenedEvents=new EventSet();brightenedEvents.addEventSet(this.selection_);brightenedEvents.addEventSet(this.analysisLinkHoveredEvents_);return brightenedEvents;},applyToEventSelectionStates(model){this.appliedToModel_=model;const dimmedEvents=this.dimmedEvents_;if(model){const newDefaultState=(dimmedEvents.length?SelectionState.DIMMED0:SelectionState.NONE);const currentDefaultState=tr.b.getFirstElement(model.getDescendantEvents()).selectionState;if(currentDefaultState!==newDefaultState){for(const e of model.getDescendantEvents()){e.selectionState=newDefaultState;}}}
-let score;for(const e of dimmedEvents){score=0;if(this.findMatches_.contains(e)){score++;}
-if(this.analysisViewRelatedEvents_.contains(e)){score++;}
-e.selectionState=SelectionState.getFromDimmingLevel(score);}
-for(const e of this.brightenedEvents_){score=0;if(this.selection_.contains(e)){score++;}
-if(this.analysisLinkHoveredEvents_.contains(e)){score++;}
-e.selectionState=SelectionState.getFromBrighteningLevel(score);}},transferModelOwnershipToClone(that){if(!this.appliedToModel_){throw new Error('Not applied');}
+this.analysisLinkHoveredEvents_=analysisLinkHoveredEvents;},get isAppliedToModel(){return this.appliedToModel_!==undefined;},get viewSpecificBrushingStates(){return this.viewSpecificBrushingStates_;},set viewSpecificBrushingStates(viewSpecificBrushingStates){this.viewSpecificBrushingStates_=viewSpecificBrushingStates;},get defaultState_(){const standoutEventExists=(this.analysisLinkHoveredEvents_.length>0||this.analysisViewRelatedEvents_.length>0||this.findMatches_.length>0);return(standoutEventExists?SelectionState.DIMMED0:SelectionState.NONE);},get brightenedEvents_(){const brightenedEvents=new EventSet();brightenedEvents.addEventSet(this.findMatches);brightenedEvents.addEventSet(this.analysisViewRelatedEvents_);brightenedEvents.addEventSet(this.selection_);brightenedEvents.addEventSet(this.analysisLinkHoveredEvents_);return brightenedEvents;},applyToEventSelectionStates(model){this.appliedToModel_=model;if(model){const newDefaultState=this.defaultState_;const currentDefaultState=tr.b.getFirstElement(model.getDescendantEvents()).selectionState;if(currentDefaultState!==newDefaultState){for(const e of model.getDescendantEvents()){e.selectionState=newDefaultState;}}}
+let level;for(const e of this.brightenedEvents_){level=0;if(this.analysisViewRelatedEvents_.contains(e)||this.findMatches_.contains(e)){level++;}
+if(this.analysisLinkHoveredEvents_.contains(e)){level++;}
+if(this.selection_.contains(e)){level++;}
+e.selectionState=SelectionState.getFromBrighteningLevel(level);}},transferModelOwnershipToClone(that){if(!this.appliedToModel_){throw new Error('Not applied');}
 that.appliedToModel_=this.appliedToModel_;this.appliedToModel_=undefined;},unapplyFromEventSelectionStates(){if(!this.appliedToModel_){throw new Error('Not applied');}
-const model=this.appliedToModel_;this.appliedToModel_=undefined;const dimmedEvents=this.dimmedEvents_;const defaultState=(dimmedEvents.length?SelectionState.DIMMED0:SelectionState.NONE);for(const e of this.brightenedEvents_){e.selectionState=defaultState;}
-for(const e of dimmedEvents){e.selectionState=defaultState;}
+const model=this.appliedToModel_;this.appliedToModel_=undefined;const defaultState=this.defaultState_;for(const e of this.brightenedEvents_){e.selectionState=defaultState;}
 return defaultState;}};return{BrushingState,};});'use strict';tr.exportTo('tr.ui.b',function(){function Animation(){}
 Animation.prototype={canTakeOverFor(existingAnimation){throw new Error('Not implemented');},takeOverFor(existingAnimation,newStartTimestamp,target){throw new Error('Not implemented');},start(timestamp,target){throw new Error('Not implemented');},didStopEarly(timestamp,target,willBeTakenOverByAnotherAnimation){},tick(timestamp,target){throw new Error('Not implemented');}};return{Animation,};});'use strict';tr.exportTo('tr.ui.b',function(){function AnimationController(){tr.b.EventTarget.call(this);this.target_=undefined;this.activeAnimation_=undefined;this.tickScheduled_=false;}
 AnimationController.prototype={__proto__:tr.b.EventTarget.prototype,get target(){return this.target_;},set target(target){if(this.activeAnimation_){throw new Error('Cannot change target while animation is running.');}
@@ -6967,6 +7120,9 @@
 (this.count*other.count*deltaMean*deltaMean/result.count);if(this.meanlogs_===undefined||other.meanlogs_===undefined){result.meanlogs_=undefined;}else{result.meanlogs_=(this.count*this.meanlogs_+
 other.count*other.meanlogs_)/result.count;}}
 return result;}
+truncate(unit){this.max_=unit.truncate(this.max_);if(this.meanlogs_!==undefined){const formatted=unit.format(this.geometricMean);let lo=1;let hi=16;while(lo<hi-1){const digits=parseInt((lo+hi)/2);const test=tr.b.math.truncate(this.meanlogs_,digits);if(formatted===unit.format(Math.exp(test))){hi=digits;}else{lo=digits;}}
+const test=tr.b.math.truncate(this.meanlogs_,lo);if(formatted===unit.format(Math.exp(test))){this.meanlogs_=test;}else{this.meanlogs_=tr.b.math.truncate(this.meanlogs_,hi);}}
+this.mean_=unit.truncate(this.mean_);this.min_=unit.truncate(this.min_);this.sum_=unit.truncate(this.sum_);this.variance_=unit.truncate(this.variance_);}
 asDict(){if(!this.count){return[];}
 return[this.count_,this.max_,this.meanlogs_,this.mean_,this.min_,this.sum_,this.variance_,];}
 static fromDict(dict){const result=new RunningStatistics();if(dict.length!==7){return result;}
@@ -6986,9 +7142,16 @@
 this.asDictInto_(result);return result;}
 asDictInto_(d){throw new Error('Abstract virtual method: subclasses must override '+'this method if they override canAddDiagnostic');}
 static fromDict(d){const typeInfo=Diagnostic.findTypeInfoWithName(d.type);if(!typeInfo){throw new Error('Unrecognized diagnostic type: '+d.type);}
-const diagnostic=typeInfo.constructor.fromDict(d);if(d.guid!==undefined)diagnostic.guid=d.guid;return diagnostic;}}
-const options=new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);options.defaultMetadata={};options.mandatoryBaseClass=Diagnostic;tr.b.decorateExtensionRegistry(Diagnostic,options);Diagnostic.addEventListener('will-register',function(e){const constructor=e.typeInfo.constructor;if(!(constructor.fromDict instanceof Function)||(constructor.fromDict===Diagnostic.fromDict)||(constructor.fromDict.length!==1)){throw new Error('Diagnostics must define fromDict(d)');}});return{Diagnostic,};});'use strict';tr.exportTo('tr.v.d',function(){class Breakdown extends tr.v.d.Diagnostic{constructor(){super();this.values_=new Map();this.colorScheme=undefined;}
+const diagnostic=typeInfo.constructor.fromDict(d);if(d.guid!==undefined)diagnostic.guid=d.guid;return diagnostic;}
+static deserialize(type,d,deserializer){const typeInfo=Diagnostic.findTypeInfoWithName(type);if(!typeInfo){throw new Error('Unrecognized diagnostic type: '+type);}
+return typeInfo.constructor.deserialize(d,deserializer);}}
+const options=new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);options.defaultMetadata={};options.mandatoryBaseClass=Diagnostic;tr.b.decorateExtensionRegistry(Diagnostic,options);Diagnostic.addEventListener('will-register',function(e){const constructor=e.typeInfo.constructor;if(!(constructor.deserialize instanceof Function)||(constructor.deserialize===Diagnostic.deserialize)||(constructor.deserialize.length!==2)){throw new Error(`Please define ${constructor.name}.deserialize(data, deserializer)`);}
+if(!(constructor.fromDict instanceof Function)||(constructor.fromDict===Diagnostic.fromDict)||(constructor.fromDict.length!==1)){throw new Error(`Please define ${constructor.name}.fromDict(d)`);}
+if(!(constructor.prototype.serialize instanceof Function)||(constructor.prototype.serialize===Diagnostic.prototype.serialize)||(constructor.prototype.serialize.length!==1)){throw new Error(`Please define ${constructor.name}.serialize(serializer)`);}});return{Diagnostic,};});'use strict';tr.exportTo('tr.v.d',function(){class Breakdown extends tr.v.d.Diagnostic{constructor(){super();this.values_=new Map();this.colorScheme='';}
+truncate(unit){for(const[name,value]of this){this.values_.set(name,unit.truncate(value));}}
 clone(){const clone=new Breakdown();clone.colorScheme=this.colorScheme;clone.addDiagnostic(this);return clone;}
+equals(other){if(this.colorScheme!==other.colorScheme)return false;if(this.values_.size!==other.values_.size)return false;for(const[k,v]of this){if(v!==other.get(k))return false;}
+return true;}
 canAddDiagnostic(otherDiagnostic){return((otherDiagnostic instanceof Breakdown)&&(otherDiagnostic.colorScheme===this.colorScheme));}
 addDiagnostic(otherDiagnostic){for(const[name,value]of otherDiagnostic){this.set(name,this.get(name)+value);}
 return this;}
@@ -6996,15 +7159,20 @@
 this.values_.set(name,value);}
 get(name){return this.values_.get(name)||0;}*[Symbol.iterator](){for(const pair of this.values_){yield pair;}}
 get size(){return this.values_.size;}
+serialize(serializer){const keys=[...this.values_.keys()];keys.sort();return[serializer.getOrAllocateId(this.colorScheme),serializer.getOrAllocateId(keys.map(k=>serializer.getOrAllocateId(k))),...keys.map(k=>this.get(k)),];}
 asDictInto_(d){d.values={};for(const[name,value]of this){d.values[name]=tr.b.numberToJson(value);}
 if(this.colorScheme){d.colorScheme=this.colorScheme;}}
 static fromEntries(entries){const breakdown=new Breakdown();for(const[name,value]of entries){breakdown.set(name,value);}
 return breakdown;}
+static deserialize(data,deserializer){const breakdown=new Breakdown();breakdown.colorScheme=deserializer.getObject(data[0]);const keys=deserializer.getObject(data[1]);for(let i=0;i<keys.length;++i){breakdown.set(deserializer.getObject(keys[i]),tr.b.numberFromJson(data[i+2]));}
+return breakdown;}
 static fromDict(d){const breakdown=new Breakdown();for(const[name,value]of Object.entries(d.values)){breakdown.set(name,tr.b.numberFromJson(value));}
 if(d.colorScheme){breakdown.colorScheme=d.colorScheme;}
 return breakdown;}}
 tr.v.d.Diagnostic.register(Breakdown,{elementName:'tr-v-ui-breakdown-span'});return{Breakdown,};});'use strict';tr.exportTo('tr.v.d',function(){class CollectedRelatedEventSet extends tr.v.d.Diagnostic{constructor(){super();this.eventSetsByCanonicalUrl_=new Map();}
 asDictInto_(d){d.events={};for(const[canonicalUrl,eventSet]of this){d.events[canonicalUrl]=[];for(const event of eventSet){d.events[canonicalUrl].push({stableId:event.stableId,title:event.title,start:event.start,duration:event.duration});}}}
+static deserialize(events,deserializer){return CollectedRelatedEventSet.fromDict({events});}
+serialize(serializer){const d={};this.asDictInto(d);return d.events;}
 static fromDict(d){const result=new CollectedRelatedEventSet();for(const[canonicalUrl,events]of Object.entries(d.events)){result.eventSetsByCanonicalUrl_.set(canonicalUrl,events.map(e=>new tr.v.d.EventRef(e)));}
 return result;}
 get size(){return this.eventSetsByCanonicalUrl_.size;}
@@ -7016,6 +7184,8 @@
 return;}
 if(!otherDiagnostic.canonicalUrl)return;this.addEventSetForCanonicalUrl(otherDiagnostic.canonicalUrl,otherDiagnostic);}}
 tr.v.d.Diagnostic.register(CollectedRelatedEventSet,{elementName:'tr-v-ui-collected-related-event-set-span'});return{CollectedRelatedEventSet,};});'use strict';tr.exportTo('tr.v.d',function(){class DateRange extends tr.v.d.Diagnostic{constructor(ms){super();this.range_=new tr.b.math.Range();this.range_.addValue(ms);}
+get minTimestamp(){return this.range_.min;}
+get maxTimestamp(){return this.range_.max;}
 get minDate(){return new Date(this.range_.min);}
 get maxDate(){return new Date(this.range_.max);}
 get durationMs(){return this.range_.duration;}
@@ -7024,7 +7194,10 @@
 canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof DateRange;}
 addDiagnostic(other){this.range_.addRange(other.range_);}
 toString(){const minDate=tr.b.formatDate(this.minDate);if(this.durationMs===0)return minDate;const maxDate=tr.b.formatDate(this.maxDate);return`${minDate} - ${maxDate}`;}
+serialize(serializer){if(this.durationMs===0)return this.range_.min;return[this.range_.min,this.range_.max];}
 asDictInto_(d){d.min=this.range_.min;if(this.durationMs===0)return;d.max=this.range_.max;}
+static deserialize(data,deserializer){if(data instanceof Array){const dr=new DateRange(data[0]);dr.range_.addValue(data[1]);return dr;}
+return new DateRange(data);}
 static fromDict(d){const dateRange=new DateRange(d.min);if(d.max!==undefined)dateRange.range_.addValue(d.max);return dateRange;}}
 tr.v.d.Diagnostic.register(DateRange,{elementName:'tr-v-ui-date-range-span'});return{DateRange,};});'use strict';tr.exportTo('tr.v.d',function(){class DiagnosticRef{constructor(guid){this.guid=guid;}
 asDict(){return this.guid;}
@@ -7042,7 +7215,10 @@
 get hashKey(){if(this.has_objects_)return undefined;if(this.hash_key_!==undefined){return this.hash_key_;}
 let key='';for(const value of Array.from(this.values_.values()).sort()){key+=value;}
 this.hash_key_=key;return key;}
+serialize(serializer){const i=[...this].map(x=>serializer.getOrAllocateId(x));return(i.length===1)?i[0]:i;}
 asDictInto_(d){d.values=Array.from(this);}
+static deserialize(data,deserializer){if(!(data instanceof Array)){data=[data];}
+return new GenericSet(data.map(datum=>deserializer.getObject(datum)));}
 static fromDict(d){return new GenericSet(d.values);}
 clone(){return new GenericSet(this.values_);}
 canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof GenericSet;}
@@ -7050,98 +7226,55 @@
 for(const value of otherDiagnostic){if(typeof value==='object'){if(jsons.has(stableStringify(value))){continue;}
 this.has_objects_=true;}
 this.values_.add(value);}}}
-tr.v.d.Diagnostic.register(GenericSet,{elementName:'tr-v-ui-generic-set-span'});return{GenericSet,};});'use strict';tr.exportTo('tr.v.d',function(){class GroupingPath extends tr.v.d.Diagnostic{constructor(groupingPath){super();this.groupingPath_=groupingPath;}
-clone(){return new GroupingPath(Array.from(this.groupingPath_));}
-addToHistogram(hist){hist.diagnostics.set(tr.v.d.RESERVED_NAMES.GROUPING_PATH,this);}
-static getFromHistogram(hist){return hist.diagnostics.get(tr.v.d.RESERVED_NAMES.GROUPING_PATH);}
-equals(other){return 0===tr.b.compareArrays(this.groupingPath_,other.groupingPath_,(x,y)=>x.localeCompare(y));}
-asDictInto_(d){d.groupingPath=this.groupingPath_;}
-static fromDict(d){return new GroupingPath(d.groupingPath);}}
-tr.v.d.Diagnostic.register(GroupingPath);return{GroupingPath,};});'use strict';tr.exportTo('tr.v.d',function(){class EventRef{constructor(event){this.stableId=event.stableId;this.title=event.title;this.start=event.start;this.duration=event.duration;this.end=this.start+this.duration;this.guid=tr.b.GUID.allocateSimple();}}
+tr.v.d.Diagnostic.register(GenericSet,{elementName:'tr-v-ui-generic-set-span'});return{GenericSet,};});'use strict';tr.exportTo('tr.v.d',function(){class EventRef{constructor(event){this.stableId=event.stableId;this.title=event.title;this.start=event.start;this.duration=event.duration;this.end=this.start+this.duration;this.guid=tr.b.GUID.allocateSimple();}}
 return{EventRef,};});'use strict';tr.exportTo('tr.v.d',function(){class RelatedEventSet extends tr.v.d.Diagnostic{constructor(opt_events){super();this.eventsByStableId_=new Map();this.canonicalUrl_=undefined;if(opt_events){if(opt_events instanceof tr.model.EventSet||opt_events instanceof Array){for(const event of opt_events){this.add(event);}}else{this.add(opt_events);}}}
 clone(){const clone=new tr.v.d.CollectedRelatedEventSet();clone.addDiagnostic(this);return clone;}
+equals(other){if(this.length!==other.length)return false;for(const event of this){if(!other.has(event))return false;}
+return true;}
 add(event){this.eventsByStableId_.set(event.stableId,event);}
 has(event){return this.eventsByStableId_.has(event.stableId);}
 get length(){return this.eventsByStableId_.size;}*[Symbol.iterator](){for(const event of this.eventsByStableId_.values()){yield event;}}
 get canonicalUrl(){return this.canonicalUrl_;}
 resolve(model,opt_required){for(const[stableId,value]of this.eventsByStableId_){if(!(value instanceof tr.v.d.EventRef))continue;const event=model.getEventByStableId(stableId);if(event instanceof tr.model.Event){this.eventsByStableId_.set(stableId,event);}else if(opt_required){throw new Error('Unable to find Event '+stableId);}}}
-asDictInto_(d){d.events=[];for(const event of this){d.events.push({stableId:event.stableId,title:event.title,start:event.start,duration:event.duration});}}
+serialize(serializer){return[...this].map(event=>[event.stableId,serializer.getOrAllocateId(event.title),event.start,event.duration,]);}
+asDictInto_(d){d.events=[];for(const event of this){d.events.push({stableId:event.stableId,title:event.title,start:tr.b.Unit.byName.timeStampInMs.truncate(event.start),duration:tr.b.Unit.byName.timeDurationInMs.truncate(event.duration),});}}
+static deserialize(data,deserializer){return new RelatedEventSet(data.map(event=>new tr.v.d.EventRef({stableId:event[0],title:deserializer.getObject(event[1]),start:event[2],duration:event[3],})));}
 static fromDict(d){return new RelatedEventSet(d.events.map(event=>new tr.v.d.EventRef(event)));}}
-tr.v.d.Diagnostic.register(RelatedEventSet,{elementName:'tr-v-ui-related-event-set-span'});return{RelatedEventSet,};});'use strict';tr.exportTo('tr.v.d',function(){function HistogramRef(guid){this.guid=guid;}
-return{HistogramRef};});'use strict';tr.exportTo('tr.v.d',function(){class RelatedHistogramMap extends tr.v.d.Diagnostic{constructor(){super();this.histogramsByName_=new Map();}
-canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof RelatedHistogramMap;}
-addDiagnostic(otherDiagnostic){}
-mergeRelationships(otherDiagnostic,parentHist,otherParentHist){const parentGroupingPath=tr.v.d.GroupingPath.getFromHistogram(parentHist);for(const[name,otherRelatedHist]of otherDiagnostic){const mergedTo=otherRelatedHist.diagnostics.get(tr.v.d.RESERVED_NAMES.MERGED_TO);if(mergedTo===undefined)continue;for(const relatedHist of mergedTo.histogramsByName_.values()){const relatedGroupingPath=tr.v.d.GroupingPath.getFromHistogram(relatedHist);if(relatedGroupingPath===undefined)continue;if(!parentGroupingPath.equals(relatedGroupingPath))continue;this.set(name,relatedHist);}}}
-get(name){return this.histogramsByName_.get(name);}
-set(name,hist){if(!(hist instanceof tr.v.Histogram)&&!(hist instanceof tr.v.d.HistogramRef)){throw new Error('Must be instanceof Histogram or HistogramRef: '+
-hist);}
-this.histogramsByName_.set(name,hist);}
-add(hist){this.set(hist.name,hist);}
-get length(){return this.histogramsByName_.size;}*[Symbol.iterator](){for(const pair of this.histogramsByName_){yield pair;}}
-resolve(histograms,opt_required){for(const[name,value]of this){if(!(value instanceof tr.v.d.HistogramRef))continue;const guid=value.guid;const hist=histograms.lookupHistogram(guid);if(hist instanceof tr.v.Histogram){this.histogramsByName_.set(name,hist);}else if(opt_required){throw new Error('Unable to find Histogram '+guid);}}}
-asDictInto_(d){d.values={};for(const[name,hist]of this){d.values[name]=hist.guid;}}
-static fromDict(d){const map=new RelatedHistogramMap();for(const[name,guid]of Object.entries(d.values)){map.set(name,new tr.v.d.HistogramRef(guid));}
-return map;}}
-tr.v.d.Diagnostic.register(RelatedHistogramMap,{elementName:'tr-v-ui-related-histogram-map-span'});return{RelatedHistogramMap,};});'use strict';tr.exportTo('tr.v.d',function(){const COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER='ChromeUserFriendlyCategory';class RelatedHistogramBreakdown extends tr.v.d.RelatedHistogramMap{constructor(){super();this.colorScheme=undefined;}
-clone(){const clone=new RelatedHistogramBreakdown();clone.colorScheme=this.colorScheme;return clone;}
-canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof RelatedHistogramBreakdown&&otherDiagnostic.colorScheme===this.colorScheme;}
-set(name,hist){if(!(hist instanceof tr.v.d.HistogramRef)){if(!(hist instanceof tr.v.Histogram)){throw new Error('RelatedHistogramBreakdown can only contain Histograms');}
-if((this.length>0)&&(hist.unit!==tr.b.getFirstElement(this)[1].unit)){throw new Error('Units mismatch',tr.b.getFirstElement(this)[1].unit,hist.unit);}}
-tr.v.d.RelatedHistogramMap.prototype.set.call(this,name,hist);}
-asDictInto_(d){tr.v.d.RelatedHistogramMap.prototype.asDictInto_.call(this,d);if(this.colorScheme)d.colorScheme=this.colorScheme;}
-static fromDict(d){const diagnostic=new RelatedHistogramBreakdown();for(const[name,guid]of Object.entries(d.values)){diagnostic.set(name,new tr.v.d.HistogramRef(guid));}
-if(d.colorScheme)diagnostic.colorScheme=d.colorScheme;return diagnostic;}
-static buildFromEvents(histograms,namePrefix,events,categoryForEvent,unit,opt_sampleForEvent,opt_binBoundaries,opt_this){const sampleForEvent=opt_sampleForEvent||((event)=>event.cpuSelfTime);const diagnostic=new RelatedHistogramBreakdown();for(const event of events){const sample=sampleForEvent.call(opt_this,event);if(sample===undefined)continue;const eventCategory=categoryForEvent.call(opt_this,event);let hist=diagnostic.get(eventCategory);if(hist===undefined){hist=new tr.v.Histogram(namePrefix+eventCategory,unit,opt_binBoundaries);histograms.addHistogram(hist);diagnostic.set(eventCategory,hist);}
-hist.addSample(sample,{relatedEvents:new tr.v.d.RelatedEventSet([event])});}
-return diagnostic;}}
-tr.v.d.Diagnostic.register(RelatedHistogramBreakdown,{elementName:'tr-v-ui-breakdown-span'});return{COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER,RelatedHistogramBreakdown,};});'use strict';tr.exportTo('tr.v.d',function(){class RelatedNameMap extends tr.v.d.Diagnostic{constructor(opt_info){super();this.map_=new Map();}
+tr.v.d.Diagnostic.register(RelatedEventSet,{elementName:'tr-v-ui-related-event-set-span'});return{RelatedEventSet,};});'use strict';tr.exportTo('tr.v.d',function(){class RelatedNameMap extends tr.v.d.Diagnostic{constructor(opt_info){super();this.map_=new Map();if(opt_info){for(const[key,name]of Object.entries(opt_info)){this.set(key,name);}}}
 clone(){const clone=new RelatedNameMap();clone.addDiagnostic(this);return clone;}
 equals(other){if(!(other instanceof RelatedNameMap))return false;const keys1=new Set(this.map_.keys());const keys2=new Set(other.map_.keys());if(!tr.b.setsEqual(keys1,keys2))return false;for(const[key,name]of this){if(name!==other.get(key))return false;}
 return true;}
 canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof RelatedNameMap;}
 addDiagnostic(otherDiagnostic){for(const[key,name]of otherDiagnostic){const existing=this.get(key);if(existing===undefined){this.set(key,name);}else if(existing!==name){throw new Error('Histogram names differ: '+`"${existing}" != "${name}"`);}}}
+serialize(serializer){const keys=[...this.map_.keys()];keys.sort();const names=keys.map(k=>serializer.getOrAllocateId(this.get(k)));const keysId=serializer.getOrAllocateId(keys.map(k=>serializer.getOrAllocateId(k)));return[keysId,...names];}
 asDictInto_(d){d.names={};for(const[key,name]of this)d.names[key]=name;}
 set(key,name){this.map_.set(key,name);}
 get(key){return this.map_.get(key);}*[Symbol.iterator](){for(const pair of this.map_)yield pair;}*values(){for(const value of this.map_.values())yield value;}
 static fromEntries(entries){const names=new RelatedNameMap();for(const[key,name]of entries){names.set(key,name);}
 return names;}
+static deserialize(data,deserializer){const names=new RelatedNameMap();const keys=deserializer.getObject(data[0]);for(let i=0;i<keys.length;++i){names.set(deserializer.getObject(keys[i]),deserializer.getObject(data[i+1]));}
+return names;}
 static fromDict(d){return RelatedNameMap.fromEntries(Object.entries(d.names||{}));}}
 tr.v.d.Diagnostic.register(RelatedNameMap,{elementName:'tr-v-ui-related-name-map-span',});return{RelatedNameMap,};});'use strict';tr.exportTo('tr.v.d',function(){class Scalar extends tr.v.d.Diagnostic{constructor(value){super();if(!(value instanceof tr.b.Scalar)){throw new Error('expected Scalar');}
 this.value=value;}
 clone(){return new Scalar(this.value);}
+serialize(serializer){return this.value.asDict();}
 asDictInto_(d){d.value=this.value.asDict();}
+static deserialize(value,deserializer){return Scalar.fromDict({value});}
 static fromDict(d){return new Scalar(tr.b.Scalar.fromDict(d.value));}}
-tr.v.d.Diagnostic.register(Scalar,{elementName:'tr-v-ui-scalar-diagnostic-span'});return{Scalar,};});'use strict';tr.exportTo('tr.v.d',function(){class TagMap extends tr.v.d.Diagnostic{constructor(opt_info){super();this.tagsToStoryNames_=new Map();if(opt_info){for(const[tag,storyDisplayNames]of Object.entries(opt_info.tagsToStoryNames||{})){this.tagsToStoryNames.set(tag,new Set(storyDisplayNames));}}}
-clone(){const clone=new TagMap();clone.addDiagnostic(this);return clone;}
-addToHistogram(hist){hist.diagnostics.set(tr.v.d.RESERVED_NAMES.TAG_MAP,this);}
-equals(other){if(!(other instanceof TagMap))return false;const keys1=new Set(this.tagsToStoryNames.keys());const keys2=new Set(other.tagsToStoryNames.keys());if(!tr.b.setsEqual(keys1,keys2)){return false;}
-for(const key of keys1){if(!tr.b.setsEqual(this.tagsToStoryNames.get(key),other.tagsToStoryNames.get(key))){return false;}}
-return true;}
-canAddDiagnostic(otherDiagnostic){return otherDiagnostic instanceof TagMap;}
-addDiagnostic(otherDiagnostic){for(const[name,storyDisplayNames]of
-otherDiagnostic.tagsToStoryNames){if(!this.tagsToStoryNames.has(name)){this.tagsToStoryNames.set(name,new Set());}
-for(const t of storyDisplayNames){this.tagsToStoryNames.get(name).add(t);}}
-return this;}
-asDictInto_(d){d.tagsToStoryNames={};for(const[name,value]of this.tagsToStoryNames){d.tagsToStoryNames[name]=Array.from(value);}}
-get tagsToStoryNames(){return this.tagsToStoryNames_;}
-static fromDict(d){const info=new TagMap();for(const[name,values]of
-Object.entries(d.tagsToStoryNames||{})){info.tagsToStoryNames.set(name,new Set(values));}
-return info;}}
-tr.v.d.Diagnostic.register(TagMap,{elementName:'tr-v-ui-tag-map-span'});return{TagMap,};});'use strict';tr.exportTo('tr.v.d',function(){class UnmergeableDiagnosticSet extends tr.v.d.Diagnostic{constructor(diagnostics){super();this._diagnostics=diagnostics;}
+tr.v.d.Diagnostic.register(Scalar,{elementName:'tr-v-ui-scalar-diagnostic-span'});return{Scalar,};});'use strict';tr.exportTo('tr.v.d',function(){class UnmergeableDiagnosticSet extends tr.v.d.Diagnostic{constructor(diagnostics){super();this._diagnostics=diagnostics;}
 clone(){const clone=new tr.v.d.UnmergeableDiagnosticSet();clone.addDiagnostic(this);return clone;}
 canAddDiagnostic(otherDiagnostic){return true;}
 addDiagnostic(otherDiagnostic){if(otherDiagnostic instanceof UnmergeableDiagnosticSet){for(const subOtherDiagnostic of otherDiagnostic){const clone=subOtherDiagnostic.clone();this.addDiagnostic(clone);}
 return;}
 for(let i=0;i<this._diagnostics.length;++i){if(this._diagnostics[i].canAddDiagnostic(otherDiagnostic)){this._diagnostics[i].addDiagnostic(otherDiagnostic);return;}}
 const clone=otherDiagnostic.clone();this._diagnostics.push(clone);}
-mergeRelationships(otherDiagnostic,parentHist,otherParentHist){if(otherDiagnostic instanceof UnmergeableDiagnosticSet){for(const subDiagnostic of otherDiagnostic){this.mergeRelationships(subDiagnostic,parentHist,otherParentHist);}
-return;}
-for(const subDiagnostic of this){if(!(subDiagnostic instanceof tr.v.d.RelatedHistogramMap)&&!(subDiagnostic instanceof tr.v.d.RelatedHistogramBreakdown)){continue;}
-subDiagnostic.mergeRelationships(otherDiagnostic,parentHist,otherParentHist);}}
 get length(){return this._diagnostics.length;}*[Symbol.iterator](){for(const diagnostic of this._diagnostics)yield diagnostic;}
 asDictInto_(d){d.diagnostics=this._diagnostics.map(d=>d.asDictOrReference());}
+static deserialize(data,deserializer){return new UnmergeableDiagnosticSet(d.map(i=>deserializer.getDiagnostic(i).diagnostic));}
+serialize(serializer){return this._diagnostics.map(d=>serializer.getOrAllocateDiagnosticId('',d));}
 static fromDict(d){return new UnmergeableDiagnosticSet(d.diagnostics.map(d=>((typeof d==='string')?new tr.v.d.DiagnosticRef(d):tr.v.d.Diagnostic.fromDict(d))));}}
-tr.v.d.Diagnostic.register(UnmergeableDiagnosticSet,{elementName:'tr-v-ui-unmergeable-diagnostic-set-span'});return{UnmergeableDiagnosticSet,};});'use strict';tr.exportTo('tr.v.d',function(){const RESERVED_INFOS={ANGLE_REVISIONS:{name:'angleRevisions',type:tr.v.d.GenericSet},ARCHITECTURES:{name:'architectures',type:tr.v.d.GenericSet},BENCHMARKS:{name:'benchmarks',type:tr.v.d.GenericSet},BENCHMARK_START:{name:'benchmarkStart',type:tr.v.d.DateRange},BENCHMARK_DESCRIPTIONS:{name:'benchmarkDescriptions',type:tr.v.d.GenericSet},BOTS:{name:'bots',type:tr.v.d.GenericSet},BUG_COMPONENTS:{name:'bugComponents',type:tr.v.d.GenericSet},BUILDS:{name:'builds',type:tr.v.d.GenericSet},CATAPULT_REVISIONS:{name:'catapultRevisions',type:tr.v.d.GenericSet},CHROMIUM_COMMIT_POSITIONS:{name:'chromiumCommitPositions',type:tr.v.d.GenericSet},CHROMIUM_REVISIONS:{name:'chromiumRevisions',type:tr.v.d.GenericSet},DEVICE_IDS:{name:'deviceIds',type:tr.v.d.GenericSet},DOCUMENTATION_URLS:{name:'documentationUrls',type:tr.v.d.GenericSet},FUCHSIA_GARNET_REVISIONS:{name:'fuchsiaGarnetRevisions',type:tr.v.d.GenericSet},FUCHSIA_PERIDOT_REVISIONS:{name:'fuchsiaPeridotRevisions',type:tr.v.d.GenericSet},FUCHSIA_TOPAZ_REVISIONS:{name:'fuchsiaTopazRevisions',type:tr.v.d.GenericSet},FUCHSIA_ZIRCON_REVISIONS:{name:'fuchsiaZirconRevisions',type:tr.v.d.GenericSet},GPUS:{name:'gpus',type:tr.v.d.GenericSet},GROUPING_PATH:{name:'groupingPath',type:tr.v.d.GroupingPath},IS_REFERENCE_BUILD:{name:'isReferenceBuild',type:tr.v.d.GenericSet},LABELS:{name:'labels',type:tr.v.d.GenericSet},LOG_URLS:{name:'logUrls',type:tr.v.d.GenericSet},MASTERS:{name:'masters',type:tr.v.d.GenericSet},MEMORY_AMOUNTS:{name:'memoryAmounts',type:tr.v.d.GenericSet},MERGED_FROM:{name:'mergedFrom',type:tr.v.d.RelatedHistogramMap},MERGED_TO:{name:'mergedTo',type:tr.v.d.RelatedHistogramMap},OS_NAMES:{name:'osNames',type:tr.v.d.GenericSet},OS_VERSIONS:{name:'osVersions',type:tr.v.d.GenericSet},OWNERS:{name:'owners',type:tr.v.d.GenericSet},POINT_ID:{name:'pointId',type:tr.v.d.GenericSet},PRODUCT_VERSIONS:{name:'productVersions',type:tr.v.d.GenericSet},RELATED_NAMES:{name:'relatedNames',type:tr.v.d.GenericSet},REVISION_TIMESTAMPS:{name:'revisionTimestamps',type:tr.v.d.DateRange},SKIA_REVISIONS:{name:'skiaRevisions',type:tr.v.d.GenericSet},STORIES:{name:'stories',type:tr.v.d.GenericSet},STORYSET_REPEATS:{name:'storysetRepeats',type:tr.v.d.GenericSet},STORY_TAGS:{name:'storyTags',type:tr.v.d.GenericSet},SUMMARY_KEYS:{name:'summaryKeys',type:tr.v.d.GenericSet},TAG_MAP:{name:'tagmap',type:tr.v.d.TagMap},TEST_PATH:{name:'testPath',type:tr.v.d.GenericSet},TRACE_START:{name:'traceStart',type:tr.v.d.DateRange},TRACE_URLS:{name:'traceUrls',type:tr.v.d.GenericSet},V8_COMMIT_POSITIONS:{name:'v8CommitPositions',type:tr.v.d.DateRange},V8_REVISIONS:{name:'v8Revisions',type:tr.v.d.GenericSet},WEBRTC_REVISIONS:{name:'webrtcRevisions',type:tr.v.d.GenericSet},};const RESERVED_NAMES={};const RESERVED_NAMES_TO_TYPES=new Map();for(const[codename,info]of Object.entries(RESERVED_INFOS)){RESERVED_NAMES[codename]=info.name;if(RESERVED_NAMES_TO_TYPES.has(info.name)){throw new Error(`Duplicate reserved name "${info.name}"`);}
+tr.v.d.Diagnostic.register(UnmergeableDiagnosticSet,{elementName:'tr-v-ui-unmergeable-diagnostic-set-span'});return{UnmergeableDiagnosticSet,};});'use strict';tr.exportTo('tr.v.d',function(){const RESERVED_INFOS={ANGLE_REVISIONS:{name:'angleRevisions',type:tr.v.d.GenericSet},ARCHITECTURES:{name:'architectures',type:tr.v.d.GenericSet},BENCHMARKS:{name:'benchmarks',type:tr.v.d.GenericSet},BENCHMARK_START:{name:'benchmarkStart',type:tr.v.d.DateRange},BENCHMARK_DESCRIPTIONS:{name:'benchmarkDescriptions',type:tr.v.d.GenericSet},BOTS:{name:'bots',type:tr.v.d.GenericSet},BUG_COMPONENTS:{name:'bugComponents',type:tr.v.d.GenericSet},BUILDS:{name:'builds',type:tr.v.d.GenericSet},CATAPULT_REVISIONS:{name:'catapultRevisions',type:tr.v.d.GenericSet},CHROMIUM_COMMIT_POSITIONS:{name:'chromiumCommitPositions',type:tr.v.d.GenericSet},CHROMIUM_REVISIONS:{name:'chromiumRevisions',type:tr.v.d.GenericSet},DESCRIPTION:{name:'description',type:tr.v.d.GenericSet},DEVICE_IDS:{name:'deviceIds',type:tr.v.d.GenericSet},DOCUMENTATION_URLS:{name:'documentationUrls',type:tr.v.d.GenericSet},FUCHSIA_GARNET_REVISIONS:{name:'fuchsiaGarnetRevisions',type:tr.v.d.GenericSet},FUCHSIA_PERIDOT_REVISIONS:{name:'fuchsiaPeridotRevisions',type:tr.v.d.GenericSet},FUCHSIA_TOPAZ_REVISIONS:{name:'fuchsiaTopazRevisions',type:tr.v.d.GenericSet},FUCHSIA_ZIRCON_REVISIONS:{name:'fuchsiaZirconRevisions',type:tr.v.d.GenericSet},GPUS:{name:'gpus',type:tr.v.d.GenericSet},IS_REFERENCE_BUILD:{name:'isReferenceBuild',type:tr.v.d.GenericSet},LABELS:{name:'labels',type:tr.v.d.GenericSet},LOG_URLS:{name:'logUrls',type:tr.v.d.GenericSet},MASTERS:{name:'masters',type:tr.v.d.GenericSet},MEMORY_AMOUNTS:{name:'memoryAmounts',type:tr.v.d.GenericSet},OS_NAMES:{name:'osNames',type:tr.v.d.GenericSet},OS_VERSIONS:{name:'osVersions',type:tr.v.d.GenericSet},OWNERS:{name:'owners',type:tr.v.d.GenericSet},POINT_ID:{name:'pointId',type:tr.v.d.GenericSet},PRODUCT_VERSIONS:{name:'productVersions',type:tr.v.d.GenericSet},REVISION_TIMESTAMPS:{name:'revisionTimestamps',type:tr.v.d.DateRange},SKIA_REVISIONS:{name:'skiaRevisions',type:tr.v.d.GenericSet},STATISTICS_NAMES:{name:'statisticsNames',type:tr.v.d.GenericSet},STORIES:{name:'stories',type:tr.v.d.GenericSet},STORYSET_REPEATS:{name:'storysetRepeats',type:tr.v.d.GenericSet},STORY_TAGS:{name:'storyTags',type:tr.v.d.GenericSet},SUMMARY_KEYS:{name:'summaryKeys',type:tr.v.d.GenericSet},TEST_PATH:{name:'testPath',type:tr.v.d.GenericSet},TRACE_START:{name:'traceStart',type:tr.v.d.DateRange},TRACE_URLS:{name:'traceUrls',type:tr.v.d.GenericSet},V8_COMMIT_POSITIONS:{name:'v8CommitPositions',type:tr.v.d.DateRange},V8_REVISIONS:{name:'v8Revisions',type:tr.v.d.GenericSet},WEBRTC_REVISIONS:{name:'webrtcRevisions',type:tr.v.d.GenericSet},};const RESERVED_NAMES={};const RESERVED_NAMES_TO_TYPES=new Map();for(const[codename,info]of Object.entries(RESERVED_INFOS)){RESERVED_NAMES[codename]=info.name;if(RESERVED_NAMES_TO_TYPES.has(info.name)){throw new Error(`Duplicate reserved name "${info.name}"`);}
 RESERVED_NAMES_TO_TYPES.set(info.name,info.type);}
 const RESERVED_NAMES_SET=new Set(Object.values(RESERVED_NAMES));return{RESERVED_INFOS,RESERVED_NAMES,RESERVED_NAMES_SET,RESERVED_NAMES_TO_TYPES,};});'use strict';tr.exportTo('tr.v.d',function(){class DiagnosticMap extends Map{constructor(opt_allowReservedNames){super();if(opt_allowReservedNames===undefined){opt_allowReservedNames=true;}
 this.allowReservedNames_=opt_allowReservedNames;}
@@ -7150,23 +7283,22 @@
 if(!this.allowReservedNames_&&tr.v.d.RESERVED_NAMES_SET.has(name)&&!(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet)&&!(diagnostic instanceof tr.v.d.DiagnosticRef)){const type=tr.v.d.RESERVED_NAMES_TO_TYPES.get(name);if(type&&!(diagnostic instanceof type)){throw new Error(`Diagnostics named "${name}" must be ${type.name}, `+`not ${diagnostic.constructor.name}`);}}
 Map.prototype.set.call(this,name,diagnostic);}
 delete(name){if(name===undefined)throw new Error('missing name');Map.prototype.delete.call(this,name);}
-addDicts(dict){for(const[name,diagnosticDict]of Object.entries(dict)){if(typeof diagnosticDict==='string'){this.set(name,new tr.v.d.DiagnosticRef(diagnosticDict));}else{this.set(name,tr.v.d.Diagnostic.fromDict(diagnosticDict));}}}
+deserializeAdd(data,deserializer){for(const id of data){const{name,diagnostic}=deserializer.getDiagnostic(id);this.set(name,diagnostic);}}
+addDicts(dict){for(const[name,diagnosticDict]of Object.entries(dict)){if(name==='tagmap')continue;if(typeof diagnosticDict==='string'){this.set(name,new tr.v.d.DiagnosticRef(diagnosticDict));}else if(diagnosticDict.type!=='RelatedHistogramMap'&&diagnosticDict.type!=='RelatedHistogramBreakdown'&&diagnosticDict.type!=='TagMap'){this.set(name,tr.v.d.Diagnostic.fromDict(diagnosticDict));}}}
 resolveSharedDiagnostics(histograms,opt_required){for(const[name,value]of this){if(!(value instanceof tr.v.d.DiagnosticRef)){continue;}
 const guid=value.guid;const diagnostic=histograms.lookupDiagnostic(guid);if(diagnostic instanceof tr.v.d.Diagnostic){this.set(name,diagnostic);}else if(opt_required){throw new Error('Unable to find shared Diagnostic '+guid);}}}
+serialize(serializer){const data=[];for(const[name,diagnostic]of this){data.push(serializer.getOrAllocateDiagnosticId(name,diagnostic));}
+return data;}
 asDict(){const dict={};for(const[name,diagnostic]of this){dict[name]=diagnostic.asDictOrReference();}
 return dict;}
+static deserialize(data,deserializer){const diagnostics=new DiagnosticMap();diagnostics.deserializeAdd(data,deserializer);return diagnostics;}
 static fromDict(d){const diagnostics=new DiagnosticMap();diagnostics.addDicts(d);return diagnostics;}
-static fromObject(obj){const diagnostics=new DiagnosticMap();if(!(obj instanceof Map))obj=Object.entries(obj);for(const[name,diagnostic]of obj){diagnostics.set(name,diagnostic);}
+static fromObject(obj){const diagnostics=new DiagnosticMap();if(!(obj instanceof Map))obj=Object.entries(obj);for(const[name,diagnostic]of obj){if(!diagnostic)continue;diagnostics.set(name,diagnostic);}
 return diagnostics;}
-addDiagnostics(other){for(const[name,otherDiagnostic]of other){if(name===tr.v.d.RESERVED_NAMES.MERGED_FROM||name===tr.v.d.RESERVED_NAMES.MERGED_TO||name===tr.v.d.RESERVED_NAMES.GROUPING_PATH){continue;}
-const myDiagnostic=this.get(name);if(myDiagnostic!==undefined&&myDiagnostic.canAddDiagnostic(otherDiagnostic)){myDiagnostic.addDiagnostic(otherDiagnostic);continue;}
+addDiagnostics(other){for(const[name,otherDiagnostic]of other){const myDiagnostic=this.get(name);if(myDiagnostic!==undefined&&myDiagnostic.canAddDiagnostic(otherDiagnostic)){myDiagnostic.addDiagnostic(otherDiagnostic);continue;}
 const clone=otherDiagnostic.clone();if(myDiagnostic===undefined){this.set(name,clone);continue;}
-this.set(name,new tr.v.d.UnmergeableDiagnosticSet([myDiagnostic,clone]));}}
-mergeRelationships(parentHist){for(const[name,diagnostic]of this){if(!(diagnostic instanceof tr.v.d.RelatedHistogramMap)&&!(diagnostic instanceof tr.v.d.RelatedHistogramBreakdown)&&!(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet)){continue;}
-for(const[unusedName,otherHist]of
-this.get(tr.v.d.RESERVED_NAMES.MERGED_FROM)){const otherDiagnostic=otherHist.diagnostics.get(name);if(!(otherDiagnostic instanceof tr.v.d.RelatedHistogramMap)&&!(otherDiagnostic instanceof tr.v.d.RelatedHistogramBreakdown)&&!(otherDiagnostic instanceof tr.v.d.UnmergeableDiagnosticSet)){continue;}
-diagnostic.mergeRelationships(otherDiagnostic,parentHist,otherHist);}}}}
-return{DiagnosticMap,};});'use strict';tr.exportTo('tr.v',function(){const MAX_DIAGNOSTIC_MAPS=16;const DEFAULT_SAMPLE_VALUES_PER_BIN=10;const DEFAULT_REBINNED_COUNT=40;const DEFAULT_BOUNDARIES_FOR_UNIT=new Map();const DELTA=String.fromCharCode(916);const Z_SCORE_NAME='z-score';const P_VALUE_NAME='p-value';const U_STATISTIC_NAME='U';function percentToString(percent,opt_force3){if(percent<0||percent>1){throw new Error('percent must be in [0,1]');}
+this.set(name,new tr.v.d.UnmergeableDiagnosticSet([myDiagnostic,clone]));}}}
+return{DiagnosticMap};});'use strict';tr.exportTo('tr.v',function(){const MAX_DIAGNOSTIC_MAPS=16;const DEFAULT_SAMPLE_VALUES_PER_BIN=10;const DEFAULT_REBINNED_COUNT=40;const DEFAULT_BOUNDARIES_FOR_UNIT=new Map();const DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING=500;const DELTA=String.fromCharCode(916);const Z_SCORE_NAME='z-score';const P_VALUE_NAME='p-value';const U_STATISTIC_NAME='U';function percentToString(percent,opt_force3){if(percent<0||percent>1){throw new Error('percent must be in [0,1]');}
 if(percent===0)return'000';if(percent===1)return'100';let str=percent.toString();if(str[1]!=='.'){throw new Error('Unexpected percent');}
 str=str+'0'.repeat(Math.max(4-str.length,0));if(str.length>4){if(opt_force3){str=str.slice(0,4);}else{str=str.slice(0,4)+'_'+str.slice(4);}}
 return'0'+str.slice(2);}
@@ -7176,15 +7308,20 @@
 addDiagnosticMap(diagnostics){tr.b.math.Statistics.uniformlySampleStream(this.diagnosticMaps,this.count,diagnostics,MAX_DIAGNOSTIC_MAPS);}
 addBin(other){if(!this.range.equals(other.range)){throw new Error('Merging incompatible Histogram bins.');}
 tr.b.math.Statistics.mergeSampledStreams(this.diagnosticMaps,this.count,other.diagnosticMaps,other.count,MAX_DIAGNOSTIC_MAPS);this.count+=other.count;}
+deserialize(data,deserializer){if(!(data instanceof Array)){this.count=data;return;}
+this.count=data[0];for(const sample of data.slice(1)){if(!(sample instanceof Array))continue;this.diagnosticMaps.push(tr.v.d.DiagnosticMap.deserialize(sample.slice(1),deserializer));}}
 fromDict(dict){this.count=dict[0];if(dict.length>1){for(const map of dict[1]){this.diagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map));}}}
+serialize(serializer){if(!this.diagnosticMaps.length){return this.count;}
+return[this.count,...this.diagnosticMaps.map(d=>[undefined,...d.serialize(serializer)])];}
 asDict(){if(!this.diagnosticMaps.length){return[this.count];}
 return[this.count,this.diagnosticMaps.map(d=>d.asDict())];}}
-const DEFAULT_SUMMARY_OPTIONS=new Map([['avg',true],['count',true],['geometricMean',false],['max',true],['min',true],['nans',false],['std',true],['sum',true],]);class Histogram{constructor(name,unit,opt_binBoundaries){let binBoundaries=opt_binBoundaries;if(!binBoundaries){const baseUnit=unit.baseUnit?unit.baseUnit:unit;binBoundaries=DEFAULT_BOUNDARIES_FOR_UNIT.get(baseUnit.unitName);}
-this.guid_=undefined;this.binBoundariesDict_=binBoundaries.asDict();this.allBins=binBoundaries.bins.slice();this.description='';const allowReservedNames=false;this.diagnostics_=new tr.v.d.DiagnosticMap(allowReservedNames);this.maxNumSampleValues_=this.defaultMaxNumSampleValues_;this.name_=name;this.nanDiagnosticMaps=[];this.numNans=0;this.running_=undefined;this.sampleValues_=[];this.shortName=undefined;this.summaryOptions=new Map(DEFAULT_SUMMARY_OPTIONS);this.summaryOptions.set('percentile',[]);this.summaryOptions.set('iprs',[]);this.unit=unit;}
-static create(name,unit,samples,opt_options){const options=opt_options||{};const hist=new Histogram(name,unit,options.binBoundaries);if(options.description)hist.description=options.description;if(options.shortName)hist.shortName=options.shortName;if(options.summaryOptions){let summaryOptions=options.summaryOptions;if(!(summaryOptions instanceof Map)){summaryOptions=Object.entries(summaryOptions);}
+const DEFAULT_SUMMARY_OPTIONS=new Map([['avg',true],['count',true],['geometricMean',false],['max',true],['min',true],['nans',false],['std',true],['sum',true],]);class Histogram{constructor(name,unit,opt_binBoundaries){if(!(unit instanceof tr.b.Unit)){throw new Error('unit must be a Unit: '+unit);}
+let binBoundaries=opt_binBoundaries;if(!binBoundaries){const baseUnit=unit.baseUnit?unit.baseUnit:unit;binBoundaries=DEFAULT_BOUNDARIES_FOR_UNIT.get(baseUnit.unitName);}
+this.binBoundariesDict_=binBoundaries.asDict();this.allBins=binBoundaries.bins.slice();this.description='';const allowReservedNames=false;this.diagnostics_=new tr.v.d.DiagnosticMap(allowReservedNames);this.maxNumSampleValues_=this.defaultMaxNumSampleValues_;this.name_=name;this.nanDiagnosticMaps=[];this.numNans=0;this.running_=undefined;this.sampleValues_=[];this.sampleMeans_=[];this.summaryOptions=new Map(DEFAULT_SUMMARY_OPTIONS);this.summaryOptions.set('percentile',[]);this.summaryOptions.set('iprs',[]);this.summaryOptions.set('ci',[]);this.unit=unit;}
+static create(name,unit,samples,opt_options){const options=opt_options||{};const hist=new Histogram(name,unit,options.binBoundaries);if(options.description)hist.description=options.description;if(options.summaryOptions){let summaryOptions=options.summaryOptions;if(!(summaryOptions instanceof Map)){summaryOptions=Object.entries(summaryOptions);}
 for(const[name,value]of summaryOptions){hist.summaryOptions.set(name,value);}}
 if(options.diagnostics!==undefined){let diagnostics=options.diagnostics;if(!(diagnostics instanceof Map)){diagnostics=Object.entries(diagnostics);}
-for(const[name,diagnostic]of diagnostics){hist.diagnostics.set(name,diagnostic);}}
+for(const[name,diagnostic]of diagnostics){if(!diagnostic)continue;hist.diagnostics.set(name,diagnostic);}}
 if(!(samples instanceof Array))samples=[samples];for(const sample of samples){if(typeof sample==='object'){hist.addSample(sample.value,sample.diagnostics);}else{hist.addSample(sample);}}
 return hist;}
 get diagnostics(){return this.diagnostics_;}
@@ -7192,14 +7329,21 @@
 get maxNumSampleValues(){return this.maxNumSampleValues_;}
 set maxNumSampleValues(n){this.maxNumSampleValues_=n;tr.b.math.Statistics.uniformlySampleArray(this.sampleValues_,this.maxNumSampleValues_);}
 get name(){return this.name_;}
-get guid(){if(this.guid_===undefined){this.guid_=tr.b.GUID.allocateUUID4();}
-return this.guid_;}
-set guid(guid){if(this.guid_!==undefined){throw new Error('Cannot reset guid');}
-this.guid_=guid;}
-static fromDict(dict){const hist=new Histogram(dict.name,tr.b.Unit.fromJSON(dict.unit),HistogramBinBoundaries.fromDict(dict.binBoundaries));hist.guid=dict.guid;if(dict.shortName){hist.shortName=dict.shortName;}
-if(dict.description){hist.description=dict.description;}
+deserializeStatistics_(){const statisticsNames=this.diagnostics.get(tr.v.d.RESERVED_NAMES.STATISTICS_NAMES);if(!statisticsNames)return;for(const statName of statisticsNames){if(statName.startsWith('pct_')){const percent=percentFromString(statName.substr(4));this.summaryOptions.get('percentile').push(percent);}else if(statName.startsWith('ipr_')){const lower=percentFromString(statName.substr(4,3));const upper=percentFromString(statName.substr(8));this.summaryOptions.get('iprs').push(tr.b.math.Range.fromExplicitRange(lower,upper));}else if(statName.startsWith('ci_')){const percent=percentFromString(statName.replace('_lower','').replace('_upper','').substr(3));if(!this.summaryOptions.get('ci').includes(percent)){this.summaryOptions.get('ci').push(percent);}}}
+for(const statName of this.summaryOptions.keys()){if(statName==='percentile'||statName==='iprs'||statName==='ci'){continue;}
+this.summaryOptions.set(statName,statisticsNames.has(statName));}}
+deserializeBin_(i,bin,deserializer){this.allBins[i]=new HistogramBin(this.allBins[i].range);this.allBins[i].deserialize(bin,deserializer);if(!(bin instanceof Array))return;for(let sample of bin.slice(1)){if(sample instanceof Array){sample=sample[0];}
+this.sampleValues_.push(sample);}}
+deserializeBins_(bins,deserializer){if(bins instanceof Array){for(let i=0;i<bins.length;++i){this.deserializeBin_(i,bins[i],deserializer);}}else{for(const[i,binData]of Object.entries(bins)){this.deserializeBin_(i,binData,deserializer);}}}
+static deserialize(data,deserializer){const[name,unit,boundaries,diagnostics,running,bins,nanBin]=data;const hist=new Histogram(deserializer.getObject(name),tr.b.Unit.fromJSON(unit),HistogramBinBoundaries.fromDict(deserializer.getObject(boundaries)));hist.diagnostics.deserializeAdd(diagnostics,deserializer);const description=hist.diagnostics.get(tr.v.d.RESERVED_NAMES.DESCRIPTION);if(description&&description.length){hist.description=[...description][0];}
+hist.deserializeStatistics_();if(running){hist.running_=tr.b.math.RunningStatistics.fromDict(running);}
+if(bins){hist.deserializeBins_(bins,deserializer);}
+if(nanBin){if(!(nanBin instanceof Array)){hist.numNans=nanBin;}else{hist.numNans=nanBin[0];for(const sample of nanBin.slice(1)){if(!(sample instanceof Array))continue;hist.nanDiagnosticMaps.push(tr.v.d.DiagnosticMap.deserialize(sample.slice(1),deserializer));}}}
+return hist;}
+static fromDict(dict){const hist=new Histogram(dict.name,tr.b.Unit.fromJSON(dict.unit),HistogramBinBoundaries.fromDict(dict.binBoundaries));if(dict.description){hist.description=dict.description;}
 if(dict.diagnostics){hist.diagnostics.addDicts(dict.diagnostics);}
-if(dict.allBins){if(dict.allBins.length!==undefined){for(let i=0;i<dict.allBins.length;++i){hist.allBins[i]=new HistogramBin(hist.allBins[i].range);hist.allBins[i].fromDict(dict.allBins[i]);}}else{for(const[i,binDict]of Object.entries(dict.allBins)){hist.allBins[i]=new HistogramBin(hist.allBins[i].range);hist.allBins[i].fromDict(binDict);}}}
+if(dict.allBins){if(dict.allBins.length!==undefined){for(let i=0;i<dict.allBins.length;++i){hist.allBins[i]=new HistogramBin(hist.allBins[i].range);hist.allBins[i].fromDict(dict.allBins[i]);}}else{for(const[i,binDict]of Object.entries(dict.allBins)){if(i>=hist.allBins.length||i<0){throw new Error('Invalid index "'+i+'" out of bounds of [0..'+hist.allBins.length+')');}
+hist.allBins[i]=new HistogramBin(hist.allBins[i].range);hist.allBins[i].fromDict(binDict);}}}
 if(dict.running){hist.running_=tr.b.math.RunningStatistics.fromDict(dict.running);}
 if(dict.summaryOptions){if(dict.summaryOptions.iprs){dict.summaryOptions.iprs=dict.summaryOptions.iprs.map(r=>tr.b.math.Range.fromExplicitRange(r[0],r[1]));}
 hist.customizeSummaryOptions(dict.summaryOptions);}
@@ -7227,28 +7371,35 @@
 return this.allBins[this.allBins.length-1].range.min;}
 getBinIndexForValue(value){const i=tr.b.findFirstTrueIndexInSortedArray(this.allBins,b=>value<b.range.max);if(0<=i&&i<this.allBins.length)return i;return this.allBins.length-1;}
 getBinForValue(value){return this.allBins[this.getBinIndexForValue(value)];}
-addSample(value,opt_diagnostics){if(opt_diagnostics&&!(opt_diagnostics instanceof tr.v.d.DiagnosticMap)){opt_diagnostics=tr.v.d.DiagnosticMap.fromObject(opt_diagnostics);}
+addSample(value,opt_diagnostics){if(opt_diagnostics){if(!(opt_diagnostics instanceof tr.v.d.DiagnosticMap)){opt_diagnostics=tr.v.d.DiagnosticMap.fromObject(opt_diagnostics);}
+for(const[name,diag]of opt_diagnostics){if(diag instanceof tr.v.d.Breakdown){diag.truncate(this.unit);}}}
 if(typeof(value)!=='number'||isNaN(value)){this.numNans++;if(opt_diagnostics){tr.b.math.Statistics.uniformlySampleStream(this.nanDiagnosticMaps,this.numNans,opt_diagnostics,MAX_DIAGNOSTIC_MAPS);}}else{if(this.running_===undefined){this.running_=new tr.b.math.RunningStatistics();}
-this.running_.add(value);const binIndex=this.getBinIndexForValue(value);let bin=this.allBins[binIndex];if(bin.count===0){bin=new HistogramBin(bin.range);this.allBins[binIndex]=bin;}
+this.sampleMeans_=[];this.running_.add(value);value=this.unit.truncate(value);const binIndex=this.getBinIndexForValue(value);let bin=this.allBins[binIndex];if(bin.count===0){bin=new HistogramBin(bin.range);this.allBins[binIndex]=bin;}
 bin.addSample(value);if(opt_diagnostics){bin.addDiagnosticMap(opt_diagnostics);}}
 tr.b.math.Statistics.uniformlySampleStream(this.sampleValues_,this.numValues+this.numNans,value,this.maxNumSampleValues);}
+resampleMean_(percent){const filteredSamples=this.sampleValues_.filter(value=>typeof(value)==='number'&&!isNaN(value));const sampleCount=filteredSamples.length;if(sampleCount===0||percent<=0.0||percent>=1.0){return[undefined,undefined];}else if(sampleCount===1){return[filteredSamples[0],filteredSamples[0]];}
+const iterations=DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING;if(this.sampleMeans_.length!==iterations){this.sampleMeans_=[];for(let i=0;i<iterations;i++){let tempSum=0.0;for(let j=0;j<sampleCount;j++){tempSum+=filteredSamples[Math.floor(Math.random()*sampleCount)];}
+this.sampleMeans_.push(tempSum/sampleCount);}
+this.sampleMeans_.sort((a,b)=>a-b);}
+return[this.sampleMeans_[Math.floor((iterations-1)*(0.5-percent/2))],this.sampleMeans_[Math.ceil((iterations-1)*(0.5+percent/2))],];}
 sampleValuesInto(samples){for(const sampleValue of this.sampleValues){samples.push(sampleValue);}}
 canAddHistogram(other){if(this.unit!==other.unit){return false;}
 if(this.binBoundariesDict_===other.binBoundariesDict_){return true;}
+if(!this.binBoundariesDict_||!other.binBoundariesDict_){return true;}
 if(this.binBoundariesDict_.length!==other.binBoundariesDict_.length){return false;}
 for(let i=0;i<this.binBoundariesDict_.length;++i){const slice=this.binBoundariesDict_[i];const otherSlice=other.binBoundariesDict_[i];if(slice instanceof Array){if(!(otherSlice instanceof Array)){return false;}
 if(slice[0]!==otherSlice[0]||!tr.b.math.approximately(slice[1],otherSlice[1])||slice[2]!==otherSlice[2]){return false;}}else{if(otherSlice instanceof Array){return false;}
 if(!tr.b.math.approximately(slice,otherSlice)){return false;}}}
 return true;}
 addHistogram(other){if(!this.canAddHistogram(other)){throw new Error('Merging incompatible Histograms');}
+if(!!this.binBoundariesDict_===!!other.binBoundariesDict_){for(let i=0;i<this.allBins.length;++i){let bin=this.allBins[i];if(bin.count===0){bin=new HistogramBin(bin.range);this.allBins[i]=bin;}
+bin.addBin(other.allBins[i]);}}else{const[multiBin,singleBin]=this.binBoundariesDict_?[this,other]:[other,this];for(const value of singleBin.sampleValues){if(typeof(value)!=='number'||isNaN(value)){continue;}
+const binIndex=multiBin.getBinIndexForValue(value);let bin=multiBin.allBins[binIndex];if(bin.count===0){bin=new HistogramBin(bin.range);multiBin.allBins[binIndex]=bin;}
+bin.addSample(value);}}
 tr.b.math.Statistics.mergeSampledStreams(this.nanDiagnosticMaps,this.numNans,other.nanDiagnosticMaps,other.numNans,MAX_DIAGNOSTIC_MAPS);tr.b.math.Statistics.mergeSampledStreams(this.sampleValues,this.numValues+this.numNans,other.sampleValues,other.numValues+other.numNans,(this.maxNumSampleValues+other.maxNumSampleValues)/2);this.numNans+=other.numNans;if(other.running_!==undefined){if(this.running_===undefined){this.running_=new tr.b.math.RunningStatistics();}
 this.running_=this.running_.merge(other.running_);}
-for(let i=0;i<this.allBins.length;++i){let bin=this.allBins[i];if(bin.count===0){bin=new HistogramBin(bin.range);this.allBins[i]=bin;}
-bin.addBin(other.allBins[i]);}
-let mergedFrom=this.diagnostics.get(tr.v.d.RESERVED_NAMES.MERGED_FROM);if(!mergedFrom){mergedFrom=new tr.v.d.RelatedHistogramMap();this.diagnostics.set(tr.v.d.RESERVED_NAMES.MERGED_FROM,mergedFrom);}
-mergedFrom.set(mergedFrom.length,other);let mergedTo=other.diagnostics.get(tr.v.d.RESERVED_NAMES.MERGED_TO);if(!mergedTo){mergedTo=new tr.v.d.RelatedHistogramMap();other.diagnostics.set(tr.v.d.RESERVED_NAMES.MERGED_TO,mergedTo);}
-mergedTo.set(mergedTo.length,this);this.diagnostics.addDiagnostics(other.diagnostics);for(const[stat,option]of other.summaryOptions){if(stat==='percentile'){const percentiles=this.summaryOptions.get(stat);for(const percent of option){if(!percentiles.includes(percent))percentiles.push(percent);}}else if(stat==='iprs'){const thisIprs=this.summaryOptions.get(stat);for(const ipr of option){let found=false;for(const thisIpr of thisIprs){found=ipr.equals(thisIpr);if(found)break;}
-if(!found)thisIprs.push(ipr);}}else if(option&&!this.summaryOptions.get(stat)){this.summaryOptions.set(stat,true);}}}
+this.sampleMeans_=[];this.diagnostics.addDiagnostics(other.diagnostics);for(const[stat,option]of other.summaryOptions){if(stat==='percentile'){const percentiles=this.summaryOptions.get(stat);for(const percent of option){if(!percentiles.includes(percent))percentiles.push(percent);}}else if(stat==='iprs'){const thisIprs=this.summaryOptions.get(stat);for(const ipr of option){let found=false;for(const thisIpr of thisIprs){found=ipr.equals(thisIpr);if(found)break;}
+if(!found)thisIprs.push(ipr);}}else if(stat==='ci'){const CIs=this.summaryOptions.get(stat);for(const CI of option){if(!CIs.includes(CI))CIs.push(CI);}}else if(option&&!this.summaryOptions.get(stat)){this.summaryOptions.set(stat,true);}}}
 customizeSummaryOptions(summaryOptions){for(const[key,value]of Object.entries(summaryOptions)){this.summaryOptions.set(key,value);}}
 getStatisticScalar(statName,opt_referenceHistogram,opt_mwu){if(statName==='avg'){if(typeof(this.average)!=='number')return undefined;return new tr.b.Scalar(this.unit,this.average);}
 if(statName==='std'){if(typeof(this.standardDeviation)!=='number')return undefined;return new tr.b.Scalar(this.unit,this.standardDeviation);}
@@ -7257,7 +7408,10 @@
 if(typeof(this.running_[statName])!=='number')return undefined;return new tr.b.Scalar(this.unit,this.running_[statName]);}
 if(statName==='nans'){return new tr.b.Scalar(tr.b.Unit.byName.count_smallerIsBetter,this.numNans);}
 if(statName==='count'){return new tr.b.Scalar(tr.b.Unit.byName.count_smallerIsBetter,this.numValues);}
-if(statName.substr(0,4)==='pct_'){const percent=percentFromString(statName.substr(4));if(this.numValues===0)return undefined;const percentile=this.getApproximatePercentile(percent);if(typeof(percentile)!=='number')return undefined;return new tr.b.Scalar(this.unit,percentile);}
+if(statName.substr(0,4)==='pct_'){if(this.numValues===0)return undefined;const percent=percentFromString(statName.substr(4));const percentile=this.getApproximatePercentile(percent);if(typeof(percentile)!=='number')return undefined;return new tr.b.Scalar(this.unit,percentile);}
+if(statName.substr(0,3)==='ci_'){const percent=percentFromString(statName.substr(3,3));const[lowCI,highCI]=this.resampleMean_(percent);if(statName.substr(7)==='lower'){if(typeof(lowCI)!=='number')return undefined;return new tr.b.Scalar(this.unit,lowCI);}else if(statName.substr(7)==='upper'){if(typeof(highCI)!=='number')return undefined;return new tr.b.Scalar(this.unit,highCI);}
+if(typeof(highCI)!=='number'||typeof(lowCI)!=='number'){return undefined;}
+return new tr.b.Scalar(this.unit,highCI-lowCI);}
 if(statName.substr(0,4)==='ipr_'){let lower=percentFromString(statName.substr(4,3));let upper=percentFromString(statName.substr(8));if(lower>=upper){throw new Error('Invalid inter-percentile range: '+statName);}
 lower=this.getApproximatePercentile(lower);upper=this.getApproximatePercentile(upper);const ipr=upper-lower;if(typeof(ipr)!=='number')return undefined;return new tr.b.Scalar(this.unit,ipr);}
 if(!this.canCompare(opt_referenceHistogram)){throw new Error('Cannot compute '+statName+' when histograms are not comparable');}
@@ -7267,7 +7421,7 @@
 const mwu=opt_mwu||tr.b.math.Statistics.mwu(this.sampleValues,opt_referenceHistogram.sampleValues);if(statName===P_VALUE_NAME){return new tr.b.Scalar(tr.b.Unit.byName.unitlessNumber,mwu.p);}
 if(statName===U_STATISTIC_NAME){return new tr.b.Scalar(tr.b.Unit.byName.unitlessNumber,mwu.U);}
 throw new Error('Unrecognized statistic name: '+statName);}
-get statisticsNames(){const statisticsNames=new Set();for(const[statName,option]of this.summaryOptions){if(statName==='percentile'){for(const pctile of option){statisticsNames.add('pct_'+tr.v.percentToString(pctile));}}else if(statName==='iprs'){for(const range of option){statisticsNames.add('ipr_'+tr.v.percentToString(range.min,true)+'_'+tr.v.percentToString(range.max,true));}}else if(option){statisticsNames.add(statName);}}
+get statisticsNames(){const statisticsNames=new Set();for(const[statName,option]of this.summaryOptions){if(statName==='percentile'){for(const pctile of option){statisticsNames.add('pct_'+tr.v.percentToString(pctile));}}else if(statName==='iprs'){for(const range of option){statisticsNames.add('ipr_'+tr.v.percentToString(range.min,true)+'_'+tr.v.percentToString(range.max,true));}}else if(statName==='ci'){for(const CIpctile of option){const CIpctStr=tr.v.percentToString(CIpctile);statisticsNames.add('ci_'+CIpctStr+'_lower');statisticsNames.add('ci_'+CIpctStr+'_upper');statisticsNames.add('ci_'+CIpctStr);}}else if(option){statisticsNames.add(statName);}}
 return statisticsNames;}
 canCompare(other){return other instanceof Histogram&&this.unit===other.unit&&this.numValues>0&&other.numValues>0;}
 getAvailableStatisticName(statName,opt_referenceHist){if(this.canCompare(opt_referenceHist))return statName;if(statName===Z_SCORE_NAME||statName===P_VALUE_NAME||statName===U_STATISTIC_NAME){return'avg';}
@@ -7277,24 +7431,31 @@
 get statisticsScalars(){const results=new Map();for(const statName of this.statisticsNames){const scalar=this.getStatisticScalar(statName);if(scalar===undefined)continue;results.set(statName,scalar);}
 return results;}
 get sampleValues(){return this.sampleValues_;}
-clone(){const binBoundaries=HistogramBinBoundaries.fromDict(this.binBoundariesDict_);const hist=new Histogram(this.name,this.unit,binBoundaries);for(const[stat,option]of this.summaryOptions){if(stat==='percentile'||stat==='iprs'){hist.summaryOptions.set(stat,Array.from(option));}else{hist.summaryOptions.set(stat,option);}}
+clone(){const binBoundaries=HistogramBinBoundaries.fromDict(this.binBoundariesDict_);const hist=new Histogram(this.name,this.unit,binBoundaries);for(const[stat,option]of this.summaryOptions){if(stat==='percentile'||stat==='iprs'||stat==='ci'){hist.summaryOptions.set(stat,Array.from(option));}else{hist.summaryOptions.set(stat,option);}}
 hist.addHistogram(this);return hist;}
 rebin(newBoundaries){const rebinned=new tr.v.Histogram(this.name,this.unit,newBoundaries);rebinned.description=this.description;for(const sample of this.sampleValues){rebinned.addSample(sample);}
 rebinned.running_=this.running_;for(const[name,diagnostic]of this.diagnostics){rebinned.diagnostics.set(name,diagnostic);}
-for(const[stat,option]of this.summaryOptions){if(stat==='percentile'){rebinned.summaryOptions.set(stat,Array.from(option));}else{rebinned.summaryOptions.set(stat,option);}}
+for(const[stat,option]of this.summaryOptions){if(stat==='percentile'||stat==='ci'){rebinned.summaryOptions.set(stat,Array.from(option));}else{rebinned.summaryOptions.set(stat,option);}}
 return rebinned;}
-asDict(){const dict={};dict.name=this.name;dict.unit=this.unit.asJSON();dict.guid=this.guid;if(this.binBoundariesDict_!==undefined){dict.binBoundaries=this.binBoundariesDict_;}
-if(this.shortName){dict.shortName=this.shortName;}
+serialize(serializer){let nanBin=this.numNans;if(this.nanDiagnosticMaps.length){nanBin=[nanBin,...this.nanDiagnosticMaps.map(dm=>[undefined,...dm.serialize(serializer)])];}
+this.diagnostics.set(tr.v.d.RESERVED_NAMES.STATISTICS_NAMES,new tr.v.d.GenericSet([...this.statisticsNames].sort()));this.diagnostics.set(tr.v.d.RESERVED_NAMES.DESCRIPTION,new tr.v.d.GenericSet([this.description].sort()));return[serializer.getOrAllocateId(this.name),this.unit.asJSON2(),serializer.getOrAllocateId(this.binBoundariesDict_),this.diagnostics.serialize(serializer),this.running_?this.running_.asDict():0,this.serializeBins_(serializer),nanBin,];}
+asDict(){const dict={};dict.name=this.name;dict.unit=this.unit.asJSON();if(this.binBoundariesDict_!==undefined){dict.binBoundaries=this.binBoundariesDict_;}
 if(this.description){dict.description=this.description;}
 if(this.diagnostics.size){dict.diagnostics=this.diagnostics.asDict();}
 if(this.maxNumSampleValues!==this.defaultMaxNumSampleValues_){dict.maxNumSampleValues=this.maxNumSampleValues;}
 if(this.numNans){dict.numNans=this.numNans;}
 if(this.nanDiagnosticMaps.length){dict.nanDiagnostics=this.nanDiagnosticMaps.map(dm=>dm.asDict());}
-if(this.numValues){dict.sampleValues=this.sampleValues.slice();dict.running=this.running_.asDict();dict.allBins=this.allBinsAsDict_();}
-const summaryOptions={};let anyOverriddenSummaryOptions=false;for(const[name,value]of this.summaryOptions){let option;if(name==='percentile'){if(value.length===0)continue;option=Array.from(value);}else if(name==='iprs'){if(value.length===0)continue;option=value.map(r=>[r.min,r.max]);}else if(value===DEFAULT_SUMMARY_OPTIONS.get(name)){continue;}else{option=value;}
+if(this.numValues){dict.sampleValues=this.sampleValues.slice();this.running.truncate(this.unit);dict.running=this.running_.asDict();dict.allBins=this.allBinsAsDict_();}
+const summaryOptions={};let anyOverriddenSummaryOptions=false;for(const[name,value]of this.summaryOptions){let option;if(name==='percentile'){if(value.length===0)continue;option=Array.from(value);}else if(name==='iprs'){if(value.length===0)continue;option=value.map(r=>[r.min,r.max]);}else if(name==='ci'){if(value.length===0)continue;option=Array.from(value);}else if(value===DEFAULT_SUMMARY_OPTIONS.get(name)){continue;}else{option=value;}
 summaryOptions[name]=option;anyOverriddenSummaryOptions=true;}
 if(anyOverriddenSummaryOptions){dict.summaryOptions=summaryOptions;}
 return dict;}
+serializeBins_(serializer){const numBins=this.allBins.length;let emptyBins=0;for(let i=0;i<numBins;++i){if(this.allBins[i].count===0){++emptyBins;}}
+if(emptyBins===numBins){return 0;}
+if(emptyBins>(numBins/2)){const allBinsDict={};for(let i=0;i<numBins;++i){const bin=this.allBins[i];if(bin.count>0){allBinsDict[i]=bin.serialize(serializer);}}
+return allBinsDict;}
+const allBinsArray=[];for(let i=0;i<numBins;++i){allBinsArray.push(this.allBins[i].serialize(serializer));}
+return allBinsArray;}
 allBinsAsDict_(){const numBins=this.allBins.length;let emptyBins=0;for(let i=0;i<numBins;++i){if(this.allBins[i].count===0){++emptyBins;}}
 if(emptyBins===numBins){return undefined;}
 if(emptyBins>(numBins/2)){const allBinsDict={};for(let i=0;i<numBins;++i){const bin=this.allBins[i];if(bin.count>0){allBinsDict[i]=bin.asDict();}}
@@ -7797,7 +7958,7 @@
 this.addEventListener(DataSeriesEnableChangeEventType,this.onDataSeriesEnableChange_.bind(this));},get hideLegend(){return this.hideLegend_;},set hideLegend(h){this.hideLegend_=h;this.updateContents_();},get showTitleInLegend(){return this.showTitleInLegend_;},set showTitleInLegend(s){this.showTitleInLegend_=s;this.updateContents_();},isSeriesEnabled(key){return this.getDataSeries(key).enabled;},onDataSeriesEnableChange_(event){this.getDataSeries(event.key).enabled=event.enabled;this.updateContents_();},get chartTitle(){return this.chartTitle_;},set chartTitle(chartTitle){this.chartTitle_=chartTitle;this.updateContents_();},get chartAreaElement(){return Polymer.dom(this).querySelector('#chart-area');},get graphWidth(){if(this.graphWidth_===undefined)return this.defaultGraphWidth;return this.graphWidth_;},set graphWidth(width){this.graphWidth_=width;this.updateContents_();},get defaultGraphWidth(){return 0;},get graphHeight(){if(this.graphHeight_===undefined)return this.defaultGraphHeight;return this.graphHeight_;},set graphHeight(height){this.graphHeight_=height;this.updateContents_();},get titleHeight(){return this.titleHeight_;},set titleHeight(height){this.titleHeight_=height;this.updateContents_();},get defaultGraphHeight(){return 0;},get totalWidth(){return this.margin.left+this.graphWidth+this.margin.right;},get totalHeight(){return this.margin.top+this.graphHeight+this.margin.bottom;},updateMargins_(){const legendSize=this.computeLegendSize_();this.margin.right=Math.max(this.margin.right,legendSize.width);this.margin.bottom=Math.max(this.margin.bottom,legendSize.height-this.graphHeight);if(this.chartTitle_){const titleSize=getSVGTextSize(this,this.chartTitle_,textNode=>{textNode.style.fontSize='16pt';});this.margin.top=Math.max(this.margin.top,titleSize.height+15);const horizontalOverhangPx=(titleSize.width-this.graphWidth)/2;this.margin.left=Math.max(this.margin.left,horizontalOverhangPx);this.margin.right=Math.max(this.margin.right,horizontalOverhangPx);}},computeLegendSize_(){let width=0;let height=0;if(this.hideLegend)return{width,height};let series=[...this.seriesByKey_.values()];if(this.showTitleInLegend){series=series.filter(series=>series.title!=='');}
 for(const seriesEntry of series){const legendText=this.showTitleInLegend?seriesEntry.title:seriesEntry.key;const textSize=getSVGTextSize(this,legendText);width=Math.max(width,textSize.width+30);height+=textSize.height;}
 return{width,height};},updateDimensions_(){const thisSel=d3.select(this);thisSel.attr('width',this.totalWidth);thisSel.attr('height',this.totalHeight);d3.select(this.chartAreaElement).attr('transform','translate('+this.margin.left+', '+this.margin.top+')');},updateContents_(){this.updateMargins_();this.updateDimensions_();this.updateTitle_();this.updateLegend_();},updateTitle_(){const titleSel=d3.select(this.chartAreaElement).select('#title');if(!this.chartTitle_){titleSel.style('display','none');return;}
-titleSel.attr('transform','translate('+this.graphWidth*0.5+',-15)').style('display',undefined).style('text-anchor','middle').style('font-size',this.titleHeight).attr('class','title').attr('width',this.graphWidth).text(this.chartTitle_);},updateLegend_(){const chartAreaSel=d3.select(this.chartAreaElement);chartAreaSel.selectAll('.legend').remove();if(this.hideLegend)return;let series;let seriesText;if(this.showTitleInLegend){series=[...this.seriesByKey_.values()].filter(series=>series.title!=='').reverse();seriesText=series=>series.title;}else{series=[...this.seriesByKey_.values()].reverse();seriesText=series=>series.key;}
+titleSel.attr('transform','translate('+this.graphWidth*0.5+',-15)').style('display',undefined).style('text-anchor','middle').style('font-size',this.titleHeight).attr('class','title').attr('width',this.graphWidth).text(this.chartTitle_);},updateLegend_(){const chartAreaSel=d3.select(this.chartAreaElement);chartAreaSel.selectAll('.legend').remove();if(this.hideLegend)return;let series;let seriesText;if(this.showTitleInLegend){series=[...this.seriesByKey_.values()].filter(series=>series.title!=='').filter(series=>series.color!=='transparent').reverse();seriesText=series=>series.title;}else{series=[...this.seriesByKey_.values()].filter(series=>series.color!=='transparent').reverse();seriesText=series=>series.key;}
 const legendEntriesSel=chartAreaSel.selectAll('.legend').data(series);legendEntriesSel.enter().append('foreignObject').attr('class','legend').attr('x',this.graphWidth+2).attr('width',this.margin.right).attr('height',18).attr('transform',(series,i)=>'translate(0,'+i*18+')').append('xhtml:body').style('margin',0).append('tr-ui-b-chart-legend-key').property('color',series=>((this.currentHighlightedLegendKey===series.key)?series.highlightedColor:series.color)).property('width',this.margin.right).property('target',series=>series.target).property('title',series=>series.title).property('optional',series=>series.optional).property('enabled',series=>series.enabled).text(seriesText);legendEntriesSel.exit().remove();},get highlightedLegendKey(){return this.highlightedLegendKey_;},set highlightedLegendKey(highlightedLegendKey){this.highlightedLegendKey_=highlightedLegendKey;this.updateHighlight_();},get currentHighlightedLegendKey(){if(this.tempHighlightedLegendKey_){return this.tempHighlightedLegendKey_;}
 return this.highlightedLegendKey_;},pushTempHighlightedLegendKey(key){if(this.tempHighlightedLegendKey_){throw new Error('push cannot nest');}
 this.tempHighlightedLegendKey_=key;this.updateHighlight_();},popTempHighlightedLegendKey(key){if(this.tempHighlightedLegendKey_!==key){throw new Error('pop cannot happen');}
@@ -7933,7 +8094,7 @@
 ctx.fillText(fullname,leftView-10,currentY-height/4);currentY+=height;}}
 return currentY;}};tr.ui.tracks.ObjectInstanceTrack.register(SystemStatsInstanceTrack,{typeName:'base::TraceEventSystemStatsMonitor::SystemStats'});return{SystemStatsInstanceTrack,};});'use strict';tr.exportTo('tr.ui.e.system_stats',function(){const SystemStatsSnapshotView=tr.ui.b.define('tr-ui-e-system-stats-snapshot-view',tr.ui.analysis.ObjectSnapshotView);SystemStatsSnapshotView.prototype={__proto__:tr.ui.analysis.ObjectSnapshotView.prototype,decorate(){Polymer.dom(this).classList.add('tr-ui-e-system-stats-snapshot-view');},updateContents(){const snapshot=this.objectSnapshot_;if(!snapshot||!snapshot.getStats()){Polymer.dom(this).textContent='No system stats snapshot found.';return;}
 Polymer.dom(this).textContent='';const stats=snapshot.getStats();Polymer.dom(this).appendChild(this.buildList_(stats));},isFloat(n){return typeof n==='number'&&n%1!==0;},buildList_(stats){const statList=document.createElement('ul');for(const statName in stats){const statText=document.createElement('li');Polymer.dom(statText).textContent=''+statName+': ';Polymer.dom(statList).appendChild(statText);if(stats[statName]instanceof Object){Polymer.dom(statList).appendChild(this.buildList_(stats[statName]));}else{if(this.isFloat(stats[statName])){Polymer.dom(statText).textContent+=stats[statName].toFixed(2);}else{Polymer.dom(statText).textContent+=stats[statName];}}}
-return statList;}};tr.ui.analysis.ObjectSnapshotView.register(SystemStatsSnapshotView,{typeName:'base::TraceEventSystemStatsMonitor::SystemStats'});return{SystemStatsSnapshotView,};});'use strict';tr.exportTo('tr.ui.e.v8',function(){const IGNORED_ENTRIES={match:full=>full.startsWith('*CODE_AGE_')};const INSTANCE_TYPE_GROUPS={FIXED_ARRAY_TYPE:{match:full=>full.startsWith('*FIXED_ARRAY_'),realEntry:'FIXED_ARRAY_TYPE',keyToName:key=>key.slice('*FIXED_ARRAY_'.length).slice(0,-('_SUB_TYPE'.length)),nameToKey:name=>'*FIXED_ARRAY_'+name+'_SUB_TYPE'},CODE_TYPE:{match:full=>full.startsWith('*CODE_'),realEntry:'CODE_TYPE',keyToName:key=>key.slice('*CODE_'.length),nameToKey:name=>'*CODE_'+name},JS_OBJECTS:{match:full=>full.startsWith('JS_'),keyToName:key=>key,nameToKey:name=>name},Strings:{match:full=>full.endsWith('STRING_TYPE'),keyToName:key=>key,nameToKey:name=>name}};const DIFF_COLOR={GREEN:'#64DD17',RED:'#D50000'};function computePercentage(valueA,valueB){if(valueA===0)return 0;return valueA/valueB*100;}
+return statList;}};tr.ui.analysis.ObjectSnapshotView.register(SystemStatsSnapshotView,{typeName:'base::TraceEventSystemStatsMonitor::SystemStats'});return{SystemStatsSnapshotView,};});'use strict';tr.exportTo('tr.ui.e.v8',function(){const IGNORED_ENTRIES={match:full=>full.startsWith('*CODE_AGE_')};const INSTANCE_TYPE_GROUPS={FIXED_ARRAY_TYPE:{match:full=>full.startsWith('*FIXED_ARRAY_'),realEntry:'FIXED_ARRAY_TYPE',keyToName:key=>key.slice('*FIXED_ARRAY_'.length).slice(0,-('_SUB_TYPE'.length)),nameToKey:name=>'*FIXED_ARRAY_'+name+'_SUB_TYPE'},CODE_TYPE:{match:full=>full.startsWith('*CODE_'),realEntry:'CODE_TYPE',keyToName:key=>key.slice('*CODE_'.length),nameToKey:name=>'*CODE_'+name},JS_OBJECTS:{match:full=>full.startsWith('JS_'),keyToName:key=>key,nameToKey:name=>name},Strings:{match:full=>full.endsWith('STRING_TYPE'),keyToName:key=>key,nameToKey:name=>name},Maps:{match:full=>full.startsWith('MAP_')&&full.endsWith('_TYPE'),keyToName:key=>key,nameToKey:name=>name},DescriptorArrays:{match:full=>full.endsWith('DESCRIPTOR_ARRAY_TYPE'),keyToName:key=>key,nameToKey:name=>name}};const DIFF_COLOR={GREEN:'#64DD17',RED:'#D50000'};function computePercentage(valueA,valueB){if(valueA===0)return 0;return valueA/valueB*100;}
 class DiffEntry{constructor(originalEntry,diffEntry){this.originalEntry_=originalEntry;this.diffEntry_=diffEntry;}
 get title(){return this.diffEntry_.title;}
 get overall(){return this.diffEntry_.overall;}
@@ -8077,7 +8238,7 @@
 MetricRegistry.checkFilename=function(metricName,opt_metricPathForTest){if(metricName==='runtimeStatsTotalMetric'||metricName==='v8AndMemoryMetrics'){return;}
 const expectedFilename=camelCaseToHackerString(metricName)+'.html';const stack=getCallStack();let metricPath=opt_metricPathForTest;if(metricPath===undefined){const paths=getPathsFromStack(stack);const METRIC_STACK_INDEX=5;if(paths.length<=METRIC_STACK_INDEX||paths[METRIC_STACK_INDEX].join('/')===paths[0].join('/')){return;}
 metricPath=paths[METRIC_STACK_INDEX].slice(paths[METRIC_STACK_INDEX].length-2);}
-if(!metricPath[1].endsWith('_test.html')&&metricPath[1]!==expectedFilename&&metricPath.join('_')!==expectedFilename){throw new Error('Expected '+metricName+' to be in a file named '+
+if(!metricPath[1].endsWith('_test.html')&&!metricPath[1].endsWith('_test.html.js')&&metricPath[1]!==expectedFilename&&metricPath[1]!==expectedFilename+'.js'&&metricPath.join('_')!==expectedFilename&&metricPath.join('_')!==expectedFilename+'.js'){throw new Error('Expected '+metricName+' to be in a file named '+
 expectedFilename+'; actual: '+metricPath.join('/')+'; stack: '+stack.replace(/\n/g,'\n  '));}};MetricRegistry.addEventListener('will-register',function(e){const metric=e.typeInfo.constructor;if(!(metric instanceof Function)){throw new Error('Metrics must be functions.');}
 if(!metric.name.endsWith('Metric')&&!metric.name.endsWith('Metrics')){throw new Error('Metric names must end with "Metric" or "Metrics".');}
 if(metric.length<2){throw new Error('Metrics take a HistogramSet and a Model and '+'optionally an options dictionary.');}
@@ -8085,14 +8246,21 @@
 if(slice.title==='RenderAccessibilityImpl::SendLocationChanges'){renderAccessibilityLocationsHist.addSample(slice.duration,{event:new tr.v.d.RelatedEventSet(slice)});}}}
 for(const browserHelper of Object.values(chromeHelper.browserHelpers)){const mainThread=browserHelper.mainThread;if(mainThread===undefined)continue;for(const slice of mainThread.getDescendantEvents()){if(slice.title==='BrowserAccessibilityManager::OnAccessibilityEvents'){browserAccessibilityEventsHist.addSample(slice.duration,{event:new tr.v.d.RelatedEventSet(slice)});}}}
 histograms.addHistogram(browserAccessibilityEventsHist);histograms.addHistogram(renderAccessibilityEventsHist);histograms.addHistogram(renderAccessibilityLocationsHist);}
-tr.metrics.MetricRegistry.register(accessibilityMetric);return{accessibilityMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const MESSAGE_LOOP_EVENT_NAME='Startup.BrowserMessageLoopStartTimeFromMainEntry3';const FIRST_CONTENTFUL_PAINT_EVENT_NAME='firstContentfulPaint';function androidStartupMetric(histograms,model){let messageLoopStartEvents=[];const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;for(const helper of chromeHelper.browserHelpers){for(const ev of helper.mainThread.asyncSliceGroup.childEvents()){if(ev.title===MESSAGE_LOOP_EVENT_NAME){messageLoopStartEvents.push(ev);}}}
-let firstContentfulPaintEvents=[];const rendererHelpers=chromeHelper.rendererHelpers;const pids=Object.keys(rendererHelpers);for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){if(!rendererHelper.mainThread)continue;for(const ev of rendererHelper.mainThread.sliceGroup.childEvents()){if(ev.title===FIRST_CONTENTFUL_PAINT_EVENT_NAME){firstContentfulPaintEvents.push(ev);break;}}}
-let totalBrowserStarts=messageLoopStartEvents.length;let totalFcpEvents=firstContentfulPaintEvents.length;if(totalFcpEvents!==totalBrowserStarts||totalBrowserStarts===0){messageLoopStartEvents=[];firstContentfulPaintEvents=[];for(const proc of Object.values(model.processes)){for(const ev of proc.getDescendantEvents()){if(ev.title===MESSAGE_LOOP_EVENT_NAME){messageLoopStartEvents.push(ev);}}
+tr.metrics.MetricRegistry.register(accessibilityMetric);return{accessibilityMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const MESSAGE_LOOP_EVENT_NAME='Startup.BrowserMessageLoopStartTimeFromMainEntry3';const CONTENT_START_EVENT_NAME='content::Start';const NAVIGATION_EVENT_NAME='Navigation StartToCommit';const FIRST_CONTENTFUL_PAINT_EVENT_NAME='firstContentfulPaint';function androidStartupMetric(histograms,model){let messageLoopStartEvents=[];let navigationEvents=[];const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;for(const helper of chromeHelper.browserHelpers){for(const ev of helper.mainThread.asyncSliceGroup.childEvents()){if(ev.title===MESSAGE_LOOP_EVENT_NAME){messageLoopStartEvents.push(ev);}else if(ev.title===NAVIGATION_EVENT_NAME){navigationEvents.push(ev);}}}
+let contentStartEvents=[];let firstContentfulPaintEvents=[];const rendererHelpers=chromeHelper.rendererHelpers;const pids=Object.keys(rendererHelpers);for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){if(!rendererHelper.mainThread)continue;for(const ev of rendererHelper.mainThread.sliceGroup.childEvents()){if(ev.title===FIRST_CONTENTFUL_PAINT_EVENT_NAME){firstContentfulPaintEvents.push(ev);break;}else if(ev.title===CONTENT_START_EVENT_NAME){contentStartEvents.push(ev);}}}
+let totalBrowserStarts=messageLoopStartEvents.length;let totalContentStartEvents=contentStartEvents.length;let totalFcpEvents=firstContentfulPaintEvents.length;let totalNavigations=navigationEvents.length;if(totalFcpEvents!==totalBrowserStarts||totalNavigations!==totalBrowserStarts||totalContentStartEvents!==totalBrowserStarts||totalBrowserStarts===0){messageLoopStartEvents=[];contentStartEvents=[];navigationEvents=[];firstContentfulPaintEvents=[];for(const proc of Object.values(model.processes)){for(const ev of proc.getDescendantEvents()){if(ev.title===MESSAGE_LOOP_EVENT_NAME){messageLoopStartEvents.push(ev);}else if(ev.title===NAVIGATION_EVENT_NAME){navigationEvents.push(ev);}else if(ev.title===CONTENT_START_EVENT_NAME){contentStartEvents.push(ev);}}
 for(const ev of proc.getDescendantEvents()){if(ev.title===FIRST_CONTENTFUL_PAINT_EVENT_NAME){firstContentfulPaintEvents.push(ev);break;}}}
-totalBrowserStarts=messageLoopStartEvents.length;totalFcpEvents=firstContentfulPaintEvents.length;}
-if(totalFcpEvents!==totalBrowserStarts){throw new Error('Number of FCP events ('+totalFcpEvents+') differs from number of browser starts ('+totalBrowserStarts+')');}
-const messageLoopStartHistogram=histograms.createHistogram('messageloop_start_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);const firstContentfulPaintHistogram=histograms.createHistogram('first_contentful_paint_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);function orderEvents(event1,event2){return event1.start-event2.start;}
-messageLoopStartEvents.sort(orderEvents);firstContentfulPaintEvents.sort(orderEvents);for(let i=2;i<totalBrowserStarts;i++){const startEvent=messageLoopStartEvents[i];messageLoopStartHistogram.addSample(startEvent.duration,{events:new tr.v.d.RelatedEventSet([startEvent])});const fcpEvent=firstContentfulPaintEvents[i];firstContentfulPaintHistogram.addSample(fcpEvent.end-startEvent.start,{events:new tr.v.d.RelatedEventSet([startEvent,fcpEvent])});}}
+totalBrowserStarts=messageLoopStartEvents.length;totalContentStartEvents=contentStartEvents.length;totalNavigations=navigationEvents.length;totalFcpEvents=firstContentfulPaintEvents.length;}
+function orderEvents(event1,event2){return event1.start-event2.start;}
+messageLoopStartEvents.sort(orderEvents);contentStartEvents.sort(orderEvents);navigationEvents.sort(orderEvents);firstContentfulPaintEvents.sort(orderEvents);if(totalFcpEvents<totalBrowserStarts){throw new Error('Found fewer FCP events ('+totalFcpEvents+') than browser starts ('+totalBrowserStarts+')');}
+const messageLoopStartHistogram=histograms.createHistogram('messageloop_start_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);const contentStartHistogram=histograms.createHistogram('experimental_content_start_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);const navigationStartHistogram=histograms.createHistogram('experimental_navigation_start_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);const navigationCommitHistogram=histograms.createHistogram('navigation_commit_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);const firstContentfulPaintHistogram=histograms.createHistogram('first_contentful_paint_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[]);let contentIndex=0;let navIndex=0;let fcpIndex=0;for(let loopStartIndex=0;loopStartIndex<totalBrowserStarts;){const startEvent=messageLoopStartEvents[loopStartIndex];if(fcpIndex===totalFcpEvents){break;}
+const contentStartEvent=contentIndex<contentStartEvents.length?contentStartEvents[contentIndex]:null;if(contentStartEvent&&contentStartEvent.start<startEvent.start){contentIndex++;continue;}
+const navEvent=navIndex<navigationEvents.length?navigationEvents[navIndex]:null;if(navEvent&&navEvent.start<startEvent.start){navIndex++;continue;}
+const fcpEvent=firstContentfulPaintEvents[fcpIndex];if(fcpEvent.start<startEvent.start){fcpIndex++;continue;}
+loopStartIndex++;if(fcpIndex<2){continue;}
+messageLoopStartHistogram.addSample(startEvent.duration,{events:new tr.v.d.RelatedEventSet([startEvent])});if(contentStartEvent){contentStartHistogram.addSample(contentStartEvent.start-startEvent.start,{events:new tr.v.d.RelatedEventSet([startEvent,contentStartEvent])});}
+if(navEvent){navigationStartHistogram.addSample(navEvent.start-startEvent.start,{events:new tr.v.d.RelatedEventSet([startEvent,navEvent])});navigationCommitHistogram.addSample(navEvent.end-startEvent.start,{events:new tr.v.d.RelatedEventSet([startEvent,navEvent])});}
+firstContentfulPaintHistogram.addSample(fcpEvent.end-startEvent.start,{events:new tr.v.d.RelatedEventSet([startEvent,fcpEvent])});}}
 tr.metrics.MetricRegistry.register(androidStartupMetric);return{androidStartupMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS=2000;const MIN_DRAW_DELAY_IN_MS=80;const MAX_DRAW_DELAY_IN_MS=2000;function findProcess(processName,model){for(const pid in model.processes){const process=model.processes[pid];if(process.name===processName){return process;}}
 return undefined;}
 function findThreads(process,threadPrefix){if(process===undefined)return undefined;const threads=[];for(const tid in process.threads){const thread=process.threads[tid];if(thread.name.startsWith(threadPrefix)){threads.push(thread);}}
@@ -8151,8 +8319,10 @@
 function topGarbageCollectionEventName(event){if(event.title===FULL_GC_EVENT){if(findParent(event,isLowMemoryEvent)){return LOW_MEMORY_MARK_COMPACTOR;}}
 return TOP_GC_EVENTS[event.title];}
 function subGarbageCollectionEventName(event){const topEvent=findParent(event,isTopGarbageCollectionEvent);const prefix=topEvent?topGarbageCollectionEventName(topEvent):'unknown';const name=event.title.replace('V8.GC_MC_','').replace('V8.GC_SCAVENGER_','').replace('V8.GC_','').replace(/_/g,'-').toLowerCase();return prefix+'-'+name;}
-function groupAndProcessEvents(model,filterCallback,nameCallback,processCallback){const nameToEvents={};for(const event of model.getDescendantEvents()){if(!filterCallback(event))continue;const name=nameCallback(event);nameToEvents[name]=nameToEvents[name]||[];nameToEvents[name].push(event);}
-for(const[name,events]of Object.entries(nameToEvents)){processCallback(name,events);}}
+function jsExecutionThreads(model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);let threads=[];for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){if(rendererHelper.isChromeTracingUI)continue;threads.push(rendererHelper.mainThread);threads=threads.concat(rendererHelper.dedicatedWorkerThreads);threads=threads.concat(rendererHelper.foregroundWorkerThreads);}
+return threads;}
+function groupAndProcessEvents(model,filterCallback,groupCallback,processCallback){const groupToEvents={};const threads=jsExecutionThreads(model);for(const thread of threads){for(const event of thread.sliceGroup.childEvents()){if(!filterCallback(event))continue;const group=groupCallback(event);groupToEvents[group]=groupToEvents[group]||[];groupToEvents[group].push(event);}}
+for(const[group,events]of Object.entries(groupToEvents)){processCallback(group,events);}}
 function unionOfIntervals(intervals){if(intervals.length===0)return[];return tr.b.math.mergeRanges(intervals.map(x=>{return{min:x.start,max:x.end};}),1e-6,function(ranges){return{start:ranges.reduce((acc,x)=>Math.min(acc,x.min),ranges[0].min),end:ranges.reduce((acc,x)=>Math.max(acc,x.max),ranges[0].max)};});}
 function hasV8Stats(globalMemoryDump){let v8stats=undefined;globalMemoryDump.iterateContainerDumps(function(dump){v8stats=v8stats||dump.getMemoryAllocatorDumpByFullName('v8');});return!!v8stats;}
 function rangeForMemoryDumps(model){const startOfFirstDumpWithV8=model.globalMemoryDumps.filter(hasV8Stats).reduce((start,dump)=>Math.min(start,dump.start),Infinity);if(startOfFirstDumpWithV8===Infinity)return new tr.b.math.Range();return tr.b.math.Range.fromExplicitRange(startOfFirstDumpWithV8,Infinity);}
@@ -8161,31 +8331,32 @@
 function mutatorUtilization(start,end,timeWindow,intervals){const mu=new tr.b.math.PiecewiseLinearFunction();if(end-start<=timeWindow){return mu;}
 if(intervals.length===0){mu.push(start,1.0,end-timeWindow,1.0);return mu;}
 intervals=unionOfIntervals(intervals);const points=[];for(const interval of intervals){points.push({position:interval.start,delta:1});points.push({position:interval.end,delta:-1});}
-points.sort((a,b)=>a.position-b.position);points.push({position:end,delta:0});const left=new WindowEndpoint(start,points);const right=new WindowEndpoint(start,points);while(right.position-left.position<timeWindow){right.advance(timeWindow-(right.position-left.position));}
+points.sort((a,b)=>a.position-b.position);points.push({position:end,delta:0});const left=new WindowEndpoint(start,points);const right=new WindowEndpoint(start,points);const EPSILON=1e-6;while(right.position-left.position<timeWindow-EPSILON){right.advance(timeWindow-(right.position-left.position));}
 while(right.lastIndex<points.length){const distanceUntilNextPoint=Math.min(left.distanceUntilNextPoint,right.distanceUntilNextPoint);const position1=left.position;const value1=right.cummulativePause-left.cummulativePause;left.advance(distanceUntilNextPoint);right.advance(distanceUntilNextPoint);if(distanceUntilNextPoint>0){const position2=left.position;const value2=right.cummulativePause-left.cummulativePause;mu.push(position1,1.0-value1/timeWindow,position2,1.0-value2/timeWindow);}}
 return mu;}
-function addMutatorUtilization(metricName,eventFilter,timeWindows,rendererHelpers,histograms){const histogramMap=new Map();for(const timeWindow of timeWindows){const histogramOptions={avg:false,count:false,max:false,min:true,std:false,sum:false};const histogram=histograms.createHistogram(`${metricName}-${timeWindow}ms_window`,tr.b.Unit.byName.normalizedPercentage_biggerIsBetter,[],histogramOptions);histogramMap.set(timeWindow,histogram);}
+function addMutatorUtilization(metricName,eventFilter,timeWindows,rendererHelpers,histograms){const histogramMap=new Map();for(const timeWindow of timeWindows){const summaryOptions={avg:false,count:false,max:false,min:true,std:false,sum:false};const description=`The minimum mutator utilization in ${timeWindow}ms time window`;const histogram=histograms.createHistogram(`${metricName}-${timeWindow}ms_window`,tr.b.Unit.byName.normalizedPercentage_biggerIsBetter,[],{summaryOptions,description});histogramMap.set(timeWindow,histogram);}
 for(const rendererHelper of rendererHelpers){if(rendererHelper.isChromeTracingUI)continue;const pauses=[];for(const event of rendererHelper.mainThread.sliceGroup.childEvents()){if(eventFilter(event)&&event.end>event.start){pauses.push({start:event.start,end:event.end});}}
 pauses.sort((a,b)=>a.start-b.start);const start=rendererHelper.mainThread.bounds.min;const end=rendererHelper.mainThread.bounds.max;for(const timeWindow of timeWindows){const mu=mutatorUtilization(start,end,timeWindow,pauses);histogramMap.get(timeWindow).addSample(mu.min);}}}
-return{addMutatorUtilization,findParent,forcedGCEventName,groupAndProcessEvents,isForcedGarbageCollectionEvent,isFullMarkCompactorEvent,isGarbageCollectionEvent,isIdleTask,isIncrementalMarkingEvent,isLatencyMarkCompactorEvent,isLowMemoryEvent,isMarkCompactorSummaryEvent,isMarkCompactorMarkingSummaryEvent,isMemoryMarkCompactorEvent,isNotForcedMarkCompactorEvent,isNotForcedTopGarbageCollectionEvent,isNotForcedSubGarbageCollectionEvent,isScavengerEvent,isSubGarbageCollectionEvent,isTopGarbageCollectionEvent,isTopV8ExecuteEvent,isV8Event,isV8ExecuteEvent,isV8RCSEvent,isCompileRCSCategory,isCompileOptimizeRCSCategory,isCompileUnoptimizeRCSCategory,isCompileParseRCSCategory,mutatorUtilization,rangeForMemoryDumps,subGarbageCollectionEventName,topGarbageCollectionEventName,unionOfIntervals,};});'use strict';tr.exportTo('tr.metrics.blink',function(){const BLINK_TOP_GC_EVENTS={'BlinkGC.AtomicPhase':'blink-gc-atomic-phase','BlinkGC.CompleteSweep':'blink-gc-complete-sweep','BlinkGC.IncrementalMarkingStartMarking':'blink-gc-incremental-start','BlinkGC.IncrementalMarkingStep':'blink-gc-incremental-step','BlinkGC.LazySweepInIdle':'blink-gc-lazy-sweep-idle','BlinkGC.LazySweepOnAllocation':'blink-gc-lazy-sweep-allocation'};function blinkGarbageCollectionEventName(event){return BLINK_TOP_GC_EVENTS[event.title];}
-function isNonForcedBlinkGarbageCollectionEvent(event){return event.title in BLINK_TOP_GC_EVENTS&&(!event.args||!event.args.forced)&&!tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event);}
+return{addMutatorUtilization,findParent,forcedGCEventName,groupAndProcessEvents,isForcedGarbageCollectionEvent,isFullMarkCompactorEvent,isGarbageCollectionEvent,isIdleTask,isIncrementalMarkingEvent,isLatencyMarkCompactorEvent,isLowMemoryEvent,isMarkCompactorSummaryEvent,isMarkCompactorMarkingSummaryEvent,isMemoryMarkCompactorEvent,isNotForcedMarkCompactorEvent,isNotForcedTopGarbageCollectionEvent,isNotForcedSubGarbageCollectionEvent,isScavengerEvent,isSubGarbageCollectionEvent,isTopGarbageCollectionEvent,isTopV8ExecuteEvent,isV8Event,isV8ExecuteEvent,isV8RCSEvent,isCompileRCSCategory,isCompileOptimizeRCSCategory,isCompileUnoptimizeRCSCategory,isCompileParseRCSCategory,mutatorUtilization,rangeForMemoryDumps,subGarbageCollectionEventName,topGarbageCollectionEventName,unionOfIntervals,};});'use strict';tr.exportTo('tr.metrics.blink',function(){const BLINK_TOP_GC_FOREGROUND_SWEEPING_EVENTS={'BlinkGC.CompleteSweep':'blink-gc-complete-sweep','BlinkGC.LazySweepInIdle':'blink-gc-sweep-task-foreground','BlinkGC.LazySweepOnAllocation':'blink-gc-sweep-allocation'};const BLINK_TOP_GC_BACKGROUND_SWEEPING_EVENTS={'BlinkGC.ConcurrentSweep':'blink-gc-sweep-task-background'};const BLINK_TOP_GC_EVENTS=Object.assign({'BlinkGC.AtomicPauseMarkEpilogue':'blink-gc-atomic-pause-mark-epilogue','BlinkGC.AtomicPauseMarkPrologue':'blink-gc-atomic-pause-mark-prologue','BlinkGC.AtomicPauseMarkRoots':'blink-gc-atomic-pause-mark-roots','BlinkGC.AtomicPauseMarkTransitiveClosure':'blink-gc-atomic-pause-mark-transitive-closure','BlinkGC.AtomicPauseSweepAndCompact':'blink-gc-atomic-pause-sweep-and-compact','BlinkGC.IncrementalMarkingStartMarking':'blink-gc-incremental-start','BlinkGC.IncrementalMarkingStep':'blink-gc-incremental-step'},BLINK_TOP_GC_FOREGROUND_SWEEPING_EVENTS);const ATOMIC_PAUSE_EVENTS=['BlinkGC.AtomicPauseMarkEpilogue','BlinkGC.AtomicPauseMarkPrologue','BlinkGC.AtomicPauseMarkRoots','BlinkGC.AtomicPauseMarkTransitiveClosure','BlinkGC.AtomicPauseSweepAndCompact'];function blinkGarbageCollectionEventName(event){return BLINK_TOP_GC_EVENTS[event.title];}
+function isNonForcedEvent(event){return(!event.args||!event.args.forced)&&!tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event);}
+function isNonForcedBlinkGarbageCollectionEvent(event){return event.title in BLINK_TOP_GC_EVENTS&&isNonForcedEvent(event);}
+function isNonForcedBlinkGarbageCollectionAtomicPauseEvent(event){return ATOMIC_PAUSE_EVENTS.includes(event.title)&&isNonForcedEvent(event);}
+function isNonForcedBlinkGarbageCollectionForegroundSweepingEvent(event){return event.title in BLINK_TOP_GC_FOREGROUND_SWEEPING_EVENTS&&isNonForcedEvent(event);}
+function isNonForcedBlinkGarbageCollectionBackgroundSweepingEvent(event){return event.title in BLINK_TOP_GC_BACKGROUND_SWEEPING_EVENTS&&isNonForcedEvent(event);}
 function isNonNestedNonForcedBlinkGarbageCollectionEvent(event){return isNonForcedBlinkGarbageCollectionEvent(event)&&!tr.metrics.v8.utils.findParent(event,tr.metrics.v8.utils.isGarbageCollectionEvent);}
-function blinkGcMetric(histograms,model){addDurationOfTopEvents(histograms,model);addTotalDurationOfTopEvents(histograms,model);addIdleTimesOfTopEvents(histograms,model);addTotalIdleTimesOfTopEvents(histograms,model);addTotalDurationOfBlinkAndV8TopEvents(histograms,model);}
-tr.metrics.MetricRegistry.register(blinkGcMetric);const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const percentage_biggerIsBetter=tr.b.Unit.byName.normalizedPercentage_biggerIsBetter;const CUSTOM_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,20,200).addExponentialBins(200,100);function createNumericForTopEventTime(name){const n=new tr.v.Histogram(name,timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);n.customizeSummaryOptions({avg:true,count:true,max:true,min:false,std:true,sum:true,percentile:[0.90]});return n;}
+function blinkGcMetric(histograms,model){addDurationOfTopEvents(histograms,model);addDurationOfAtomicPause(histograms,model);addTotalDurationOfTopEvents(histograms,model);addTotalDurationOfBlinkAndV8TopEvents(histograms,model);addTotalDurationOfForegroundSweeping(histograms,model);addTotalDurationOfBackgroundSweeping(histograms,model);}
+tr.metrics.MetricRegistry.register(blinkGcMetric);const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const CUSTOM_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,20,200).addExponentialBins(200,100);function createNumericForTopEventTime(name){const n=new tr.v.Histogram(name,timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);n.customizeSummaryOptions({avg:true,count:true,max:true,min:false,std:true,sum:true,percentile:[0.90]});return n;}
 function createNumericForTotalEventTime(name){const n=new tr.v.Histogram(name,timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);n.customizeSummaryOptions({avg:false,count:true,max:false,min:false,std:false,sum:true,percentile:[0.90]});return n;}
 function createNumericForUnifiedEventTime(name){const n=new tr.v.Histogram(name,timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);n.customizeSummaryOptions({avg:false,count:true,max:true,min:false,std:false,sum:true,percentile:[0.90]});return n;}
-function createNumericForIdleTime(name){const n=new tr.v.Histogram(name,timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);n.customizeSummaryOptions({avg:true,count:false,max:true,min:false,std:false,sum:true,percentile:[]});return n;}
-function createPercentage(name,numerator,denominator){const histogram=new tr.v.Histogram(name,percentage_biggerIsBetter);if(denominator===0){histogram.addSample(0);}else{histogram.addSample(numerator/denominator);}
-return histogram;}
 function addDurationOfTopEvents(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionEvent,blinkGarbageCollectionEventName,function(name,events){const cpuDuration=createNumericForTopEventTime(name);for(const event of events){cpuDuration.addSample(event.cpuDuration);}
 histograms.addHistogram(cpuDuration);});}
+function addDurationOfAtomicPause(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionAtomicPauseEvent,event=>event.args.epoch,function(group,events){const cpuDuration=createNumericForTopEventTime('blink-gc-atomic-pause');cpuDuration.addSample(events.reduce((acc,current)=>acc+current.cpuDuration,0));histograms.addHistogram(cpuDuration);});}
 function addTotalDurationOfTopEvents(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionEvent,event=>'blink-gc-total',function(name,events){const cpuDuration=createNumericForTotalEventTime(name);for(const event of events){cpuDuration.addSample(event.cpuDuration);}
 histograms.addHistogram(cpuDuration);});}
-function addIdleTimesOfTopEvents(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionEvent,blinkGarbageCollectionEventName,function(name,events){addIdleTimes(histograms,model,name,events);});}
-function addTotalIdleTimesOfTopEvents(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionEvent,event=>'blink-gc-total',function(name,events){addIdleTimes(histograms,model,name,events);});}
-function addIdleTimes(histograms,model,name,events){const cpuDuration=createNumericForIdleTime(name+'_cpu');const insideIdle=createNumericForIdleTime(name+'_inside_idle');const outsideIdle=createNumericForIdleTime(name+'_outside_idle');const idleDeadlineOverrun=createNumericForIdleTime(name+'_idle_deadline_overrun');for(const event of events){const idleTask=tr.metrics.v8.utils.findParent(event,tr.metrics.v8.utils.isIdleTask);let inside=0;let overrun=0;if(idleTask){const allottedTime=idleTask.args.allotted_time_ms;if(event.duration>allottedTime){overrun=event.duration-allottedTime;inside=event.cpuDuration*allottedTime/event.duration;}else{inside=event.cpuDuration;}}
-cpuDuration.addSample(event.cpuDuration);insideIdle.addSample(inside);outsideIdle.addSample(event.cpuDuration-inside);idleDeadlineOverrun.addSample(overrun);}
-histograms.addHistogram(idleDeadlineOverrun);histograms.addHistogram(outsideIdle);const percentage=createPercentage(name+'_percentage_idle',insideIdle.sum,cpuDuration.sum);histograms.addHistogram(percentage);}
+function addTotalDurationOfForegroundSweeping(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionForegroundSweepingEvent,event=>'blink-gc-sweep-foreground',function(name,events){const cpuDuration=createNumericForTotalEventTime(name);for(const event of events){cpuDuration.addSample(event.cpuDuration);}
+histograms.addHistogram(cpuDuration);});}
+function addTotalDurationOfBackgroundSweeping(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isNonForcedBlinkGarbageCollectionBackgroundSweepingEvent,event=>'blink-gc-sweep-background',function(name,events){const cpuDuration=createNumericForTotalEventTime(name);for(const event of events){cpuDuration.addSample(event.cpuDuration);}
+histograms.addHistogram(cpuDuration);});}
 function isV8OrBlinkTopLevelGarbageCollectionEvent(event){return tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent(event)||isNonNestedNonForcedBlinkGarbageCollectionEvent(event);}
 function addTotalDurationOfBlinkAndV8TopEvents(histograms,model){tr.metrics.v8.utils.groupAndProcessEvents(model,isV8OrBlinkTopLevelGarbageCollectionEvent,event=>'unified-gc-total',function(name,events){const cpuDuration=createNumericForUnifiedEventTime(name);for(const event of events){cpuDuration.addSample(event.cpuDuration);}
 histograms.addHistogram(cpuDuration);});}
@@ -8214,18 +8385,19 @@
 function cpuProcessMetric(histograms,model){const snapshots=getCpuSnapshotsFromModel(model);const processNumerics=buildNumericsFromSnapshots(snapshots);for(const[processName,processData]of processNumerics){const numeric=processData.numeric;const missingSnapshotCount=snapshots.length-numeric.numValues;for(let i=0;i<missingSnapshotCount;i++){numeric.addSample(0);}
 numeric.diagnostics.set('paths',new
 tr.v.d.GenericSet([...processData.paths]));histograms.addHistogram(numeric);}}
-tr.metrics.MetricRegistry.register(cpuProcessMetric);return{cpuProcessMetric,};});'use strict';tr.exportTo('tr.metrics',function(){function mediaMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(chromeHelper===undefined)return;for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){const mainThread=rendererHelper.mainThread;if(mainThread===undefined)continue;const compositorThread=rendererHelper.compositorThread;const audioThreads=rendererHelper.process.findAllThreadsNamed('AudioOutputDevice');if(compositorThread===undefined&&audioThreads.length===0)continue;const processData=new PerProcessData();processData.recordPlayStarts(mainThread);if(!processData.hasPlaybacks)continue;if(compositorThread!==undefined){processData.calculateTimeToVideoPlays(compositorThread);processData.calculateDroppedFrameCounts(compositorThread);}
+tr.metrics.MetricRegistry.register(cpuProcessMetric);return{cpuProcessMetric,};});'use strict';tr.exportTo('tr.metrics',function(){function mediaMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(chromeHelper===undefined)return;for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){const mainThread=rendererHelper.mainThread;if(mainThread===undefined)continue;const videoThreads=rendererHelper.process.findAllThreadsMatching(thread=>(thread.name?thread.name.startsWith('ThreadPoolSingleThreadSharedForegroundBlocking'):false));const compositorThread=rendererHelper.compositorThread;if(compositorThread!==undefined){videoThreads.push(compositorThread);}
+const audioThreads=rendererHelper.process.findAllThreadsNamed('AudioOutputDevice');if(audioThreads.length===0&&videoThreads.length===0)continue;const processData=new PerProcessData();processData.recordPlayStarts(mainThread);if(!processData.hasPlaybacks)continue;if(videoThreads.length!==0){processData.calculateTimeToVideoPlays(videoThreads);processData.calculateDroppedFrameCounts(videoThreads);}
 if(audioThreads.length!==0){processData.calculateTimeToAudioPlays(audioThreads);}
 processData.calculateSeekTimes(mainThread);processData.calculateBufferingTimes(mainThread);processData.addMetricToHistograms(histograms);}}
 class PerProcessData{constructor(){this.playbackIdToDataMap_=new Map();}
 recordPlayStarts(mainThread){for(const event of mainThread.sliceGroup.getDescendantEvents()){if(event.title==='WebMediaPlayerImpl::DoLoad'){const id=event.args.id;if(this.playbackIdToDataMap_.has(id)){throw new Error('Unexpected multiple initialization of a media playback');}
 this.playbackIdToDataMap_.set(id,new PerPlaybackData(event.start));}}}
 get hasPlaybacks(){return this.playbackIdToDataMap_.size>0;}
-calculateTimeToVideoPlays(compositorThread){for(const event of compositorThread.sliceGroup.getDescendantEvents()){if(event.title==='VideoRendererImpl::Render'){this.getPerPlaybackObject_(event.args.id).processVideoRenderTime(event.start);}}}
+calculateTimeToVideoPlays(videoThreads){for(const thread of videoThreads){for(const event of thread.sliceGroup.getDescendantEvents()){if(event.title==='VideoRendererImpl::Render'){this.getPerPlaybackObject_(event.args.id).processVideoRenderTime(event.start);}}}}
 calculateTimeToAudioPlays(audioThreads){for(const audioThread of audioThreads){for(const event of audioThread.sliceGroup.getDescendantEvents()){if(event.title==='AudioRendererImpl::Render'){this.getPerPlaybackObject_(event.args.id).processAudioRenderTime(event.start);}}}}
 calculateSeekTimes(mainThread){for(const event of mainThread.sliceGroup.getDescendantEvents()){if(event.title==='WebMediaPlayerImpl::DoSeek'){this.getPerPlaybackObject_(event.args.id).processDoSeek(event.args.target,event.start);}else if(event.title==='WebMediaPlayerImpl::OnPipelineSeeked'){this.getPerPlaybackObject_(event.args.id).processOnPipelineSeeked(event.args.target,event.start);}else if(event.title==='WebMediaPlayerImpl::BufferingHaveEnough'){this.getPerPlaybackObject_(event.args.id).processBufferingHaveEnough(event.start);}}}
 calculateBufferingTimes(mainThread){for(const event of mainThread.sliceGroup.getDescendantEvents()){if(event.title==='WebMediaPlayerImpl::OnEnded'){this.getPerPlaybackObject_(event.args.id).processOnEnded(event.start,event.args.duration);}}}
-calculateDroppedFrameCounts(compositorThread){for(const event of compositorThread.sliceGroup.getDescendantEvents()){if(event.title==='VideoFramesDropped'){this.getPerPlaybackObject_(event.args.id).processVideoFramesDropped(event.args.count);}}}
+calculateDroppedFrameCounts(videoThreads){for(const thread of videoThreads){for(const event of thread.sliceGroup.getDescendantEvents()){if(event.title==='VideoFramesDropped'){this.getPerPlaybackObject_(event.args.id).processVideoFramesDropped(event.args.count);}}}}
 addMetricToHistograms(histograms){for(const[id,playbackData]of this.playbackIdToDataMap_){playbackData.addMetricToHistograms(histograms);}}
 getPerPlaybackObject_(playbackId){let perPlaybackObject=this.playbackIdToDataMap_.get(playbackId);if(perPlaybackObject===undefined){perPlaybackObject=new PerPlaybackData(undefined);this.playbackIdToDataMap_.set(playbackId,perPlaybackObject);}
 return perPlaybackObject;}}
@@ -8251,40 +8423,62 @@
 addMetricToHistograms(histograms){this.addSample_(histograms,'time_to_video_play',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,this.timeToVideoPlay);this.addSample_(histograms,'time_to_audio_play',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,this.timeToAudioPlay);this.addSample_(histograms,'dropped_frame_count',tr.b.Unit.byName.count_smallerIsBetter,this.droppedFrameCount);for(const[key,value]of this.seekTimes.entries()){const keyString=key.toString().replace('.','_');this.addSample_(histograms,'pipeline_seek_time_'+keyString,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,value.pipelineSeekTime);this.addSample_(histograms,'seek_time_'+keyString,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,value.seekTime);}
 this.addSample_(histograms,'buffering_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,this.bufferingTime);}
 addSample_(histograms,name,unit,sample){if(sample===undefined)return;const histogram=histograms.getHistogramNamed(name);if(histogram===undefined){histograms.createHistogram(name,unit,sample);}else{histogram.addSample(sample);}}}
-tr.metrics.MetricRegistry.register(mediaMetric);return{mediaMetric,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const UNKNOWN_THREAD_NAME='Unknown';const CATEGORY_THREAD_MAP=new Map();CATEGORY_THREAD_MAP.set('all',[/.*/]);CATEGORY_THREAD_MAP.set('browser',[/^Browser Compositor$/,/^CrBrowserMain$/]);CATEGORY_THREAD_MAP.set('display_compositor',[/^VizCompositorThread$/]);CATEGORY_THREAD_MAP.set('fast_path',[/^Browser Compositor$/,/^Chrome_InProcGpuThread$/,/^Compositor$/,/^CrBrowserMain$/,/^CrGpuMain$/,/IOThread/,/^VizCompositorThread$/]);CATEGORY_THREAD_MAP.set('gpu',[/^Chrome_InProcGpuThread$/,/^CrGpuMain$/]);CATEGORY_THREAD_MAP.set('io',[/IOThread/]);CATEGORY_THREAD_MAP.set('raster',[/CompositorTileWorker/]);CATEGORY_THREAD_MAP.set('renderer_compositor',[/^Compositor$/]);CATEGORY_THREAD_MAP.set('renderer_main',[/^CrRendererMain$/]);const ALL_CATEGORIES=[...CATEGORY_THREAD_MAP.keys(),'other'];function addValueToMap_(map,key,value){const oldValue=map.get(key)||0;map.set(key,oldValue+value);}
-function*getCategories_(threadName){let isOther=true;for(const[category,regexps]of CATEGORY_THREAD_MAP){for(const regexp of regexps){if(regexp.test(threadName)){if(category!=='all')isOther=false;yield category;break;}}}
+tr.metrics.MetricRegistry.register(mediaMetric);return{mediaMetric,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const UNKNOWN_THREAD_NAME='Unknown';const CATEGORY_THREAD_MAP=new Map();CATEGORY_THREAD_MAP.set('total_all',[/.*/]);CATEGORY_THREAD_MAP.set('browser',[/^Browser Compositor$/,/^CrBrowserMain$/]);CATEGORY_THREAD_MAP.set('display_compositor',[/^VizCompositorThread$/]);CATEGORY_THREAD_MAP.set('GPU',[/^Chrome_InProcGpuThread$/,/^CrGpuMain$/]);CATEGORY_THREAD_MAP.set('IO',[/IOThread/]);CATEGORY_THREAD_MAP.set('raster',[/CompositorTileWorker/]);CATEGORY_THREAD_MAP.set('renderer_compositor',[/^Compositor$/]);CATEGORY_THREAD_MAP.set('renderer_main',[/^CrRendererMain$/]);CATEGORY_THREAD_MAP.set('total_rendering',[/^Browser Compositor$/,/^Chrome_InProcGpuThread$/,/^Compositor$/,/CompositorTileWorker/,/^CrBrowserMain$/,/^CrGpuMain$/,/^CrRendererMain$/,/IOThread/,/^VizCompositorThread$/]);const ALL_CATEGORIES=[...CATEGORY_THREAD_MAP.keys(),'other'];function addValueToMap_(map,key,value){const oldValue=map.get(key)||0;map.set(key,oldValue+value);}
+function categoryShouldHaveBreakdown(category){return category==='total_all'||category==='total_rendering';}
+function*getCategories_(threadName){let isOther=true;for(const[category,regexps]of CATEGORY_THREAD_MAP){for(const regexp of regexps){if(regexp.test(threadName)){if(category!=='total_all')isOther=false;yield category;break;}}}
 if(isOther)yield'other';}
 function isSubset_(regexps1,regexps2){for(const r1 of regexps1){if(regexps2.find(r2=>r2.toString()===r1.toString())===undefined){return false;}}
 return true;}
-function addCpuUtilizationHistograms(histograms,model,segments,segmentName,shouldNormalize){const histogramMap=new Map();for(const category of ALL_CATEGORIES){const histogram=histograms.createHistogram(`thread_${category}_cpu_time_per_${segmentName}_tbmv2`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{binBoundaries:tr.v.HistogramBinBoundaries.createExponential(1,50,20),description:`CPU cores per ${segmentName} of a thread group`,summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histogramMap.set(category,histogram);}
+function addCpuUtilizationHistograms(histograms,model,segments,shouldNormalize,segmentCostFunc,histogramNameFunc,description,unit){if(!unit)unit=tr.b.Unit.byName.unitlessNumber;const histogramMap=new Map();for(const category of ALL_CATEGORIES){const histogram=histograms.createHistogram(histogramNameFunc(category),unit,[],{binBoundaries:tr.v.HistogramBinBoundaries.createExponential(1,50,20),description,summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histogramMap.set(category,histogram);}
 for(const[category,regexps]of CATEGORY_THREAD_MAP){const relatedCategories=new tr.v.d.RelatedNameMap();const histogram=histogramMap.get(category);for(const[otherCategory,otherRegexps]of CATEGORY_THREAD_MAP){if(otherCategory===category)continue;if(category!=='all'&&!isSubset_(otherRegexps,regexps))continue;const otherHistogram=histogramMap.get(otherCategory);relatedCategories.set(otherCategory,otherHistogram.name);}
 if([...relatedCategories.values()].length>0){histogram.diagnostics.set('breakdown',relatedCategories);}}
-for(const segment of segments){const threadValues=new Map();for(const thread of model.getAllThreads()){addValueToMap_(threadValues,thread.name||UNKNOWN_THREAD_NAME,thread.getCpuTimeForRange(segment.boundsRange));}
-const categoryValues=new Map();const breakdowns=new Map();for(const[threadName,coresPerSec]of threadValues){for(const category of getCategories_(threadName)){addValueToMap_(categoryValues,category,coresPerSec);if(!breakdowns.has(category)){breakdowns.set(category,new tr.v.d.Breakdown());}
+for(const segment of segments){const threadValues=new Map();for(const thread of model.getAllThreads()){addValueToMap_(threadValues,thread.name||UNKNOWN_THREAD_NAME,segmentCostFunc(thread,segment));}
+const categoryValues=new Map();const breakdowns=new Map();for(const[threadName,coresPerSec]of threadValues){for(const category of getCategories_(threadName)){addValueToMap_(categoryValues,category,coresPerSec);if(!categoryShouldHaveBreakdown(category))continue;if(!breakdowns.has(category)){breakdowns.set(category,new tr.v.d.Breakdown());}
 breakdowns.get(category).set(threadName,coresPerSec);}}
 for(const category of ALL_CATEGORIES){let value=categoryValues.get(category)||0;if(shouldNormalize)value/=segment.duration;const diagnostics=new tr.v.d.DiagnosticMap();const breakdown=breakdowns.get(category);if(breakdown)diagnostics.set('breakdown',breakdown);const histogram=histogramMap.get(category);histogram.addSample(value,diagnostics);}}}
-const SUMMARY_OPTIONS={percentile:[0.90,0.95],};return{addCpuUtilizationHistograms,SUMMARY_OPTIONS,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const DISPLAY_EVENT='BenchmarkInstrumentation::DisplayRenderingStats';const DRM_EVENT='DrmEventFlipComplete';const SURFACE_FLINGER_EVENT='vsync_before';const COMPOSITOR_FRAME_PRESENTED_EVENT='FramePresented';const MIN_FRAME_LENGTH=0.5;const PAUSE_THRESHOLD=20;const ASH_ENVIRONMENT='ash';const BROWSER_ENVIRONMENT='browser';function getDisplayCompositorPresentationEvents_(modelHelper){if(!modelHelper||!modelHelper.browserProcess)return[];let events=[];if(modelHelper.surfaceFlingerProcess){events=[...modelHelper.surfaceFlingerProcess.findTopmostSlicesNamed(SURFACE_FLINGER_EVENT)];if(events.length>0)return events;}
+const SUMMARY_OPTIONS={percentile:[0.90,0.95],ci:[0.95],};return{addCpuUtilizationHistograms,SUMMARY_OPTIONS,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const PRESENT_EVENT='Display::FrameDisplayed';const DISPLAY_EVENT='BenchmarkInstrumentation::DisplayRenderingStats';const DRM_EVENT='DrmEventFlipComplete';const SURFACE_FLINGER_EVENT='vsync_before';const COMPOSITOR_FRAME_PRESENTED_EVENT='FramePresented';const MIN_FRAME_LENGTH=0.5;const MIN_FRAME_COUNT=10;const PAUSE_THRESHOLD=20;const ASH_ENVIRONMENT='ash';const BROWSER_ENVIRONMENT='browser';class FrameEvent{constructor(event){this.event_=event;}
+get eventStart(){return this.event_.start;}
+get frameStart(){if(this.event_.title!==DRM_EVENT)return this.event_.start;const data=this.event_.args.data;const TIME=tr.b.UnitScale.TIME;return tr.b.convertUnit(data['vblank.tv_sec'],TIME.SEC,TIME.MILLI_SEC)+
+tr.b.convertUnit(data['vblank.tv_usec'],TIME.MICRO_SEC,TIME.MILLI_SEC);}
+get event(){return this.event_;}}
+class FrameSegment{constructor(frameEvent,duration){this.frameEvent_=frameEvent;this.duration_=duration;this.segment_=new tr.model.um.Segment(frameEvent.eventStart,duration);this.length_=undefined;}
+updateLength(refreshPeriod){this.length_=this.duration_/refreshPeriod;}
+get segment(){return this.segment_;}
+get boundsRange(){return this.segment_.boundsRange;}
+get length(){return this.length_;}
+get duration(){return this.duration_;}
+get event(){return this.frameEvent_.event;}}
+function getDisplayCompositorPresentationEventsExp_(modelHelper){if(!modelHelper)return[];function findEventsFromProcess(process){const events=[];for(const event of process.findTopmostSlicesNamed(PRESENT_EVENT)){events.push(event);}
+return events;}
+if(modelHelper.gpuHelper){const events=findEventsFromProcess(modelHelper.gpuHelper.process);if(events.length>0)return events;}
+if(!modelHelper.browserProcess)return[];return findEventsFromProcess(modelHelper.browserProcess);}
+function getDisplayCompositorPresentationEvents_(modelHelper){if(!modelHelper||!modelHelper.browserProcess)return[];let events=[];if(modelHelper.surfaceFlingerProcess){events=[...modelHelper.surfaceFlingerProcess.findTopmostSlicesNamed(SURFACE_FLINGER_EVENT)];if(events.length>0)return events;}
 if(modelHelper.gpuHelper){const gpuProcess=modelHelper.gpuHelper.process;events=[...gpuProcess.findTopmostSlicesNamed(DRM_EVENT)];if(events.length>0)return events;events=[...gpuProcess.findTopmostSlicesNamed(DISPLAY_EVENT)];if(events.length>0)return events;}
 return[...modelHelper.browserProcess.findTopmostSlicesNamed(DISPLAY_EVENT)];}
 function getUIPresentationEvents_(modelHelper){if(!modelHelper||!modelHelper.browserProcess)return[];const legacyEvents=[];const eventsByEnvironment={};eventsByEnvironment[ASH_ENVIRONMENT]=[];eventsByEnvironment[BROWSER_ENVIRONMENT]=[];for(const event of modelHelper.browserProcess.findTopmostSlicesNamed(COMPOSITOR_FRAME_PRESENTED_EVENT)){if(!('environment'in event.args)){legacyEvents.push(event);}else{eventsByEnvironment[event.args.environment].push(event);}}
 if(eventsByEnvironment[ASH_ENVIRONMENT].length>0){return eventsByEnvironment[ASH_ENVIRONMENT];}
 if(eventsByEnvironment[BROWSER_ENVIRONMENT].length>0){return eventsByEnvironment[BROWSER_ENVIRONMENT];}
 return legacyEvents;}
-function addSurfaceFlingerHistograms_(histograms,frameSegments,refreshPeriod){let frameLengths=frameSegments.map(x=>x.duration/refreshPeriod);frameLengths=frameLengths.filter(length=>length>=MIN_FRAME_LENGTH);histograms.createHistogram('frame_lengths',tr.b.Unit.byName.unitlessNumber_smallerIsBetter,frameLengths,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,5,20),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Frame times in vsyncs.'});histograms.createHistogram('avg_surface_fps',tr.b.Unit.byName.unitlessNumber_biggerIsBetter,frameLengths.length/tr.b.convertUnit(tr.b.math.Statistics.sum(frameSegments,x=>x.duration),tr.b.UnitScale.TIME.MILLI_SEC,tr.b.UnitScale.TIME.SEC),{description:'Average frames per second.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});let jankCount=0;for(let i=1;i<frameLengths.length;i++){const change=Math.round(frameLengths[i]-frameLengths[i-1]);if(change>0&&change<PAUSE_THRESHOLD)jankCount++;}
-histograms.createHistogram('jank_count',tr.b.Unit.byName.unitlessNumber_smallerIsBetter,jankCount,{description:'Number of changes in frame rate.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});}
-function computeFrameSegments_(timestamps,segments){const frameSegments=[];for(const segment of segments){const filtered=segment.boundsRange.filterArray(timestamps,x=>x[0]);for(let i=1;i<filtered.length;i++){const duration=filtered[i][1]-filtered[i-1][1];frameSegments.push(new tr.model.um.Segment(filtered[i-1][0],duration));}}
+function computeFrameSegments_(events,segments,opt_minFrameCount){const minFrameCount=opt_minFrameCount||MIN_FRAME_COUNT;const frameEvents=events.map(e=>new FrameEvent(e));const frameSegments=[];for(const segment of segments){const filtered=segment.boundsRange.filterArray(frameEvents,x=>x.eventStart);if(filtered.length<minFrameCount)continue;for(let i=1;i<filtered.length;i++){const duration=filtered[i].frameStart-filtered[i-1].frameStart;frameSegments.push(new FrameSegment(filtered[i-1],duration));}}
 return frameSegments;}
-function addBasicFrameTimeHistograms_(histograms,frameSegments,prefix){const frameTimes=frameSegments.map(x=>x.duration);histograms.createHistogram(`${prefix}frame_times`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,frameTimes,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,20),description:'Raw frame times.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram(`${prefix}percentage_smooth`,tr.b.Unit.byName.unitlessNumber_biggerIsBetter,100*tr.b.math.Statistics.sum(frameTimes,(x=>(x<17?1:0)))/frameTimes.length,{description:'Percentage of frames that were hitting 60 FPS.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});}
-function addFrameTimeHistograms(histograms,model,segments){const events=getDisplayCompositorPresentationEvents_(model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper));if(!events)return;const timestamps=events.map(event=>[event.start,event.title!==DRM_EVENT?event.start:(tr.b.convertUnit(event.args.data['vblank.tv_sec'],tr.b.UnitScale.TIME.SEC,tr.b.UnitScale.TIME.MILLI_SEC)+
-tr.b.convertUnit(event.args.data['vblank.tv_usec'],tr.b.UnitScale.TIME.MICRO_SEC,tr.b.UnitScale.TIME.MILLI_SEC))]);const frameSegments=computeFrameSegments_(timestamps,segments);addBasicFrameTimeHistograms_(histograms,frameSegments,'');tr.metrics.rendering.addCpuUtilizationHistograms(histograms,model,frameSegments,'frame',false);for(const metadata of model.metadata){if(metadata.value&&metadata.value.surface_flinger){addSurfaceFlingerHistograms_(histograms,frameSegments,metadata.value.surface_flinger.refresh_period);return;}}}
-function addUIFrameTimeHistograms(histograms,model,segments){const events=getUIPresentationEvents_(model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper));if(!events)return;const timestamps=events.map(event=>[event.start,event.start]);const frameSements=computeFrameSegments_(timestamps,segments);addBasicFrameTimeHistograms_(histograms,frameSements,'ui_');}
-return{addFrameTimeHistograms,addUIFrameTimeHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const BEGIN_SCROLL_UPDATE_COMP_NAME='LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT';const END_COMP_NAME='INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT';function*iterAsyncEvents_(processHelpers,ranges,processEventFn){for(const processHelper of processHelpers){const process=processHelper.process;for(const event of process.getDescendantEventsInSortedRanges(ranges,container=>container instanceof tr.model.AsyncSliceGroup)){yield*processEventFn(event);}}}
-function*processInputLatencyEvent(event){if(!(event instanceof tr.e.cc.InputLatencyAsyncSlice))return;const latency=event.inputLatency;if(latency===undefined)return;yield tr.b.Unit.timestampFromUs(latency);}
+function addBasicFrameTimeHistograms_(histograms,frameSegments,prefix){const frameTimes=(frameSegments.length===0)?[0]:frameSegments.map(x=>x.duration);histograms.createHistogram(`${prefix}frame_times`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,frameTimes,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,20),description:'Raw frame times.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram(`${prefix}percentage_smooth`,tr.b.Unit.byName.unitlessNumber_biggerIsBetter,100*tr.b.math.Statistics.sum(frameTimes,(x=>(x<17?1:0)))/frameTimes.length,{description:'Percentage of frames that were hitting 60 FPS.',summaryOptions:{},});}
+function addFrameTimeHistograms(histograms,model,segments,opt_minFrameCount){const minFrameCount=opt_minFrameCount||MIN_FRAME_COUNT;const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const events=getDisplayCompositorPresentationEvents_(modelHelper);if(!events)return;addFrameTimeHistogramsHelper(histograms,model,segments,events,'',true,minFrameCount);const eventsExp=getDisplayCompositorPresentationEventsExp_(modelHelper);if(eventsExp&&eventsExp.length>0){addFrameTimeHistogramsHelper(histograms,model,segments,eventsExp,'exp_',minFrameCount);}}
+function addFrameTimeHistogramsHelper(histograms,model,segments,events,prefix,addCpuMetrics,minFrameCount){const frameSegments=computeFrameSegments_(events,segments,minFrameCount);addBasicFrameTimeHistograms_(histograms,frameSegments,prefix+'');if(addCpuMetrics){tr.metrics.rendering.addCpuUtilizationHistograms(histograms,model,frameSegments,false,(thread,segment)=>thread.getCpuTimeForRange(segment.boundsRange),category=>`thread_${category}_cpu_time_per_frame`,'CPU cores of a thread group per frame',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);tr.metrics.rendering.addCpuUtilizationHistograms(histograms,model,frameSegments,false,(thread,segment)=>thread.getNumToplevelSlicesForRange(segment.boundsRange),category=>`tasks_per_frame_${category}`,'Number of tasks of a thread group per frame',tr.b.Unit.byName.unitlessNumber_smallerIsBetter);}
+const refreshPeriod=getRefreshPeriod(model,frameSegments.map(fs=>fs.boundsRange));frameSegments.forEach(fs=>fs.updateLength(refreshPeriod));const validFrames=frameSegments.filter(fs=>fs.length>=MIN_FRAME_LENGTH);const totalFrameDuration=tr.b.math.Statistics.sum(frameSegments,fs=>fs.duration);addJankCountHistograms(histograms,validFrames,prefix);const frameLengths=validFrames.map(frame=>frame.length);histograms.createHistogram(prefix+'frame_lengths',tr.b.Unit.byName.unitlessNumber_smallerIsBetter,frameLengths,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,5,20),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Frame times in vsyncs.'});histograms.createHistogram(prefix+'avg_surface_fps',tr.b.Unit.byName.unitlessNumber_biggerIsBetter,frameLengths.length/tr.b.convertUnit(totalFrameDuration,tr.b.UnitScale.TIME.MILLI_SEC,tr.b.UnitScale.TIME.SEC),{description:'Average frames per second.',summaryOptions:{},});}
+function addUIFrameTimeHistograms(histograms,model,segments,opt_minFrameCount){const minFrameCount=opt_minFrameCount||MIN_FRAME_COUNT;const events=getUIPresentationEvents_(model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper));if(events.length===0)return;const frameSegments=computeFrameSegments_(events,segments,minFrameCount);addBasicFrameTimeHistograms_(histograms,frameSegments,'ui_');}
+function addJankCountHistograms(histograms,validFrames,prefix){const jankEvents=[];for(let i=1;i<validFrames.length;i++){const change=Math.round((validFrames[i].length-validFrames[i-1].length));if(change>0&&change<PAUSE_THRESHOLD){jankEvents.push(validFrames[i].event);}}
+const jankCount=jankEvents.length;const diagnostics=new tr.v.d.DiagnosticMap();diagnostics.set('events',new tr.v.d.RelatedEventSet(jankEvents));diagnostics.set('timestamps',new tr.v.d.GenericSet(jankEvents.map(e=>e.start)));const histogram=histograms.createHistogram(prefix+'jank_count',tr.b.Unit.byName.count_smallerIsBetter,{value:jankCount,diagnostics},{description:'Number of changes in frame rate.',summaryOptions:{},});}
+function getRefreshPeriod(model,ranges){for(const metadata of model.metadata){if(metadata.value&&metadata.value.surface_flinger){return metadata.value.surface_flinger.refresh_period;}}
+const FRAME_LENGTH=1000.0/60;const BEGIN_FRAME_ARGS='Scheduler::BeginFrame';const FRAME_INTERVAL='interval_us';const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){if(rendererHelper.compositorThread===undefined)continue;const slices=rendererHelper.compositorThread.sliceGroup;for(const slice of slices.getDescendantEventsInSortedRanges(ranges)){if(slice.title!==BEGIN_FRAME_ARGS)continue;const data=slice.args.args;if(!(FRAME_INTERVAL in data)){throw new Error(`${FRAME_INTERVAL} is missing`);}
+return tr.b.convertUnit(data[FRAME_INTERVAL],tr.b.UnitScale.TIME.MICRO_SEC,tr.b.UnitScale.TIME.MILLI_SEC);}}
+return FRAME_LENGTH;}
+return{addFrameTimeHistograms,addUIFrameTimeHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const RGB_DECODE_EVENT='ImageFrameGenerator::decode';const YUV_DECODE_EVENT='ImageFrameGenerator::decodeToYUV';const BLINK_GPU_RASTER_DECODE_EVENT='GpuImageDecodeCache::DecodeImage';const BLINK_SOFTWARE_RASTER_DECODE_EVENT='SoftwareImageDecodeCache::'+'DecodeImageInTask';function getImageDecodingEvents_(modelHelper,ranges){if(!modelHelper||!modelHelper.rendererHelpers)return[];const events=[];for(const renderer of Object.values(modelHelper.rendererHelpers)){for(const thread of renderer.rasterWorkerThreads){const slices=thread.sliceGroup;for(const slice of slices.getDescendantEventsInSortedRanges(ranges)){if(slice.title===RGB_DECODE_EVENT||slice.title===YUV_DECODE_EVENT||slice.title===BLINK_GPU_RASTER_DECODE_EVENT||slice.title===BLINK_SOFTWARE_RASTER_DECODE_EVENT){events.push(slice);}}}}
+return events;}
+function addImageDecodeTimeHistograms(histograms,model,segments){const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const decodeEvents=getImageDecodingEvents_(modelHelper,segments.map(s=>s.boundsRange));if(!decodeEvents)return;histograms.createHistogram('rgb_decode_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,decodeEvents.filter(slice=>slice.title===RGB_DECODE_EVENT).map(slice=>slice.cpuDuration),{description:'Duration of the Blink RGB decoding path for a chunk '+'of image data (possibly the whole image).',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram('yuv_decode_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,decodeEvents.filter(slice=>slice.title===YUV_DECODE_EVENT).map(slice=>slice.cpuDuration),{description:'Duration of the Blink YUV decoding path for a '+'chunk of image data (possibly the whole image).',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram('blink_decode_time_gpu_rasterization',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,decodeEvents.filter(slice=>slice.title===BLINK_GPU_RASTER_DECODE_EVENT).map(slice=>slice.cpuDuration),{description:'Duration of decoding and scaling within the '+'GpuImageDecodeCache for a chunk of image data '+'(possibly the whole image)',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram('blink_decode_time_software_rasterization',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,decodeEvents.filter(slice=>slice.title===BLINK_SOFTWARE_RASTER_DECODE_EVENT).map(slice=>slice.cpuDuration),{description:'Duration of decoding and scaling within the '+'SoftwareImageDecodeCache for a chunk of image data '+'(possibly the whole image)',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});}
+return{addImageDecodeTimeHistograms};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const BEGIN_SCROLL_UPDATE_COMP_NAME='LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT';const END_COMP_NAME='INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT';function*iterAsyncEvents_(processHelpers,ranges,processEventFn){for(const processHelper of processHelpers){const process=processHelper.process;for(const event of process.getDescendantEventsInSortedRanges(ranges,container=>container instanceof tr.model.AsyncSliceGroup)){yield*processEventFn(event);}}}
 function*processLatencyEvent(event){if(event.title!=='Latency::ScrollUpdate'||!('data'in event.args)||!(END_COMP_NAME in event.args.data)){return;}
 const data=event.args.data;const endTime=data[END_COMP_NAME].time;if(BEGIN_SCROLL_UPDATE_COMP_NAME in data){yield tr.b.Unit.timestampFromUs(endTime-data[BEGIN_SCROLL_UPDATE_COMP_NAME].time);}else{throw new Error('LatencyInfo has no begin component');}}
-function*processGestureScrollUpdateLatencyEvent(event){if(event.title!=='InputLatency::GestureScrollUpdate')return;if(!(event instanceof tr.e.cc.InputLatencyAsyncSlice)){throw new Error('Gesture scroll update latency event is not an '+'instance of tr.e.cc.InputLatencyAsyncSlice');}
-const latency=event.inputLatency;if(latency===undefined)return;yield[event.start,tr.b.Unit.timestampFromUs(latency)];}
-function addLatencyHistograms(histograms,model,segments){const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!modelHelper)return;const ranges=segments.map(s=>s.boundsRange);const inputEventLatencies=[...iterAsyncEvents_(modelHelper.browserHelpers,ranges,processInputLatencyEvent)];histograms.createHistogram('input_event_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,inputEventLatencies,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,20),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Input event latencies.'});const mainThreadScrollLatencies=[...iterAsyncEvents_(Object.values(modelHelper.rendererHelpers),ranges,processLatencyEvent)];histograms.createHistogram('main_thread_scroll_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,mainThreadScrollLatencies,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,50),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Main thread scroll latencies.'});const gestureScrollUpdateLatencies=[...iterAsyncEvents_(modelHelper.browserHelpers,ranges,processGestureScrollUpdateLatencyEvent)].sort();if(gestureScrollUpdateLatencies.length){histograms.createHistogram('first_gesture_scroll_update_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,gestureScrollUpdateLatencies[0][1],{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,20),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Latency of the first gesture scroll update.'});}}
+function addLatencyHistograms(histograms,model,segments){const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!modelHelper)return;const ranges=segments.map(s=>s.boundsRange);const mainThreadScrollLatencies=[...iterAsyncEvents_(Object.values(modelHelper.rendererHelpers),ranges,processLatencyEvent)];histograms.createHistogram('main_thread_scroll_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,mainThreadScrollLatencies,{binBoundaries:tr.v.HistogramBinBoundaries.createLinear(0,50,50),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Main thread scroll latencies.'});}
 return{addLatencyHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){function eventIsValidGraphicsEvent_(event,eventMap){if(event.title!=='Graphics.Pipeline'||!event.bindId||!event.args||!event.args.step){return false;}
 const bindId=event.bindId;if(eventMap.has(bindId)&&event.args.step in eventMap.get(bindId)){if(event.args.step==='IssueBeginFrame'||event.args.step==='ReceiveBeginFrame'){throw new Error('Unexpected duplicate step: '+event.args.step);}
 return false;}
@@ -8295,25 +8489,31 @@
 return breakdown;}
 function getDisplayCompositorThread_(model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const gpuHelper=chromeHelper.gpuHelper;if(gpuHelper){const thread=gpuHelper.process.findAtMostOneThreadNamed('VizCompositorThread');if(thread){return thread;}}
 if(!chromeHelper.browserProcess)return null;return chromeHelper.browserProcess.findAtMostOneThreadNamed('CrBrowserMain');}
+function getRasterTaskTimes(sourceFrameNumber,model){const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const renderers=modelHelper.telemetryHelper.renderersWithIR;if(renderers.length===0)return;const rasterThreads=renderers[0].rasterWorkerThreads;let earliestStart=undefined;let lastEnd=undefined;for(const rasterThread of rasterThreads){for(const slice of[...rasterThread.findTopmostSlicesNamed('TaskGraphRunner::RunTask')]){if(slice.args&&slice.args.source_frame_number_&&slice.args.source_frame_number_===sourceFrameNumber){if(earliestStart===undefined||slice.start<earliestStart){earliestStart=slice.start;}
+if(lastEnd===undefined||slice.end>lastEnd){lastEnd=slice.end;}}}}
+return{start:earliestStart,end:lastEnd};}
 function addPipelineHistograms(histograms,model,segments){const ranges=segments.map(s=>s.boundsRange);const bindEvents=new Map();for(const thread of model.getAllThreads()){for(const event of thread.sliceGroup.childEvents()){if(!eventIsValidGraphicsEvent_(event,bindEvents))continue;for(const range of ranges){if(range.containsExplicitRangeInclusive(event.start,event.end)){if(!bindEvents.has(event.bindId))bindEvents.set(event.bindId,{});break;}}
 if(bindEvents.has(event.bindId)){bindEvents.get(event.bindId)[event.args.step]=event;}}}
 const dcThread=getDisplayCompositorThread_(model);const drawEvents={};if(dcThread){const events=[...dcThread.findTopmostSlicesNamed('Graphics.Pipeline.DrawAndSwap')];for(const segment of segments){const filteredEvents=segment.boundsRange.filterArray(events,evt=>evt.start);for(const event of filteredEvents){if((event.args&&event.args.status==='canceled')||!event.id.startsWith(':ptr:')){continue;}
 const id=parseInt(event.id.substring(5),16);if(id in drawEvents){throw new Error('Duplicate draw events: '+id);}
 drawEvents[id]=event;}}}
-const issueToReceipt=histograms.createHistogram('pipeline:begin_frame_transport',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency of begin-frame message from the display '+'compositor to the client, including the IPC latency and task-'+'queue time in the client.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const receiptToSubmit=histograms.createHistogram('pipeline:begin_frame_to_frame_submission',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between begin-frame reception and '+'CompositorFrame submission in the renderer.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const submitToAggregate=histograms.createHistogram('pipeline:frame_submission_to_display',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between CompositorFrame submission in the '+'renderer to display in the display-compositor, including IPC '+'latency, task-queue time in the display-compositor, and '+'additional processing (e.g. surface-sync etc.)',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const aggregateToDraw=histograms.createHistogram('pipeline:draw',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'How long it takes for the gpu-swap step.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});for(const flow of bindEvents.values()){if(!flow.IssueBeginFrame||!flow.ReceiveBeginFrame||!flow.SubmitCompositorFrame||!flow.SurfaceAggregation){continue;}
+const issueToReceipt=histograms.createHistogram('pipeline:begin_frame_transport',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency of begin-frame message from the display '+'compositor to the client, including the IPC latency and task-'+'queue time in the client.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const issueToRasterStart=histograms.createHistogram('pipeline:begin_frame_to_raster_start',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between begin-frame message and '+'the beginning of the first CompositorTask run in the compositor.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const issueToRasterEnd=histograms.createHistogram('pipeline:begin_frame_to_raster_end',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between begin-frame message and '+'the end of the last CompositorTask run in the compositor.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const receiptToSubmit=histograms.createHistogram('pipeline:begin_frame_to_frame_submission',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between begin-frame reception and '+'CompositorFrame submission in the renderer.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const submitToAggregate=histograms.createHistogram('pipeline:frame_submission_to_display',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'Latency between CompositorFrame submission in the '+'renderer to display in the display-compositor, including IPC '+'latency, task-queue time in the display-compositor, and '+'additional processing (e.g. surface-sync etc.)',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});const aggregateToDraw=histograms.createHistogram('pipeline:draw',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:'How long it takes for the gpu-swap step.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});for(const flow of bindEvents.values()){if(!flow.IssueBeginFrame||!flow.ReceiveBeginFrame||!flow.SubmitCompositorFrame||!flow.SurfaceAggregation){continue;}
 issueToReceipt.addSample(flow.ReceiveBeginFrame.start-
-flow.IssueBeginFrame.start);receiptToSubmit.addSample(flow.SubmitCompositorFrame.end-flow.ReceiveBeginFrame.start,{breakdown:generateBreakdownForCompositorPipelineInClient_(flow)});submitToAggregate.addSample(flow.SurfaceAggregation.end-flow.SubmitCompositorFrame.end,{breakdown:generateBreakdownForCompositorPipelineInService_(flow)});if(flow.SurfaceAggregation.args&&flow.SurfaceAggregation.args.display_trace){const displayTrace=flow.SurfaceAggregation.args.display_trace;if(!(displayTrace in drawEvents))continue;const drawEvent=drawEvents[displayTrace];aggregateToDraw.addSample(drawEvent.duration,{breakdown:generateBreakdownForDraw_(drawEvent)});}}}
+flow.IssueBeginFrame.start);receiptToSubmit.addSample(flow.SubmitCompositorFrame.end-flow.ReceiveBeginFrame.start,{breakdown:generateBreakdownForCompositorPipelineInClient_(flow)});submitToAggregate.addSample(flow.SurfaceAggregation.end-flow.SubmitCompositorFrame.end,{breakdown:generateBreakdownForCompositorPipelineInService_(flow)});if(flow.SubmitCompositorFrame.parentSlice){const sourceFrameNumber=flow.SubmitCompositorFrame.parentSlice.args.source_frame_number_;const rasterDuration=getRasterTaskTimes(sourceFrameNumber,model);if(rasterDuration&&rasterDuration.start&&rasterDuration.end){const receiveToStart=rasterDuration.start-
+flow.ReceiveBeginFrame.start;const receiveToEnd=rasterDuration.end-flow.ReceiveBeginFrame.end;if(receiveToEnd>0){issueToRasterStart.addSample(receiveToStart>0?receiveToStart:0);issueToRasterEnd.addSample(receiveToEnd);}}}
+if(flow.SurfaceAggregation.args&&flow.SurfaceAggregation.args.display_trace){const displayTrace=flow.SurfaceAggregation.args.display_trace;if(!(displayTrace in drawEvents))continue;const drawEvent=drawEvents[displayTrace];aggregateToDraw.addSample(drawEvent.duration,{breakdown:generateBreakdownForDraw_(drawEvent)});}}}
 return{addPipelineHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const IMPL_THREAD_RENDERING_STATS_EVENT='BenchmarkInstrumentation::ImplThreadRenderingStats';const VISIBLE_CONTENT_DATA='visible_content_area';const APPROXIMATED_VISIBLE_CONTENT_DATA='approximated_visible_content_area';const CHECKERBOARDED_VISIBLE_CONTENT_DATA='checkerboarded_visible_content_area';function addPixelsHistograms(histograms,model,segments){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;const approximatedPixelPercentages=[];const checkerboardedPixelPercentages=[];const ranges=segments.map(s=>s.boundsRange);for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){if(rendererHelper.compositorThread===undefined)continue;const slices=rendererHelper.compositorThread.sliceGroup;for(const slice of slices.getDescendantEventsInSortedRanges(ranges)){if(slice.title!==IMPL_THREAD_RENDERING_STATS_EVENT)continue;const data=slice.args.data;if(!(VISIBLE_CONTENT_DATA in data)){throw new Error(`${VISIBLE_CONTENT_DATA} is missing`);}
 const visibleContentArea=data[VISIBLE_CONTENT_DATA];if(visibleContentArea===0){continue;}
 if(APPROXIMATED_VISIBLE_CONTENT_DATA in data){approximatedPixelPercentages.push(data[APPROXIMATED_VISIBLE_CONTENT_DATA]/visibleContentArea);}
 if(CHECKERBOARDED_VISIBLE_CONTENT_DATA in data){checkerboardedPixelPercentages.push(data[CHECKERBOARDED_VISIBLE_CONTENT_DATA]/visibleContentArea);}}}
-histograms.createHistogram('mean_pixels_approximated',tr.b.Unit.byName.normalizedPercentage_smallerIsBetter,100*tr.b.math.Statistics.mean(approximatedPixelPercentages),{description:'Percentage of pixels that were approximated '+'(checkerboarding, low-resolution tiles, etc.).',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});histograms.createHistogram('mean_pixels_checkerboarded',tr.b.Unit.byName.normalizedPercentage_smallerIsBetter,100*tr.b.math.Statistics.mean(checkerboardedPixelPercentages),{description:'Percentage of pixels that were checkerboarded.',summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,});}
+histograms.createHistogram('mean_pixels_approximated',tr.b.Unit.byName.normalizedPercentage_smallerIsBetter,100*tr.b.math.Statistics.mean(approximatedPixelPercentages),{description:'Percentage of pixels that were approximated '+'(checkerboarding, low-resolution tiles, etc.).',summaryOptions:{},});histograms.createHistogram('mean_pixels_checkerboarded',tr.b.Unit.byName.normalizedPercentage_smallerIsBetter,100*tr.b.math.Statistics.mean(checkerboardedPixelPercentages),{description:'Percentage of pixels that were checkerboarded.',summaryOptions:{},});}
 return{addPixelsHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const BEGIN_MAIN_FRAME_EVENT='ThreadProxy::BeginMainFrame';const SEND_BEGIN_FRAME_EVENT='ThreadProxy::ScheduledActionSendBeginMainFrame';function getEventTimesByBeginFrameId_(thread,title,ranges){const out=new Map();const slices=thread.sliceGroup;for(const slice of slices.getDescendantEventsInSortedRanges(ranges)){if(slice.title!==title)continue;const id=slice.args.begin_frame_id;if(id===undefined)throw new Error('Event is missing begin_frame_id');if(out.has(id))throw new Error(`There must be exactly one ${title}`);out.set(id,slice.start);}
 return out;}
 function addQueueingDurationHistograms(histograms,model,segments){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;let targetRenderers=chromeHelper.telemetryHelper.renderersWithIR;if(targetRenderers.length===0){targetRenderers=Object.values(chromeHelper.rendererHelpers);}
 const queueingDurations=[];const ranges=segments.map(s=>s.boundsRange);for(const rendererHelper of targetRenderers){const mainThread=rendererHelper.mainThread;const compositorThread=rendererHelper.compositorThread;if(mainThread===undefined||compositorThread===undefined)continue;const beginMainFrameTimes=getEventTimesByBeginFrameId_(mainThread,BEGIN_MAIN_FRAME_EVENT,ranges);const sendBeginFrameTimes=getEventTimesByBeginFrameId_(compositorThread,SEND_BEGIN_FRAME_EVENT,ranges);for(const[id,time]of sendBeginFrameTimes){queueingDurations.push(beginMainFrameTimes.get(id)-time);}}
 histograms.createHistogram('queueing_durations',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,queueingDurations,{binBoundaries:tr.v.HistogramBinBoundaries.createExponential(0.01,2,20),summaryOptions:tr.metrics.rendering.SUMMARY_OPTIONS,description:'Time between ScheduledActionSendBeginMainFrame in '+'the compositor thread and the corresponding '+'BeginMainFrame in the main thread.'});}
-return{addQueueingDurationHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const GESTURE_EVENT='SyntheticGestureController::running';function renderingMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;const segments=chromeHelper.telemetryHelper.segments;if(segments.length>0){tr.metrics.rendering.addFrameTimeHistograms(histograms,model,segments);tr.metrics.rendering.addLatencyHistograms(histograms,model,segments);tr.metrics.rendering.addPipelineHistograms(histograms,model,segments);tr.metrics.rendering.addPixelsHistograms(histograms,model,segments);tr.metrics.rendering.addQueueingDurationHistograms(histograms,model,segments);}
+return{addQueueingDurationHistograms,};});'use strict';tr.exportTo('tr.metrics.rendering',function(){const GESTURE_EVENT='SyntheticGestureController::running';function renderingMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper)return;let segments=chromeHelper.telemetryHelper.irSegments;if(segments.length===0){segments=chromeHelper.telemetryHelper.animationSegments;}
+if(segments.length>0){tr.metrics.rendering.addFrameTimeHistograms(histograms,model,segments);tr.metrics.rendering.addImageDecodeTimeHistograms(histograms,model,segments);tr.metrics.rendering.addLatencyHistograms(histograms,model,segments);tr.metrics.rendering.addPipelineHistograms(histograms,model,segments);tr.metrics.rendering.addPixelsHistograms(histograms,model,segments);tr.metrics.rendering.addQueueingDurationHistograms(histograms,model,segments);}
 const uiSegments=chromeHelper.telemetryHelper.uiSegments;if(uiSegments.length>0){tr.metrics.rendering.addUIFrameTimeHistograms(histograms,model,chromeHelper.telemetryHelper.uiSegments);}}
 tr.metrics.MetricRegistry.register(renderingMetric,{requiredCategories:['benchmark','toplevel'],});return{renderingMetric,};});'use strict';tr.exportTo('tr.metrics',function(){function sampleExceptionMetric(histograms,model){const hist=new tr.v.Histogram('foo',tr.b.Unit.byName.sizeInBytes_smallerIsBetter);hist.addSample(9);hist.addSample(91,{bar:new tr.v.d.GenericSet([{hello:42}])});for(const expectation of model.userModel.expectations){if(expectation instanceof tr.model.um.ResponseExpectation){}else if(expectation instanceof tr.model.um.AnimationExpectation){}else if(expectation instanceof tr.model.um.IdleExpectation){}else if(expectation instanceof tr.model.um.LoadExpectation){}}
 const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const[pid,process]of Object.entries(model.processes)){}
@@ -8355,39 +8555,83 @@
 breakdownTree.blocked_on_network={total:Math.max(totalBlockedDuration,0),events:{}};}
 function generateWallClockTimeBreakdownTree(mainThread,networkEvents,rangeOfInterest){const breakdownTree=generateTimeBreakdownTree(mainThread,rangeOfInterest,getWallClockSelfTime_);const mainThreadEventsInRange=tr.model.helpers.getSlicesIntersectingRange(rangeOfInterest,mainThread.sliceGroup.topLevelSlices);addIdleAndBlockByNetworkBreakdown_(breakdownTree,mainThreadEventsInRange,networkEvents,rangeOfInterest);return breakdownTree;}
 function generateCpuTimeBreakdownTree(mainThread,rangeOfInterest){return generateTimeBreakdownTree(mainThread,rangeOfInterest,getCPUSelfTime_);}
-return{generateTimeBreakdownTree,generateWallClockTimeBreakdownTree,generateCpuTimeBreakdownTree,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const LONG_TASK_THRESHOLD_MS=50;const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const RelatedEventSet=tr.v.d.RelatedEventSet;const hasCategoryAndName=tr.metrics.sh.hasCategoryAndName;const EventFinderUtils=tr.e.chrome.EventFinderUtils;function getNetworkEventsInRange(process,range){const networkEvents=[];for(const thread of Object.values(process.threads)){const threadHelper=new tr.model.helpers.ChromeThreadHelper(thread);const events=threadHelper.getNetworkEvents();for(const event of events){if(range.intersectsExplicitRangeInclusive(event.start,event.end)){networkEvents.push(event);}}}
-return networkEvents;}
-function createBreakdownDiagnostic(breakdownTree){const breakdownDiagnostic=new tr.v.d.Breakdown();breakdownDiagnostic.colorScheme=tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;for(const label in breakdownTree){breakdownDiagnostic.set(label,breakdownTree[label].total);}
+return{generateTimeBreakdownTree,generateWallClockTimeBreakdownTree,generateCpuTimeBreakdownTree,};});'use strict';tr.exportTo('tr.e.chrome',function(){const LCP_CANDIDATE_EVENT_TITLE='NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM';const LCP_INVALIDATE_EVENT_TITLE='NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';class LcpEvent{constructor(event){if(!LcpInvalidateEvent.isLcpInvalidateEvent(event)&&!LcpCandidateEvent.isLcpCandidateEvent(event)){throw new Error('The LCP event should be either a candidate event or'+'an invalidate event.');}
+if(event.start===undefined||event.args.main_frame_tree_node_id===undefined){throw new Error('The LCP event is in unexpected format.');}
+this.start=event.start;this.mainFrameTreeNodeId=event.args.main_frame_tree_node_id;}}
+class LcpCandidateEvent extends LcpEvent{constructor(event){super(event);const{durationInMilliseconds,size,type,inMainFrame}=event.args.data;if(durationInMilliseconds===undefined||size===undefined||type===undefined||inMainFrame===undefined||event.args.main_frame_tree_node_id===undefined||!LcpCandidateEvent.isLcpCandidateEvent(event)){throw new Error('The LCP candidate event is in unexpected format.');}
+this.durationInMilliseconds=durationInMilliseconds;this.size=size;this.type=type;this.inMainFrame=inMainFrame;}
+static isLcpCandidateEvent(event){return event.title===LCP_CANDIDATE_EVENT_TITLE;}}
+class LcpInvalidateEvent extends LcpEvent{constructor(event){super(event);if(!LcpInvalidateEvent.isLcpInvalidateEvent(event)){throw new Error('The LCP invalidate event is in unexpected format.');}}
+static isLcpInvalidateEvent(event){return event.title===LCP_INVALIDATE_EVENT_TITLE;}}
+class LargestContentfulPaint{constructor(allBrowserEvents){this.allBrowserEvents=allBrowserEvents;}
+findCandidates(){const finalLcpEvents=this.findFinalLcpEventOfEachNavigation(this.allBrowserEvents);const finalCandidates=finalLcpEvents.filter(finalLcpEvent=>!LcpInvalidateEvent.isLcpInvalidateEvent(finalLcpEvent));return finalCandidates;}
+findFinalLcpEventOfEachNavigation(allBrowserEvents){const lcpEvents=[];for(const lcpEvent of allBrowserEvents){if(LcpCandidateEvent.isLcpCandidateEvent(lcpEvent)){lcpEvents.push(new LcpCandidateEvent(lcpEvent));}else if(LcpInvalidateEvent.isLcpInvalidateEvent(lcpEvent)){lcpEvents.push(new LcpInvalidateEvent(lcpEvent));}}
+const lcpEventsGroupedByNavigation=new Map();for(const e of lcpEvents){const key=e.mainFrameTreeNodeId;if(!lcpEventsGroupedByNavigation.has(key)){lcpEventsGroupedByNavigation[key]=[];}
+lcpEventsGroupedByNavigation[key].push(e);}
+const finalLcpEventOfEachNaivgation=[];for(const lcpEventList of Object.values(lcpEventsGroupedByNavigation)){lcpEventList.sort((a,b)=>a.start-b.start);finalLcpEventOfEachNaivgation.push(lcpEventList[lcpEventList.length-1]);}
+return finalLcpEventOfEachNaivgation;}}
+return{LCP_CANDIDATE_EVENT_TITLE,LCP_INVALIDATE_EVENT_TITLE,LargestContentfulPaint,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const LONG_TASK_THRESHOLD_MS=50;const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const unitlessNumber_smallerIsBetter=tr.b.Unit.byName.unitlessNumber_smallerIsBetter;const RelatedEventSet=tr.v.d.RelatedEventSet;const hasCategoryAndName=tr.metrics.sh.hasCategoryAndName;const EventFinderUtils=tr.e.chrome.EventFinderUtils;function createBreakdownDiagnostic(breakdownTree){const breakdownDiagnostic=new tr.v.d.Breakdown();breakdownDiagnostic.colorScheme=tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;for(const label in breakdownTree){breakdownDiagnostic.set(label,breakdownTree[label].total);}
 return breakdownDiagnostic;}
-const LOADING_METRIC_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,1e3,20).addLinearBins(3e3,20).addExponentialBins(20e3,20);const TIME_TO_INTERACTIVE_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1,40e3,35).addExponentialBins(80e3,15);const SUMMARY_OPTIONS={avg:true,count:false,max:true,min:true,std:true,sum:false,};function findFrameLoaderSnapshotAt(rendererHelper,frameIdRef,ts){const objects=rendererHelper.process.objects;const frameLoaderInstances=objects.instancesByTypeName_.FrameLoader;if(frameLoaderInstances===undefined)return undefined;let snapshot;for(const instance of frameLoaderInstances){if(!instance.isAliveAt(ts))continue;const maybeSnapshot=instance.getSnapshotAt(ts);if(frameIdRef!==maybeSnapshot.args.frame.id_ref)continue;snapshot=maybeSnapshot;}
+const LOADING_METRIC_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,1e3,20).addLinearBins(3e3,20).addExponentialBins(20e3,20);const TIME_TO_INTERACTIVE_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1,40e3,35).addExponentialBins(80e3,15);const LAYOUT_SHIFT_SCORE_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,50,25);const SUMMARY_OPTIONS={avg:true,count:false,max:true,min:true,std:true,sum:false,};function findFrameLoaderSnapshotAt(rendererHelper,frameIdRef,ts){const objects=rendererHelper.process.objects;const frameLoaderInstances=objects.instancesByTypeName_.FrameLoader;if(frameLoaderInstances===undefined)return undefined;let snapshot;for(const instance of frameLoaderInstances){if(!instance.isAliveAt(ts))continue;const maybeSnapshot=instance.getSnapshotAt(ts);if(frameIdRef!==maybeSnapshot.args.frame.id_ref)continue;snapshot=maybeSnapshot;}
 return snapshot;}
 function findAllEvents(rendererHelper,category,title){const targetEvents=[];for(const ev of rendererHelper.process.getDescendantEvents()){if(!hasCategoryAndName(ev,category,title))continue;targetEvents.push(ev);}
 return targetEvents;}
+function getMostRecentValidEvent(rendererHelper,category,title){const targetEvents=findAllEvents(rendererHelper,category,title);let validEvent;for(const targetEvent of targetEvents){if(rendererHelper.isTelemetryInternalEvent(targetEvent))continue;if(validEvent===undefined){validEvent=targetEvent;}else{if(validEvent.start<targetEvent.start){validEvent=targetEvent;}}}
+return validEvent;}
+function getFirstViewportReadySamples(rendererHelper,navIdToNavStartEvents){const samples=[];const pcEvent=getMostRecentValidEvent(rendererHelper,'blink.user_timing','pc');if(pcEvent===undefined)return samples;if(rendererHelper.isTelemetryInternalEvent(pcEvent))return samples;const navigationStartEvent=navIdToNavStartEvents.get(pcEvent.args.data.navigationId);if(navigationStartEvent===undefined)return samples;const navStartToEventRange=tr.b.math.Range.fromExplicitRange(navigationStartEvent.start,pcEvent.start);const networkEvents=EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,navStartToEventRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToEventRange);samples.push({value:navStartToEventRange.duration,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),Start:new RelatedEventSet(navigationStartEvent),End:new RelatedEventSet(pcEvent)}});return samples;}
+function getAboveTheFoldLoadedToVisibleSamples(rendererHelper){const samples=[];const pcEvent=getMostRecentValidEvent(rendererHelper,'blink.user_timing','pc');const visibleEvent=getMostRecentValidEvent(rendererHelper,'blink.user_timing','visible');if(pcEvent!==undefined&&visibleEvent!==undefined){samples.push({value:Math.max(0.0,pcEvent.start-visibleEvent.start),diagnostics:{Start:new RelatedEventSet(visibleEvent),End:new RelatedEventSet(pcEvent)}});}
+return samples;}
 function findTimeToXEntries(category,eventName,rendererHelper,frameToNavStartEvents,navIdToNavStartEvents){const targetEvents=findAllEvents(rendererHelper,category,eventName);const entries=[];for(const targetEvent of targetEvents){if(rendererHelper.isTelemetryInternalEvent(targetEvent))continue;const frameIdRef=targetEvent.args.frame;const snapshot=findFrameLoaderSnapshotAt(rendererHelper,frameIdRef,targetEvent.start);if(snapshot===undefined||!snapshot.args.isLoadingMainFrame)continue;const url=snapshot.args.documentLoaderURL;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(url))continue;let navigationStartEvent;if(targetEvent.args.data===undefined||targetEvent.args.data.navigationId===undefined){navigationStartEvent=EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp(frameToNavStartEvents.get(frameIdRef)||[],targetEvent.start);}else{navigationStartEvent=navIdToNavStartEvents.get(targetEvent.args.data.navigationId);}
 if(navigationStartEvent===undefined)continue;entries.push({navigationStartEvent,targetEvent,url,});}
 return entries;}
-function collectTimeToEvent(rendererHelper,timeToXEntries){const samples=[];for(const{targetEvent,navigationStartEvent,url}of timeToXEntries){const navStartToEventRange=tr.b.math.Range.fromExplicitRange(navigationStartEvent.start,targetEvent.start);const networkEvents=getNetworkEventsInRange(rendererHelper.process,navStartToEventRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToEventRange);samples.push({value:navStartToEventRange.duration,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),url:new tr.v.d.GenericSet([url]),Start:new RelatedEventSet(navigationStartEvent),End:new RelatedEventSet(targetEvent)}});}
+function collectTimeToEvent(rendererHelper,timeToXEntries){const samples=[];for(const{targetEvent,navigationStartEvent,url}of timeToXEntries){const navStartToEventRange=tr.b.math.Range.fromExplicitRange(navigationStartEvent.start,targetEvent.start);const networkEvents=EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,navStartToEventRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToEventRange);samples.push({value:navStartToEventRange.duration,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),url:new tr.v.d.GenericSet([url]),Start:new RelatedEventSet(navigationStartEvent),End:new RelatedEventSet(targetEvent)}});}
 return samples;}
 function collectTimeToEventInCpuTime(rendererHelper,timeToXEntries){const samples=[];for(const{targetEvent,navigationStartEvent,url}of timeToXEntries){const navStartToEventRange=tr.b.math.Range.fromExplicitRange(navigationStartEvent.start,targetEvent.start);const mainThreadCpuTime=rendererHelper.mainThread.getCpuTimeForRange(navStartToEventRange);const breakdownTree=tr.metrics.sh.generateCpuTimeBreakdownTree(rendererHelper.mainThread,navStartToEventRange);samples.push({value:mainThreadCpuTime,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),start:new RelatedEventSet(navigationStartEvent),end:new RelatedEventSet(targetEvent),infos:new tr.v.d.GenericSet([{pid:rendererHelper.pid,start:navigationStartEvent.start,event:targetEvent.start,}]),}});}
 return samples;}
-function addFirstMeaningfulPaintSample(samples,rendererHelper,navigationStart,fmpMarkerEvent,url){const navStartToFMPRange=tr.b.math.Range.fromExplicitRange(navigationStart.start,fmpMarkerEvent.start);const networkEvents=getNetworkEventsInRange(rendererHelper.process,navStartToFMPRange);const timeToFirstMeaningfulPaint=navStartToFMPRange.duration;const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToFMPRange);samples.push({value:timeToFirstMeaningfulPaint,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),start:new RelatedEventSet(navigationStart),end:new RelatedEventSet(fmpMarkerEvent),infos:new tr.v.d.GenericSet([{url,pid:rendererHelper.pid,start:navigationStart.start,fmp:fmpMarkerEvent.start,}]),}});}
+function findLayoutShiftSamples(rendererHelper){let sample;EventFinderUtils.getSortedMainThreadEventsByFrame(rendererHelper,'LayoutShift','loading').forEach((events)=>{const evData=events.pop().args.data;if(evData.is_main_frame){sample={value:evData.cumulative_score};}});return sample?[sample]:[];}
+function lineSweep(lineSweepRects,viewport){const verticalSweepRects=[];const horizontalSweepRects=[];for(let i=0;i<lineSweepRects.length;i++){const rect=lineSweepRects[i];let left=rect.left;let right=rect.right;let top=rect.top;let bottom=rect.bottom;if(left>viewport.x+viewport.width)continue;if(right<viewport.x)continue;if(top>viewport.y+viewport.height)continue;if(bottom<viewport.y)continue;left=Math.max(left,viewport.y);right=Math.min(right,viewport.y+viewport.width);top=Math.max(top,viewport.y);bottom=Math.min(bottom,viewport.y+viewport.height);verticalSweepRects.push({id:i,value:left,type:'left'},{id:i,value:right,type:'right'});horizontalSweepRects.push({id:i,value:top,type:'top'},{id:i,value:bottom,type:'bottom'});}
+verticalSweepRects.sort((a,b)=>a.value-b.value);horizontalSweepRects.sort((a,b)=>a.value-b.value);const active=new Array(lineSweepRects.length).fill(0);let area=0;active[verticalSweepRects[0].id]=1;for(let i=1;i<verticalSweepRects.length;i++){const currentLine=verticalSweepRects[i];const previousLine=verticalSweepRects[i-1];const deltaX=currentLine.value-previousLine.value;if(deltaX===0)continue;let count=0;let firstRect;for(let j=0;j<horizontalSweepRects.length;j++){if(active[horizontalSweepRects[j].id]===1){if(horizontalSweepRects[j].type==='top'){if(count===0){firstRect=j;count++;}}else{if(count===1){const deltaY=horizontalSweepRects[j].value-
+horizontalSweepRects[firstRect].value;area+=deltaX*deltaY;count--;}}}}
+active[currentLine.id]=(currentLine.type==='left');}
+return area;}
+function addVisuallyCompleteBeforeSomeTimeSample(samples,rendererHelper,navigationStart,loadDuration,frameID){const someTime=2000;if(loadDuration<someTime)return;let viewport;for(const event of EventFinderUtils.getMainThreadEvents(rendererHelper,'viewport','loading')){if(event.args.data.frameID===frameID&&event.start>navigationStart&&event.start<navigationStart+loadDuration){viewport=event.args.data;break;}}
+if(!viewport)return;const ccDisplayItemListObjects=rendererHelper.process.objects.getAllInstancesNamed('cc::DisplayItemList');if(!ccDisplayItemListObjects||!ccDisplayItemListObjects.length)return;const RectsUpdatedAfterSomeTime=[];for(let i=0;i<ccDisplayItemListObjects.length;i++){const displayItemListSnapshots=ccDisplayItemListObjects[i].snapshots;for(let j=0;j<displayItemListSnapshots.length;j++){const snapshot=displayItemListSnapshots[j];const timestamp=snapshot.ts;if(timestamp<navigationStart||timestamp>navigationStart+loadDuration){continue;}
+if(timestamp-navigationStart>someTime){RectsUpdatedAfterSomeTime.push(snapshot.args.params.layerRect);}}}
+const areaUpdatedAfterSomeTime=RectsUpdatedAfterSomeTime.length?lineSweep(RectsUpdatedAfterSomeTime,viewport):0;const pixelsLastUpdatedBeforeSomeTime=1-
+areaUpdatedAfterSomeTime/(viewport.width*viewport.height);samples.push({value:pixelsLastUpdatedBeforeSomeTime});}
+function addSpeedIndexSample(rendererHelper){const rects=[];let totalArea=0;for(const event of EventFinderUtils.getMainThreadEvents(rendererHelper,'PaintTracker::LayoutObjectPainted','loading')){const rect=event.args.data.visible_new_visual_rect;if(!rect)continue;const area=areaOfQuad(rect);if(!area)continue;rects.push({rect,ts:event.start});totalArea+=area;}
+const sample=[];if(!totalArea)return sample;let areaSoFar=0;const progress=new Array(rects.length);for(let i=0;i<rects.length;i++){const area=areaOfQuad(rects[i].rect);areaSoFar+=(area/totalArea);progress[i]={value:areaSoFar,ts:rects[i].ts};}
+sample.push({value:calculateSpeedIndex(progress)});return sample;}
+function areaOfQuad(quad){const width=Math.max(Math.abs(quad[0]-quad[2]),Math.abs(quad[2]-quad[4]));const height=Math.max(Math.abs(quad[1]-quad[3]),Math.abs(quad[3]-quad[5]));return width*height;}
+function calculateSpeedIndex(progress){let lastMs=0;let lastProgress=0;let speedIndex=0;for(let i=0;i<progress.length;i++){const elapsed=progress[i].ts-lastMs;speedIndex+=elapsed*(1.0-lastProgress);lastMs=progress[i].ts;lastProgress=progress[i].value;}
+return speedIndex;}
+function addFirstMeaningfulPaintSample(samples,rendererHelper,navigationStart,fmpMarkerEvent,url){const navStartToFMPRange=tr.b.math.Range.fromExplicitRange(navigationStart.start,fmpMarkerEvent.start);const networkEvents=EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,navStartToFMPRange);const timeToFirstMeaningfulPaint=navStartToFMPRange.duration;const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToFMPRange);samples.push({value:timeToFirstMeaningfulPaint,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),start:new RelatedEventSet(navigationStart),end:new RelatedEventSet(fmpMarkerEvent),infos:new tr.v.d.GenericSet([{url,pid:rendererHelper.pid,start:navigationStart.start,fmp:fmpMarkerEvent.start,}]),}});}
 function addFirstMeaningfulPaintCpuTimeSample(samples,rendererHelper,navigationStart,fmpMarkerEvent,url){const navStartToFMPRange=tr.b.math.Range.fromExplicitRange(navigationStart.start,fmpMarkerEvent.start);const mainThreadCpuTime=rendererHelper.mainThread.getCpuTimeForRange(navStartToFMPRange);const breakdownTree=tr.metrics.sh.generateCpuTimeBreakdownTree(rendererHelper.mainThread,navStartToFMPRange);samples.push({value:mainThreadCpuTime,breakdownTree,diagnostics:{breakdown:createBreakdownDiagnostic(breakdownTree),start:new RelatedEventSet(navigationStart),end:new RelatedEventSet(fmpMarkerEvent),infos:new tr.v.d.GenericSet([{url,pid:rendererHelper.pid,start:navigationStart.start,fmp:fmpMarkerEvent.start,}]),}});}
-function decorateInteractivitySampleWithDiagnostics_(rendererHelper,eventTimestamp,navigationStartEvent,firstMeaningfulPaintTime,domContentLoadedEndTime,url){if(eventTimestamp===undefined)return undefined;const navigationStartTime=navigationStartEvent.start;const navStartToEventTimeRange=tr.b.math.Range.fromExplicitRange(navigationStartTime,eventTimestamp);const networkEvents=getNetworkEventsInRange(rendererHelper.process,navStartToEventTimeRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToEventTimeRange);const breakdownDiagnostic=createBreakdownDiagnostic(breakdownTree);return{value:navStartToEventTimeRange.duration,diagnostics:tr.v.d.DiagnosticMap.fromObject({'Start':new RelatedEventSet(navigationStartEvent),'Navigation infos':new tr.v.d.GenericSet([{url,pid:rendererHelper.pid,navigationStartTime,firstMeaningfulPaintTime,domContentLoadedEndTime,eventTimestamp,}]),'Breakdown of [navStart, eventTimestamp]':breakdownDiagnostic,}),};}
-function collectLoadingMetricsForRenderer(rendererHelper){const frameToNavStartEvents=EventFinderUtils.getSortedMainThreadEventsByFrame(rendererHelper,'navigationStart','blink.user_timing');const navIdToNavStartEvents=EventFinderUtils.getSortedMainThreadEventsByNavId(rendererHelper,'navigationStart','blink.user_timing');const firstPaintSamples=collectTimeToEvent(rendererHelper,findTimeToXEntries('loading','firstPaint',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents));const timeToFCPEntries=findTimeToXEntries('loading','firstContentfulPaint',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const firstContentfulPaintSamples=collectTimeToEvent(rendererHelper,timeToFCPEntries);const firstContentfulPaintCpuTimeSamples=collectTimeToEventInCpuTime(rendererHelper,timeToFCPEntries);const onLoadSamples=collectTimeToEvent(rendererHelper,findTimeToXEntries('blink.user_timing','loadEventStart',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents));return{frameToNavStartEvents,firstPaintSamples,firstContentfulPaintSamples,firstContentfulPaintCpuTimeSamples,onLoadSamples,};}
-function collectMetricsFromLoadExpectations(model,chromeHelper){const interactiveSamples=[];const firstCpuIdleSamples=[];const firstMeaningfulPaintSamples=[];const firstMeaningfulPaintCpuTimeSamples=[];for(const expectation of model.userModel.expectations){if(!(expectation instanceof tr.model.um.LoadExpectation))continue;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
-const rendererHelper=chromeHelper.rendererHelpers[expectation.renderProcess.pid];if(expectation.fmpEvent!==undefined){addFirstMeaningfulPaintSample(firstMeaningfulPaintSamples,rendererHelper,expectation.navigationStart,expectation.fmpEvent,expectation.url);addFirstMeaningfulPaintCpuTimeSample(firstMeaningfulPaintCpuTimeSamples,rendererHelper,expectation.navigationStart,expectation.fmpEvent,expectation.url);}
+function decorateInteractivitySampleWithDiagnostics_(rendererHelper,eventTimestamp,navigationStartEvent,firstMeaningfulPaintTime,domContentLoadedEndTime,url){if(eventTimestamp===undefined)return undefined;const navigationStartTime=navigationStartEvent.start;const navStartToEventTimeRange=tr.b.math.Range.fromExplicitRange(navigationStartTime,eventTimestamp);const networkEvents=EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,navStartToEventTimeRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,navStartToEventTimeRange);const breakdownDiagnostic=createBreakdownDiagnostic(breakdownTree);return{value:navStartToEventTimeRange.duration,diagnostics:tr.v.d.DiagnosticMap.fromObject({'Start':new RelatedEventSet(navigationStartEvent),'Navigation infos':new tr.v.d.GenericSet([{url,pid:rendererHelper.pid,navigationStartTime,firstMeaningfulPaintTime,domContentLoadedEndTime,eventTimestamp,}]),'Breakdown of [navStart, eventTimestamp]':breakdownDiagnostic,}),};}
+function getCandidateIndex(entry){return entry.targetEvent.args.data.candidateIndex;}
+function findLastCandidateForEachNavigation(timeToXEntries){const entryMap=new Map();for(const e of timeToXEntries){const navStartEvent=e.navigationStartEvent;if(!entryMap.has(navStartEvent)){entryMap.set(navStartEvent,[]);}
+entryMap.get(navStartEvent).push(e);}
+const lastCandidates=[];for(const timeToXEntriesByNavigation of entryMap.values()){let lastCandidate=timeToXEntriesByNavigation.shift();for(const entry of timeToXEntriesByNavigation){if(getCandidateIndex(entry)>getCandidateIndex(lastCandidate)){lastCandidate=entry;}}
+lastCandidates.push(lastCandidate);}
+return lastCandidates;}
+function findLargestTextPaintSamples(rendererHelper,frameToNavStartEvents,navIdToNavStartEvents){const timeToPaintEntries=findTimeToXEntries('loading','LargestTextPaint::Candidate',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const timeToPaintBlockingEntries=findTimeToXEntries('loading','LargestTextPaint::NoCandidate',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const lastCandidateEvents=findLastCandidateForEachNavigation(timeToPaintEntries.concat(timeToPaintBlockingEntries)).filter(event=>event.targetEvent.title!=='LargestTextPaint::NoCandidate');return collectTimeToEvent(rendererHelper,lastCandidateEvents);}
+function findLargestImagePaintSamples(rendererHelper,frameToNavStartEvents,navIdToNavStartEvents){const timeToPaintEntries=findTimeToXEntries('loading','LargestImagePaint::Candidate',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const timeToPaintBlockingEntries=findTimeToXEntries('loading','LargestImagePaint::NoCandidate',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const lastCandidateEvents=findLastCandidateForEachNavigation(timeToPaintEntries.concat(timeToPaintBlockingEntries)).filter(event=>event.targetEvent.title!=='LargestImagePaint::NoCandidate');return collectTimeToEvent(rendererHelper,lastCandidateEvents);}
+function findLargestContentfulPaintHistogramSamples(allBrowserEvents){const lcp=new tr.e.chrome.LargestContentfulPaint(allBrowserEvents);const lcpSamples=lcp.findCandidates().map(candidate=>{const{durationInMilliseconds,size,type,inMainFrame,mainFrameTreeNodeId}=candidate;return{value:durationInMilliseconds,diagnostics:{size:new tr.v.d.GenericSet([size]),type:new tr.v.d.GenericSet([type]),inMainFrame:new tr.v.d.GenericSet([inMainFrame]),mainFrameTreeNodeId:new tr.v.d.GenericSet([mainFrameTreeNodeId]),},};});return lcpSamples;}
+function collectLoadingMetricsForRenderer(rendererHelper){const frameToNavStartEvents=EventFinderUtils.getSortedMainThreadEventsByFrame(rendererHelper,'navigationStart','blink.user_timing');const navIdToNavStartEvents=EventFinderUtils.getSortedMainThreadEventsByNavId(rendererHelper,'navigationStart','blink.user_timing');const firstPaintSamples=collectTimeToEvent(rendererHelper,findTimeToXEntries('loading','firstPaint',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents));const timeToFCPEntries=findTimeToXEntries('loading','firstContentfulPaint',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const firstContentfulPaintSamples=collectTimeToEvent(rendererHelper,timeToFCPEntries);const firstContentfulPaintCpuTimeSamples=collectTimeToEventInCpuTime(rendererHelper,timeToFCPEntries);const onLoadSamples=collectTimeToEvent(rendererHelper,findTimeToXEntries('blink.user_timing','loadEventStart',rendererHelper,frameToNavStartEvents,navIdToNavStartEvents));const aboveTheFoldLoadedToVisibleSamples=getAboveTheFoldLoadedToVisibleSamples(rendererHelper);const firstViewportReadySamples=getFirstViewportReadySamples(rendererHelper,navIdToNavStartEvents);const largestImagePaintSamples=findLargestImagePaintSamples(rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const largestTextPaintSamples=findLargestTextPaintSamples(rendererHelper,frameToNavStartEvents,navIdToNavStartEvents);const layoutShiftSamples=findLayoutShiftSamples(rendererHelper);const speedIndexSamples=addSpeedIndexSample(rendererHelper);return{frameToNavStartEvents,firstPaintSamples,firstContentfulPaintSamples,firstContentfulPaintCpuTimeSamples,onLoadSamples,aboveTheFoldLoadedToVisibleSamples,firstViewportReadySamples,largestImagePaintSamples,largestTextPaintSamples,layoutShiftSamples,speedIndexSamples};}
+function collectMetricsFromLoadExpectations(model,chromeHelper){const interactiveSamples=[];const firstCpuIdleSamples=[];const firstMeaningfulPaintSamples=[];const firstMeaningfulPaintCpuTimeSamples=[];const visuallyCompleteBeforeSomeTimeSamples=[];for(const expectation of model.userModel.expectations){if(!(expectation instanceof tr.model.um.LoadExpectation))continue;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
+const rendererHelper=chromeHelper.rendererHelpers[expectation.renderProcess.pid];addVisuallyCompleteBeforeSomeTimeSample(visuallyCompleteBeforeSomeTimeSamples,rendererHelper,expectation.navigationStart.start,expectation.duration,expectation.navigationStart.args.frame);if(expectation.fmpEvent!==undefined){addFirstMeaningfulPaintSample(firstMeaningfulPaintSamples,rendererHelper,expectation.navigationStart,expectation.fmpEvent,expectation.url);addFirstMeaningfulPaintCpuTimeSample(firstMeaningfulPaintCpuTimeSamples,rendererHelper,expectation.navigationStart,expectation.fmpEvent,expectation.url);}
 if(expectation.firstCpuIdleTime!==undefined){firstCpuIdleSamples.push(decorateInteractivitySampleWithDiagnostics_(rendererHelper,expectation.firstCpuIdleTime,expectation.navigationStart,expectation.fmpEvent.start,expectation.domContentLoadedEndEvent.start,expectation.url));}
 if(expectation.timeToInteractive!==undefined){interactiveSamples.push(decorateInteractivitySampleWithDiagnostics_(rendererHelper,expectation.timeToInteractive,expectation.navigationStart,expectation.fmpEvent.start,expectation.domContentLoadedEndEvent.start,expectation.url));}}
-return{firstMeaningfulPaintSamples,firstMeaningfulPaintCpuTimeSamples,firstCpuIdleSamples,interactiveSamples,};}
+return{firstMeaningfulPaintSamples,firstMeaningfulPaintCpuTimeSamples,firstCpuIdleSamples,interactiveSamples,visuallyCompleteBeforeSomeTimeSamples,};}
 function addSamplesToHistogram(samples,histogram,histograms){for(const sample of samples){histogram.addSample(sample.value,sample.diagnostics);if(histogram.name!=='timeToFirstContentfulPaint')continue;if(!sample.breakdownTree)continue;for(const[category,breakdown]of Object.entries(sample.breakdownTree)){const relatedName=`${histogram.name}:${category}`;let relatedHist=histograms.getHistogramsNamed(relatedName)[0];if(!relatedHist){relatedHist=histograms.createHistogram(relatedName,histogram.unit,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,summaryOptions:{count:false,max:false,min:false,sum:false,},});let relatedNames=histogram.diagnostics.get('breakdown');if(!relatedNames){relatedNames=new tr.v.d.RelatedNameMap();histogram.diagnostics.set('breakdown',relatedNames);}
 relatedNames.set(category,relatedName);}
 relatedHist.addSample(breakdown.total,{breakdown:tr.v.d.Breakdown.fromEntries(Object.entries(breakdown.events)),});}}}
-function loadingMetric(histograms,model){const firstPaintHistogram=histograms.createHistogram('timeToFirstPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first paint',summaryOptions:SUMMARY_OPTIONS,});const firstContentfulPaintHistogram=histograms.createHistogram('timeToFirstContentfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first contentful paint',summaryOptions:SUMMARY_OPTIONS,});const firstContentfulPaintCpuTimeHistogram=histograms.createHistogram('cpuTimeToFirstContentfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'CPU time to first contentful paint',summaryOptions:SUMMARY_OPTIONS,});const onLoadHistogram=histograms.createHistogram('timeToOnload',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to onload. '+'This is temporary metric used for PCv1/v2 sanity checking',summaryOptions:SUMMARY_OPTIONS,});const firstMeaningfulPaintHistogram=histograms.createHistogram('timeToFirstMeaningfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first meaningful paint',summaryOptions:SUMMARY_OPTIONS,});const firstMeaningfulPaintCpuTimeHistogram=histograms.createHistogram('cpuTimeToFirstMeaningfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'CPU time to first meaningful paint',summaryOptions:SUMMARY_OPTIONS,});const timeToInteractiveHistogram=histograms.createHistogram('timeToInteractive',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time to Interactive',summaryOptions:SUMMARY_OPTIONS,});const timeToFirstCpuIdleHistogram=histograms.createHistogram('timeToFirstCpuIdle',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time to First CPU Idle',summaryOptions:SUMMARY_OPTIONS,});const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const pid in chromeHelper.rendererHelpers){const rendererHelper=chromeHelper.rendererHelpers[pid];if(rendererHelper.isChromeTracingUI)continue;const samplesSet=collectLoadingMetricsForRenderer(rendererHelper);addSamplesToHistogram(samplesSet.firstPaintSamples,firstPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstContentfulPaintSamples,firstContentfulPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstContentfulPaintCpuTimeSamples,firstContentfulPaintCpuTimeHistogram,histograms);addSamplesToHistogram(samplesSet.onLoadSamples,onLoadHistogram,histograms);}
-const samplesSet=collectMetricsFromLoadExpectations(model,chromeHelper);addSamplesToHistogram(samplesSet.firstMeaningfulPaintSamples,firstMeaningfulPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstMeaningfulPaintCpuTimeSamples,firstMeaningfulPaintCpuTimeHistogram,histograms);addSamplesToHistogram(samplesSet.interactiveSamples,timeToInteractiveHistogram,histograms);addSamplesToHistogram(samplesSet.firstCpuIdleSamples,timeToFirstCpuIdleHistogram,histograms);}
-tr.metrics.MetricRegistry.register(loadingMetric);return{loadingMetric,getNetworkEventsInRange,};});'use strict';tr.exportTo('tr.metrics',function(){const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY=tr.v.HistogramBinBoundaries.createExponential(1,1000,50);function spaNavigationMetric(histograms,model){const histogram=new tr.v.Histogram('spaNavigationStartToFpDuration',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY);histogram.description='Latency between the input event causing'+' a SPA navigation and the first paint event after it';histogram.customizeSummaryOptions({count:false,sum:false,});const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!modelHelper){return;}
+function loadingMetric(histograms,model){const firstPaintHistogram=histograms.createHistogram('timeToFirstPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first paint',summaryOptions:SUMMARY_OPTIONS,});const firstContentfulPaintHistogram=histograms.createHistogram('timeToFirstContentfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first contentful paint',summaryOptions:SUMMARY_OPTIONS,});const firstContentfulPaintCpuTimeHistogram=histograms.createHistogram('cpuTimeToFirstContentfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'CPU time to first contentful paint',summaryOptions:SUMMARY_OPTIONS,});const onLoadHistogram=histograms.createHistogram('timeToOnload',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to onload. '+'This is temporary metric used for PCv1/v2 sanity checking',summaryOptions:SUMMARY_OPTIONS,});const firstMeaningfulPaintHistogram=histograms.createHistogram('timeToFirstMeaningfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'time to first meaningful paint',summaryOptions:SUMMARY_OPTIONS,});const firstMeaningfulPaintCpuTimeHistogram=histograms.createHistogram('cpuTimeToFirstMeaningfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'CPU time to first meaningful paint',summaryOptions:SUMMARY_OPTIONS,});const timeToInteractiveHistogram=histograms.createHistogram('timeToInteractive',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time to Interactive',summaryOptions:SUMMARY_OPTIONS,});const timeToFirstCpuIdleHistogram=histograms.createHistogram('timeToFirstCpuIdle',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time to First CPU Idle',summaryOptions:SUMMARY_OPTIONS,});const aboveTheFoldLoadedToVisibleHistogram=histograms.createHistogram('aboveTheFoldLoadedToVisible',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time from first visible to load for AMP pages only.',summaryOptions:SUMMARY_OPTIONS,});const firstViewportReadyHistogram=histograms.createHistogram('timeToFirstViewportReady',timeDurationInMs_smallerIsBetter,[],{binBoundaries:TIME_TO_INTERACTIVE_BOUNDARIES,description:'Time from navigation to load for AMP pages only. ',summaryOptions:SUMMARY_OPTIONS,});const largestImagePaintHistogram=histograms.createHistogram('largestImagePaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'Time to Largest Image Paint',summaryOptions:SUMMARY_OPTIONS,});const largestTextPaintHistogram=histograms.createHistogram('largestTextPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'Time to Largest Text Paint',summaryOptions:SUMMARY_OPTIONS,});const largestContentfulPaintHistogram=histograms.createHistogram('largestContentfulPaint',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'Time to Largest Contentful Paint',summaryOptions:SUMMARY_OPTIONS,});const layoutShiftHistogram=histograms.createHistogram('mainFrameCumulativeLayoutShift',unitlessNumber_smallerIsBetter,[],{binBoundaries:LAYOUT_SHIFT_SCORE_BOUNDARIES,description:'Main Frame Document Cumulative Layout Shift Score',summaryOptions:SUMMARY_OPTIONS,});const visuallyCompleteBeforeSomeTimeHistogram=histograms.createHistogram('visuallyCompleteBeforeSomeTime',unitlessNumber_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'Fraction of pixels in the initial viewport reaching '+'final value within some time interval after navigation start.',summaryOptions:SUMMARY_OPTIONS,});const speedIndexHistogram=histograms.createHistogram('speedIndex',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:' the average time at which visible parts of the'+' page are displayed (in ms).',summaryOptions:SUMMARY_OPTIONS,});const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const pid in chromeHelper.rendererHelpers){const rendererHelper=chromeHelper.rendererHelpers[pid];if(rendererHelper.isChromeTracingUI)continue;const samplesSet=collectLoadingMetricsForRenderer(rendererHelper);const lcpSamples=findLargestContentfulPaintHistogramSamples(chromeHelper.browserHelper.mainThread.sliceGroup.slices);addSamplesToHistogram(lcpSamples,largestContentfulPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstPaintSamples,firstPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstContentfulPaintSamples,firstContentfulPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstContentfulPaintCpuTimeSamples,firstContentfulPaintCpuTimeHistogram,histograms);addSamplesToHistogram(samplesSet.onLoadSamples,onLoadHistogram,histograms);addSamplesToHistogram(samplesSet.aboveTheFoldLoadedToVisibleSamples,aboveTheFoldLoadedToVisibleHistogram,histograms);addSamplesToHistogram(samplesSet.firstViewportReadySamples,firstViewportReadyHistogram,histograms);addSamplesToHistogram(samplesSet.largestImagePaintSamples,largestImagePaintHistogram,histograms);addSamplesToHistogram(samplesSet.largestTextPaintSamples,largestTextPaintHistogram,histograms);addSamplesToHistogram(samplesSet.layoutShiftSamples,layoutShiftHistogram,histograms);addSamplesToHistogram(samplesSet.speedIndexSamples,speedIndexHistogram,histograms);}
+const samplesSet=collectMetricsFromLoadExpectations(model,chromeHelper);addSamplesToHistogram(samplesSet.firstMeaningfulPaintSamples,firstMeaningfulPaintHistogram,histograms);addSamplesToHistogram(samplesSet.firstMeaningfulPaintCpuTimeSamples,firstMeaningfulPaintCpuTimeHistogram,histograms);addSamplesToHistogram(samplesSet.interactiveSamples,timeToInteractiveHistogram,histograms);addSamplesToHistogram(samplesSet.firstCpuIdleSamples,timeToFirstCpuIdleHistogram,histograms);addSamplesToHistogram(samplesSet.visuallyCompleteBeforeSomeTimeSamples,visuallyCompleteBeforeSomeTimeHistogram,histograms);}
+tr.metrics.MetricRegistry.register(loadingMetric);return{loadingMetric,createBreakdownDiagnostic};});'use strict';tr.exportTo('tr.metrics',function(){const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY=tr.v.HistogramBinBoundaries.createExponential(1,1000,50);function spaNavigationMetric(histograms,model){const histogram=new tr.v.Histogram('spaNavigationStartToFpDuration',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY);histogram.description='Latency between the input event causing'+' a SPA navigation and the first paint event after it';histogram.customizeSummaryOptions({count:false,sum:false,});const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!modelHelper){return;}
 const rendererHelpers=modelHelper.rendererHelpers;if(!rendererHelpers){return;}
 const browserHelper=modelHelper.browserHelper;for(const rendererHelper of Object.values(rendererHelpers)){const spaNavigations=tr.metrics.findSpaNavigationsOnRenderer(rendererHelper,browserHelper);for(const spaNav of spaNavigations){let beginTs=0;if(spaNav.navStartCandidates.inputLatencyAsyncSlice){const beginData=spaNav.navStartCandidates.inputLatencyAsyncSlice.args.data;beginTs=model.convertTimestampToModelTime('traceEventClock',beginData.INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT.time);}else{beginTs=spaNav.navStartCandidates.goToIndexSlice.start;}
-const rangeOfInterest=tr.b.math.Range.fromExplicitRange(beginTs,spaNav.firstPaintEvent.start);const networkEvents=tr.metrics.sh.getNetworkEventsInRange(rendererHelper.process,rangeOfInterest);const breakdownDict=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,rangeOfInterest);const breakdownDiagnostic=new tr.v.d.Breakdown();breakdownDiagnostic.colorScheme=tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;for(const label in breakdownDict){breakdownDiagnostic.set(label,parseInt(breakdownDict[label].total*1e3)/1e3);}
+const rangeOfInterest=tr.b.math.Range.fromExplicitRange(beginTs,spaNav.firstPaintEvent.start);const networkEvents=tr.e.chrome.EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,rangeOfInterest);const breakdownDict=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,rangeOfInterest);const breakdownDiagnostic=new tr.v.d.Breakdown();breakdownDiagnostic.colorScheme=tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;for(const label in breakdownDict){breakdownDiagnostic.set(label,parseInt(breakdownDict[label].total*1e3)/1e3);}
 histogram.addSample(rangeOfInterest.duration,{'Breakdown of [navStart, firstPaint]':breakdownDiagnostic,'Start':new tr.v.d.RelatedEventSet(spaNav.navigationStart),'End':new tr.v.d.RelatedEventSet(spaNav.firstPaintEvent),'Navigation infos':new tr.v.d.GenericSet([{url:spaNav.url,pid:rendererHelper.pid,navStart:beginTs,firstPaint:spaNav.firstPaintEvent.start}]),});}}
 histograms.addHistogram(histogram);}
 tr.metrics.MetricRegistry.register(spaNavigationMetric);return{spaNavigationMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const LATENCY_BOUNDS=tr.v.HistogramBinBoundaries.createLinear(0,20,100);function clockSyncLatencyMetric(values,model){const domains=Array.from(model.clockSyncManager.domainsSeen).sort();for(let i=0;i<domains.length;i++){for(let j=i+1;j<domains.length;j++){const latency=model.clockSyncManager.getTimeTransformerError(domains[i],domains[j]);const hist=new tr.v.Histogram('clock_sync_latency_'+
@@ -8398,9 +8642,11 @@
 allProcessCpuTime+=processCpuTime;}
 let normalizedAllProcessCpuTime=0;if(rangeOfInterest.duration>0){normalizedAllProcessCpuTime=allProcessCpuTime/rangeOfInterest.duration;}
 const unit=tr.b.Unit.byName.normalizedPercentage_smallerIsBetter;const cpuTimeHist=new tr.v.Histogram('cpu_time_percentage',unit,CPU_TIME_PERCENTAGE_BOUNDARIES);cpuTimeHist.description='Percent CPU utilization, normalized against a single core. Can be '+'greater than 100% if machine has multiple cores.';cpuTimeHist.customizeSummaryOptions({avg:true,count:false,max:false,min:false,std:false,sum:false});cpuTimeHist.addSample(normalizedAllProcessCpuTime);histograms.addHistogram(cpuTimeHist);}
-tr.metrics.MetricRegistry.register(cpuTimeMetric,{supportsRangeOfInterest:true});return{cpuTimeMetric,};});'use strict';tr.exportTo('tr.v.d',function(){function addRelatedNames(histograms){for(const hist of histograms){const relatedNames=new Set();for(const[name,diagnostic]of hist.diagnostics){if(diagnostic instanceof tr.v.d.RelatedHistogramMap){for(const[relationshipName,relatedHist]of diagnostic){relatedNames.add(relatedHist.name);}}}
-if(relatedNames.size){hist.diagnostics.set(tr.v.d.RESERVED_NAMES.RELATED_NAMES,new tr.v.d.GenericSet(relatedNames));}}}
-return{addRelatedNames,};});'use strict';tr.exportTo('tr.v',function(){class HistogramGrouping{constructor(key,callback){this.key_=key;this.callback_=callback;HistogramGrouping.BY_KEY.set(key,this);}
+tr.metrics.MetricRegistry.register(cpuTimeMetric,{supportsRangeOfInterest:true});return{cpuTimeMetric,};});'use strict';tr.exportTo('tr.v',function(){class HistogramDeserializer{static deserialize(data){const deserializer=new HistogramDeserializer(data[0],data[1]);return data.slice(2).map(datum=>tr.v.Histogram.deserialize(datum,deserializer));}
+constructor(objects,diagnostics){this.objects_=objects;this.diagnostics_=[];for(const[type,diagnosticsByName]of Object.entries(diagnostics||{})){for(const[name,diagnosticsById]of Object.entries(diagnosticsByName)){for(const[id,data]of Object.entries(diagnosticsById)){const diagnostic=tr.v.d.Diagnostic.deserialize(type,data,this);this.diagnostics_[parseInt(id)]={name,diagnostic};}}}}
+getObject(id){return this.objects_[id];}
+getDiagnostic(id){return this.diagnostics_[parseInt(id)];}}
+return{HistogramDeserializer};});'use strict';tr.exportTo('tr.v',function(){class HistogramGrouping{constructor(key,callback){this.key_=key;this.callback_=callback;HistogramGrouping.BY_KEY.set(key,this);}
 get key(){return this.key_;}
 get callback(){return this.callback_;}
 get label(){return this.key;}
@@ -8421,33 +8667,31 @@
 class DateRangeGrouping extends HistogramGrouping{constructor(name){super(name,undefined);this.callback_=this.compute_.bind(this);}
 compute_(hist){const diag=hist.diagnostics.get(this.key);if(diag===undefined)return'';return diag.toString();}}
 DateRangeGrouping.NAMES=[tr.v.d.RESERVED_NAMES.BENCHMARK_START,tr.v.d.RESERVED_NAMES.TRACE_START,];for(const name of DateRangeGrouping.NAMES){new DateRangeGrouping(name);}
-return{HistogramGrouping,GenericSetGrouping,DateRangeGrouping,};});'use strict';tr.exportTo('tr.v',function(){class HistogramSet{constructor(opt_histograms){this.histogramsByGuid_=new Map();this.sharedDiagnosticsByGuid_=new Map();if(opt_histograms!==undefined){for(const hist of opt_histograms){this.addHistogram(hist);}}}
+return{HistogramGrouping,GenericSetGrouping,DateRangeGrouping,};});'use strict';tr.exportTo('tr.v',function(){class HistogramSet{constructor(opt_histograms){this.histograms_=new Set();this.sharedDiagnosticsByGuid_=new Map();if(opt_histograms!==undefined){for(const hist of opt_histograms){this.addHistogram(hist);}}}
+has(hist){return this.histograms_.has(hist);}
 createHistogram(name,unit,samples,opt_options){const hist=tr.v.Histogram.create(name,unit,samples,opt_options);this.addHistogram(hist);return hist;}
-addHistogram(hist,opt_diagnostics){if(this.histogramsByGuid_.has(hist.guid)){throw new Error('Cannot add same Histogram twice');}
+addHistogram(hist,opt_diagnostics){if(this.has(hist)){throw new Error('Cannot add same Histogram twice');}
 if(opt_diagnostics!==undefined){if(!(opt_diagnostics instanceof Map)){opt_diagnostics=Object.entries(opt_diagnostics);}
 for(const[name,diagnostic]of opt_diagnostics){hist.diagnostics.set(name,diagnostic);}}
-this.histogramsByGuid_.set(hist.guid,hist);}
+this.histograms_.add(hist);}
 addSharedDiagnosticToAllHistograms(name,diagnostic){this.addSharedDiagnostic(diagnostic);for(const hist of this){hist.diagnostics.set(name,diagnostic);}}
 addSharedDiagnostic(diagnostic){this.sharedDiagnosticsByGuid_.set(diagnostic.guid,diagnostic);}
-get length(){return this.histogramsByGuid_.size;}*[Symbol.iterator](){for(const hist of this.histogramsByGuid_.values()){yield hist;}}
+get length(){return this.histograms_.size;}*[Symbol.iterator](){for(const hist of this.histograms_){yield hist;}}
 getHistogramsNamed(name){return[...this].filter(h=>h.name===name);}
 getHistogramNamed(name){const histograms=this.getHistogramsNamed(name);if(histograms.length===0)return undefined;if(histograms.length>1){throw new Error(`Unexpectedly found multiple histograms named "${name}"`);}
 return histograms[0];}
-lookupHistogram(guid){return this.histogramsByGuid_.get(guid);}
 lookupDiagnostic(guid){return this.sharedDiagnosticsByGuid_.get(guid);}
-resolveRelatedHistograms(){const handleDiagnosticMap=dm=>{for(const[name,diagnostic]of dm){if(diagnostic instanceof tr.v.d.RelatedHistogramMap){diagnostic.resolve(this);}}};for(const hist of this){handleDiagnosticMap(hist.diagnostics);for(const dm of hist.nanDiagnosticMaps){handleDiagnosticMap(dm);}
-for(const bin of hist.allBins){for(const dm of bin.diagnosticMaps){handleDiagnosticMap(dm);}}}}
-importDicts(dicts){for(const dict of dicts){this.importDict(dict);}}
-importDict(dict){if(dict.type&&tr.v.d.Diagnostic.findTypeInfoWithName(dict.type)){this.sharedDiagnosticsByGuid_.set(dict.guid,tr.v.d.Diagnostic.fromDict(dict));}else{const hist=tr.v.Histogram.fromDict(dict);this.addHistogram(hist);hist.diagnostics.resolveSharedDiagnostics(this,true);}}
+deserialize(data){for(const hist of tr.v.HistogramDeserializer.deserialize(data)){this.addHistogram(hist);}}
+importDicts(dicts){if((dicts instanceof Array)&&(dicts.length>2)&&(dicts[0]instanceof Array)){this.deserialize(dicts);return;}
+for(const dict of dicts){this.importLegacyDict(dict);}}
+importLegacyDict(dict){if(dict.type!==undefined){if(dict.type==='TagMap')return;if(!tr.v.d.Diagnostic.findTypeInfoWithName(dict.type)){throw new Error('Unrecognized shared diagnostic type '+dict.type);}
+this.sharedDiagnosticsByGuid_.set(dict.guid,tr.v.d.Diagnostic.fromDict(dict));}else{const hist=tr.v.Histogram.fromDict(dict);this.addHistogram(hist);hist.diagnostics.resolveSharedDiagnostics(this,true);}}
 asDicts(){const dicts=[];for(const diagnostic of this.sharedDiagnosticsByGuid_.values()){dicts.push(diagnostic.asDict());}
 for(const hist of this){dicts.push(hist.asDict());}
 return dicts;}
 get sourceHistograms(){const diagnosticNames=new Set();for(const hist of this){for(const diagnostic of hist.diagnostics.values()){if(!(diagnostic instanceof tr.v.d.RelatedNameMap))continue;for(const name of diagnostic.values()){diagnosticNames.add(name);}}}
-const sourceHistograms=new Map();for(const hist of this){if(!diagnosticNames.has(hist.name)){sourceHistograms.set(hist.guid,hist);}}
-function deleteSourceHistograms(diagnosticMap){for(const[name,diagnostic]of diagnosticMap){if(diagnostic instanceof tr.v.d.RelatedHistogramMap){for(const[name,relatedHist]of diagnostic){sourceHistograms.delete(relatedHist.guid);}}}}
-for(const hist of this){deleteSourceHistograms(hist.diagnostics);for(const dm of hist.nanDiagnosticMaps){deleteSourceHistograms(dm);}
-for(const b of hist.allBins){for(const dm of b.diagnosticMaps){deleteSourceHistograms(dm);}}}
-return new HistogramSet([...sourceHistograms.values()]);}
+const sourceHistograms=new HistogramSet;for(const hist of this){if(!diagnosticNames.has(hist.name)){sourceHistograms.addHistogram(hist);}}
+return sourceHistograms;}
 groupHistogramsRecursively(groupings,opt_skipGroupingCallback){function recurse(histograms,level){if(level===groupings.length){return histograms;}
 const grouping=groupings[level];const groupedHistograms=tr.b.groupIntoMap(histograms,grouping.callback);if(opt_skipGroupingCallback&&opt_skipGroupingCallback(grouping,groupedHistograms)){return recurse(histograms,level+1);}
 for(const[key,group]of groupedHistograms){groupedHistograms.set(key,recurse(group,level+1));}
@@ -8465,9 +8709,8 @@
 for(const diagnostic of deduplicatedDiagnostics){this.sharedDiagnosticsByGuid_.set(diagnostic.guid,diagnostic);}}}}
 buildGroupingsFromTags(names){const tags=new Map();for(const hist of this){for(const name of names){if(!hist.diagnostics.has(name))continue;if(!tags.has(name))tags.set(name,new Set());for(const tag of hist.diagnostics.get(name)){tags.get(name).add(tag);}}}
 const groupings=[];for(const[name,values]of tags){const built=tr.v.HistogramGrouping.buildFromTags(values,name);for(const grouping of built){groupings.push(grouping);}}
-return groupings;}
-mergeRelationships(){for(const hist of this){hist.diagnostics.mergeRelationships(hist);}}}
-return{HistogramSet,};});'use strict';tr.exportTo('tr.e.chrome',function(){function hasTitleAndCategory(event,title,category){return event.title===title&&event.category&&tr.b.getCategoryParts(event.category).includes(category);}
+return groupings;}}
+return{HistogramSet};});'use strict';tr.exportTo('tr.e.chrome',function(){function hasTitleAndCategory(event,title,category){return event.title===title&&event.category&&tr.b.getCategoryParts(event.category).includes(category);}
 function getNavStartTimestamps(rendererHelper){const navStartTimestamps=[];for(const e of rendererHelper.mainThread.sliceGroup.childEvents()){if(hasTitleAndCategory(e,'navigationStart','blink.user_timing')){navStartTimestamps.push(e.start);}}
 return navStartTimestamps;}
 function getInteractiveTimestamps(model){const interactiveTimestampsMap=new Map();const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const rendererHelper of Object.values(chromeHelper.rendererHelpers)){const timestamps=[];interactiveTimestampsMap.set(rendererHelper.pid,timestamps);}
@@ -8494,38 +8737,33 @@
 this.lastTaskIndex_++;}}}
 function maxExpectedQueueingTimeInSlidingWindow(startTime,endTime,windowSize,tasks){if(windowSize<=0){throw Error('The window size must be positive number');}
 if(startTime+windowSize>endTime){throw Error('The sliding window must fit in the specified time range');}
-const sortedTasks=tasks.slice().sort((a,b)=>a.start-b.start);for(let i=1;i<sortedTasks.length;i++){const PRECISION_MS=0.1;if(sortedTasks[i-1].end>sortedTasks[i].start+PRECISION_MS){throw Error('Tasks must not overlap');}
-if(sortedTasks[i-1].end>sortedTasks[i].start){const midpoint=(sortedTasks[i-1].end+sortedTasks[i].start)/2;sortedTasks[i-1].end=midpoint;sortedTasks[i].start=midpoint;}}
+const sortedTasks=tasks.slice().sort((a,b)=>a.start-b.start);for(let i=1;i<sortedTasks.length;i++){if(sortedTasks[i-1].end>sortedTasks[i].start){const midpoint=(sortedTasks[i-1].end+sortedTasks[i].start)/2;sortedTasks[i-1].end=midpoint;sortedTasks[i].start=midpoint;}}
 let endpoints=[];endpoints.push(startTime);endpoints.push(endTime-windowSize);for(const task of tasks){endpoints.push(task.start-windowSize);endpoints.push(task.start);endpoints.push(task.end-windowSize);endpoints.push(task.end);}
 endpoints=endpoints.filter(x=>(startTime<=x&&x+windowSize<=endTime));endpoints.sort((a,b)=>a-b);const slidingWindow=new SlidingWindow(endpoints[0],windowSize,sortedTasks);let maxEQT=0;for(const t of endpoints){slidingWindow.slide(t);maxEQT=Math.max(maxEQT,slidingWindow.getEQT);}
 return maxEQT;}
 return{getPostInteractiveTaskWindows,getNavStartTimestamps,getInteractiveTimestamps,expectedQueueingTime,maxExpectedQueueingTimeInSlidingWindow,weightedExpectedQueueingTime};});'use strict';tr.exportTo('tr.metrics.sh',function(){const WINDOW_SIZE_MS=500;const EQT_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(0.01,WINDOW_SIZE_MS,50);function containsForcedGC_(slice){return slice.findTopmostSlicesRelativeToThisSlice(tr.metrics.v8.utils.isForcedGarbageCollectionEvent).length>0;}
-function createHistogramForEQT_(name,description){const histogram=new tr.v.Histogram(name,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,EQT_BOUNDARIES);histogram.customizeSummaryOptions({avg:false,count:false,max:true,min:false,std:false,sum:false,});histogram.description=description;return histogram;}
-function expectedQueueingTimeMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const rendererHelpers=Object.values(chromeHelper.rendererHelpers);const rendererToInteractiveTimestamps=tr.e.chrome.getInteractiveTimestamps(model);addExpectedQueueingTimeMetric_('renderer_eqt',event=>{return{start:event.start,duration:event.duration};},false,rendererHelpers,rendererToInteractiveTimestamps,histograms,model);addExpectedQueueingTimeMetric_('renderer_eqt_cpu',event=>{return{start:event.cpuStart,duration:event.cpuDuration};},true,rendererHelpers,rendererToInteractiveTimestamps,histograms,model);}
-function addExpectedQueueingTimeMetric_(eqtName,getEventTimes,isCpuTime,rendererHelpers,rendererToInteractiveTimestamps,histograms,model){function getTasks(rendererHelper){const tasks=[];for(const slice of
+function getOrCreateHistogram_(histograms,name,description){return histograms.getHistogramNamed(name)||histograms.createHistogram(name,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{binBoundaries:EQT_BOUNDARIES,description,summaryOptions:{avg:false,count:false,max:true,min:false,std:false,sum:false,},});}
+function expectedQueueingTimeMetric(histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const rendererHelpers=Object.values(chromeHelper.rendererHelpers);addExpectedQueueingTimeMetric_('renderer_eqt',event=>{return{start:event.start,duration:event.duration};},false,rendererHelpers,histograms,model);}
+function addExpectedQueueingTimeMetric_(eqtName,getEventTimes,isCpuTime,rendererHelpers,histograms,model){function getTasks(rendererHelper){const tasks=[];for(const slice of
 tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks(rendererHelper.mainThread)){const times=getEventTimes(slice);if(times.duration>0&&!containsForcedGC_(slice)){tasks.push({start:times.start,end:times.start+times.duration});}}
 return tasks;}
-const totalHistogram=createHistogramForEQT_(`total:${WINDOW_SIZE_MS}ms_window:${eqtName}`,`The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window`+' for a given renderer');const interactiveHistogram=createHistogramForEQT_(`interactive:${WINDOW_SIZE_MS}ms_window:${eqtName}`,`The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window`+' for a given renderer while the page is interactive');for(const rendererHelper of rendererHelpers){if(rendererHelper.isChromeTracingUI)continue;if(rendererHelper.mainThread.bounds.duration<WINDOW_SIZE_MS)continue;const tasks=getTasks(rendererHelper);totalHistogram.addSample(tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(rendererHelper.mainThread.bounds.min,rendererHelper.mainThread.bounds.max,WINDOW_SIZE_MS,tasks));const interactiveTimestamps=rendererToInteractiveTimestamps.get(rendererHelper.pid);if(interactiveTimestamps.length===0)continue;if(interactiveTimestamps.length>1){continue;}
-const interactiveWindow=tr.b.math.Range.fromExplicitRange(interactiveTimestamps[0],Infinity).findIntersection(rendererHelper.mainThread.bounds);interactiveHistogram.addSample(tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(interactiveWindow.min,interactiveWindow.max,WINDOW_SIZE_MS,tasks));}
-addV8ContributionToExpectedQueueingTime_(eqtName,getEventTimes,isCpuTime,totalHistogram,interactiveHistogram,rendererToInteractiveTimestamps,histograms,model);histograms.addHistogram(totalHistogram);histograms.addHistogram(interactiveHistogram);}
-function addV8ContributionToExpectedQueueingTime_(eqtName,getEventTimes,isCpuTime,totalEqtHistogram,interactiveEqtHistogram,rendererToInteractiveTimestamps,histograms,model){if(!model.categories.includes('v8'))return;const breakdownForTotal=new tr.v.d.RelatedHistogramMap();const breakdownForInteractive=new tr.v.d.RelatedHistogramMap();const eventNamesWithTaskExtractors=getV8EventNamesWithTaskExtractors_(getEventTimes);if(!isCpuTime){const taskExtractorsUsingRCS=getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes);for(const[eventName,getTasks]of taskExtractorsUsingRCS){eventNamesWithTaskExtractors.set(eventName,getTasks);}}
-for(const[eventName,getTasks]of eventNamesWithTaskExtractors){const contribution=contributionToExpectedQueueingTime_(eqtName,eventName,getTasks,rendererToInteractiveTimestamps,histograms,model);breakdownForTotal.set(eventName,contribution.total);breakdownForInteractive.set(eventName,contribution.interactive);}
-totalEqtHistogram.diagnostics.set('v8',breakdownForTotal);interactiveEqtHistogram.diagnostics.set('v8',breakdownForInteractive);}
+const totalHistogram=getOrCreateHistogram_(histograms,`total:${WINDOW_SIZE_MS}ms_window:${eqtName}`,`The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window`+' for a given renderer');for(const rendererHelper of rendererHelpers){if(rendererHelper.isChromeTracingUI)continue;if(rendererHelper.mainThread.bounds.duration<WINDOW_SIZE_MS)continue;const tasks=getTasks(rendererHelper);const totalBreakdown=getV8Contribution_(eqtName,getEventTimes,isCpuTime,totalHistogram,histograms,rendererHelper,model);totalHistogram.addSample(tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(rendererHelper.mainThread.bounds.min,rendererHelper.mainThread.bounds.max,WINDOW_SIZE_MS,tasks),{v8:totalBreakdown});}}
+function getV8Contribution_(eqtName,getEventTimes,isCpuTime,totalEqtHistogram,histograms,rendererHelper,model){if(!model.categories.includes('v8'))return null;const totalBreakdown=new tr.v.d.Breakdown();const eventNamesWithTaskExtractors=getV8EventNamesWithTaskExtractors_(getEventTimes);if(!isCpuTime){const taskExtractorsUsingRCS=getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes);for(const[eventName,getTasks]of taskExtractorsUsingRCS){eventNamesWithTaskExtractors.set(eventName,getTasks);}}
+let totalNames=totalEqtHistogram.diagnostics.get('v8');if(!totalNames){totalNames=new tr.v.d.RelatedNameMap();totalEqtHistogram.diagnostics.set('v8',totalNames);}
+for(const[eventName,getTasks]of eventNamesWithTaskExtractors){const totalHistogram=getOrCreateHistogram_(histograms,`total:${WINDOW_SIZE_MS}ms_window:${eqtName}:${eventName}`,`Contribution to the expected queueing time by ${eventName}`+' for a given renderer. It is computed as the maximum EQT in'+` a ${WINDOW_SIZE_MS}ms sliding window after shrinking top-level`+` tasks to contain only ${eventName} subevents`);const tasks=getTasks(rendererHelper);const totalSample=tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(rendererHelper.mainThread.bounds.min,rendererHelper.mainThread.bounds.max,WINDOW_SIZE_MS,tasks);totalHistogram.addSample(totalSample);totalBreakdown.set(eventName,totalSample);totalNames.set(eventName,totalHistogram.name);}
+return totalBreakdown;}
 function getV8EventNamesWithTaskExtractors_(getEventTimes,cpuMetrics){function durationOfTopmostSubSlices(slice,predicate,excludePredicate){let duration=0;for(const sub of slice.findTopmostSlicesRelativeToThisSlice(predicate)){duration+=getEventTimes(sub).duration;if(excludePredicate!==null&&excludePredicate!==undefined){duration-=durationOfTopmostSubSlices(sub,excludePredicate);}}
 return duration;}
 function taskExtractor(predicate,excludePredicate){return function(rendererHelper){const slices=tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks(rendererHelper.mainThread);const result=[];for(const slice of slices){const times=getEventTimes(slice);if(times.duration>0&&!containsForcedGC_(slice)){const duration=durationOfTopmostSubSlices(slice,predicate,excludePredicate);result.push({start:times.start,end:times.start+duration});}}
 return result;};}
-return new Map([['v8',taskExtractor(tr.metrics.v8.utils.isV8Event)],['v8:execute',taskExtractor(tr.metrics.v8.utils.isV8ExecuteEvent)],['v8:gc',taskExtractor(tr.metrics.v8.utils.isGarbageCollectionEvent)],['v8:gc:full-mark-compactor',taskExtractor(tr.metrics.v8.utils.isFullMarkCompactorEvent)],['v8:gc:incremental-marking',taskExtractor(tr.metrics.v8.utils.isIncrementalMarkingEvent)],['v8:gc:latency-mark-compactor',taskExtractor(tr.metrics.v8.utils.isLatencyMarkCompactorEvent)],['v8:gc:memory-mark-compactor',taskExtractor(tr.metrics.v8.utils.isMemoryMarkCompactorEvent)],['v8:gc:scavenger',taskExtractor(tr.metrics.v8.utils.isScavengerEvent)]]);}
+return new Map([['v8',taskExtractor(tr.metrics.v8.utils.isV8Event)],['v8:execute',taskExtractor(tr.metrics.v8.utils.isV8ExecuteEvent)],['v8:gc',taskExtractor(tr.metrics.v8.utils.isGarbageCollectionEvent)]]);}
 function extractTaskRCS(getEventTimes,predicate,rendererHelper){const result=[];for(const topSlice of
 rendererHelper.mainThread.sliceGroup.topLevelSlices){const times=getEventTimes(topSlice);if(times.duration<=0||containsForcedGC_(topSlice)){continue;}
 const v8ThreadSlices=[];for(const slice of topSlice.descendentSlices){if(tr.metrics.v8.utils.isV8RCSEvent(slice)){v8ThreadSlices.push(slice);}}
 const runtimeGroupCollection=new tr.e.v8.RuntimeStatsGroupCollection();runtimeGroupCollection.addSlices(v8ThreadSlices);let duration=0;for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){if(predicate(runtimeGroup.name)){duration+=runtimeGroup.time;}}
 duration=tr.b.convertUnit(duration,tr.b.UnitPrefixScale.METRIC.MICRO,tr.b.UnitPrefixScale.METRIC.MILLI);result.push({start:times.start,end:times.start+duration});}
 return result;}
-function getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes){const extractors=new Map();extractors.set('v8:compile_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileRCSCategory,rendererHelper));extractors.set('v8:compile:optimize_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileOptimizeRCSCategory,rendererHelper));extractors.set('v8:compile:parse_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileParseRCSCategory,rendererHelper));extractors.set('v8:compile:compile-unoptimize_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileUnoptimizeRCSCategory,rendererHelper));return extractors;}
-function contributionToExpectedQueueingTime_(eqtName,eventName,getTasks,rendererToInteractiveTimestamps,histograms,model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const totalHistogram=createHistogramForEQT_(`total:${WINDOW_SIZE_MS}ms_window:${eqtName}:${eventName}`,`Contribution to the expected queueing time by ${eventName}`+' for a given renderer. It is computed as the maximum EQT in'+` a ${WINDOW_SIZE_MS}ms sliding window after shrinking top-level`+` tasks to contain only ${eventName} subevents`);const interactiveHistogram=createHistogramForEQT_(`interactive:${WINDOW_SIZE_MS}ms_window:${eqtName}:${eventName}`,`Contribution to the expected queueing time by ${eventName}`+' for a given renderer while the page is interactive. It is computed'+` as the maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window after`+` shrinking top-level tasks to contain only ${eventName} subevents`);const rendererHelpers=Object.values(chromeHelper.rendererHelpers);for(const rendererHelper of rendererHelpers){if(rendererHelper.isChromeTracingUI)continue;const tasks=getTasks(rendererHelper);totalHistogram.addSample(tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(rendererHelper.mainThread.bounds.min,rendererHelper.mainThread.bounds.max,WINDOW_SIZE_MS,tasks));const interactiveTimestamps=rendererToInteractiveTimestamps.get(rendererHelper.pid);if(interactiveTimestamps.length===0)continue;if(interactiveTimestamps.length>1){continue;}
-const interactiveWindow=tr.b.math.Range.fromExplicitRange(interactiveTimestamps[0],Infinity).findIntersection(rendererHelper.mainThread.bounds);interactiveHistogram.addSample(tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(interactiveWindow.min,interactiveWindow.max,WINDOW_SIZE_MS,tasks));}
-histograms.addHistogram(totalHistogram);histograms.addHistogram(interactiveHistogram);return{total:totalHistogram,interactive:interactiveHistogram};}
+function getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes){const extractors=new Map();extractors.set('v8:compile_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileRCSCategory,rendererHelper));extractors.set('v8:compile:optimize_rcs',rendererHelper=>extractTaskRCS(getEventTimes,tr.metrics.v8.utils.isCompileOptimizeRCSCategory,rendererHelper));return extractors;}
 tr.metrics.MetricRegistry.register(expectedQueueingTimeMetric);return{expectedQueueingTimeMetric,};});'use strict';tr.exportTo('tr.b',function(){function MultiDimensionalViewNode(title,valueCount){this.title=title;const dimensions=title.length;this.children=new Array(dimensions);for(let i=0;i<dimensions;i++){this.children[i]=new Map();}
 this.values=new Array(valueCount);for(let v=0;v<valueCount;v++){this.values[v]={self:0,total:0,totalState:NOT_PROVIDED};}}
 MultiDimensionalViewNode.TotalState={NOT_PROVIDED:0,LOWER_BOUND:1,EXACT:2};const NOT_PROVIDED=MultiDimensionalViewNode.TotalState.NOT_PROVIDED;const LOWER_BOUND=MultiDimensionalViewNode.TotalState.LOWER_BOUND;const EXACT=MultiDimensionalViewNode.TotalState.EXACT;MultiDimensionalViewNode.prototype={get subRows(){return Array.from(this.children[0].values());}};function MultiDimensionalViewBuilder(dimensions,valueCount){if(typeof(dimensions)!=='number'||dimensions<0){throw new Error('Dimensions must be a non-negative number');}
@@ -8671,7 +8909,26 @@
 if(componentBreakdown.size>0){diagnostics.set('components',componentBreakdown);}
 if(diagnostics.size===0)return undefined;return diagnostics;}
 function buildMemoryNumericFromNode(name,node,unit){const histogram=new tr.v.Histogram(name,unit,BOUNDARIES_FOR_UNIT_MAP.get(unit));node.values.forEach(v=>histogram.addSample(v.total,buildSampleDiagnostics(v,node)));return histogram;}
-tr.metrics.MetricRegistry.register(memoryMetric,{supportsRangeOfInterest:true});return{memoryMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const CHROME_POWER_GRACE_PERIOD_MS=1;function createEmptyHistogram_(interval,histograms){if(interval.perSecond){return{perSecond:true,energy:histograms.createHistogram(`${interval.name}:power`,tr.b.Unit.byName.powerInWatts_smallerIsBetter,[],{description:`Energy consumption rate for ${interval.description}`,summaryOptions:{avg:true,count:false,max:true,min:true,std:false,sum:false,},}),};}
+tr.metrics.MetricRegistry.register(memoryMetric,{supportsRangeOfInterest:true});return{memoryMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const BYTE_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1,1e9,1e2);function nativeCodeResidentMemoryMetric(histograms,model){const histogram=new tr.v.Histogram('NativeCodeResidentMemory',tr.b.Unit.byName.sizeInBytes_smallerIsBetter,BYTE_BOUNDARIES);for(const slice of model.getDescendantEvents()){if(slice.category==='disabled-by-default-memory-infra'&&slice.title==='ReportGlobalNativeCodeResidentMemoryKb'&&slice.args.NativeCodeResidentMemory){histogram.addSample(slice.args.NativeCodeResidentMemory);}}
+histograms.addHistogram(histogram);}
+tr.metrics.MetricRegistry.register(nativeCodeResidentMemoryMetric);return{nativeCodeResidentMemoryMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const EventFinderUtils=tr.e.chrome.EventFinderUtils;const LOADING_METRIC_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,1e3,20).addLinearBins(3e3,20).addExponentialBins(20e3,20);const SUMMARY_OPTIONS={avg:true,count:false,max:false,min:false,std:false,sum:false,};function addSamplesToHistogram(pairInfo,breakdownTree,histogram,histograms,diagnostics){histogram.addSample(pairInfo.end-pairInfo.start,diagnostics);if(!breakdownTree){return;}
+for(const[category,breakdown]of Object.entries(breakdownTree)){const relatedName=`${histogram.name}:${category}`;if(!histograms.getHistogramNamed(relatedName)){const relatedHist=histograms.createHistogram(relatedName,histogram.unit,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,summaryOptions:{count:false,max:false,min:false,sum:false,},});}
+const relatedHist=histograms.getHistogramNamed(relatedName);let relatedNames=histogram.diagnostics.get('breakdown');if(!relatedNames){relatedNames=new tr.v.d.RelatedNameMap();histogram.diagnostics.set('breakdown',relatedNames);}
+relatedNames.set(category,relatedName);relatedHist.addSample(breakdown.total,{breakdown:tr.v.d.Breakdown.fromEntries(Object.entries(breakdown.events)),});}}
+function splitOneRangeIntoPerSecondRanges(startTime,endTime){const results=[];for(let i=0;startTime+(i+1)*1000<=endTime;i+=1){const start=i*1000;const end=(i+1)*1000;results.push({start,end,});}
+return results;}
+function getNavigationInfos(model){const navigationInfos=[];const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);for(const expectation of model.userModel.expectations){if(!(expectation instanceof tr.model.um.LoadExpectation))continue;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
+const rendererHelper=chromeHelper.rendererHelpers[expectation.renderProcess.pid];navigationInfos.push({navigationStart:expectation.navigationStart,rendererHelper,url:expectation.url});}
+navigationInfos.forEach((navInfo,i)=>{if(i===navigationInfos.length-1){navInfo.navigationEndTime=model.bounds.max;}else{navInfo.navigationEndTime=navigationInfos[i+1].navigationStart.start;}});return navigationInfos;}
+function getRendererHelpers(model){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const rendererHelpers=[];for(const pid in chromeHelper.rendererHelpers){const rendererHelper=chromeHelper.rendererHelpers[pid];if(rendererHelper.isChromeTracingUI)continue;rendererHelpers.push(rendererHelper);}
+return rendererHelpers;}
+function getWallTimeBreakdownTree(rendererHelper,start,end){const startEndRange=tr.b.math.Range.fromExplicitRange(start,end);const networkEvents=EventFinderUtils.getNetworkEventsInRange(rendererHelper.process,startEndRange);const breakdownTree=tr.metrics.sh.generateWallClockTimeBreakdownTree(rendererHelper.mainThread,networkEvents,startEndRange);return breakdownTree;}
+function getCpuTimeBreakdownTree(rendererHelper,start,end){const startEndRange=tr.b.math.Range.fromExplicitRange(start,end);const breakdownTree=tr.metrics.sh.generateCpuTimeBreakdownTree(rendererHelper.mainThread,startEndRange);return breakdownTree;}
+function persecondMetric(histograms,model){const rendererHelpers=getRendererHelpers(model);const navigationInfos=getNavigationInfos(model);if(navigationInfos.length===0){return;}
+navigationInfos.forEach(navInfo=>{const navigationStart=navInfo.navigationStart.start;const navigationEnd=navInfo.navigationEndTime;const startEndPairs=splitOneRangeIntoPerSecondRanges(navigationStart,navigationEnd);const breakdownList=startEndPairs.map(p=>{const wallHistogramName=`wall_${p.start}_to_${p.end}`;const wallHistogramDescription=`Wall-clock time ${p.start} to ${p.end} breakdown`;const cpuHistogramName=`cpu_${p.start}_to_${p.end}`;const cpuHistogramDescription=`CPU time ${p.start} to ${p.end} breakdown`;const pid=navInfo.rendererHelper.pid;const breakdownTree=getWallTimeBreakdownTree(navInfo.rendererHelper,navigationStart+p.start,navigationStart+p.end);const cpuBreakdownTree=getCpuTimeBreakdownTree(navInfo.rendererHelper,navigationStart+p.start,navigationStart+p.end);const diagnostics={'Navigation infos':new tr.v.d.GenericSet([{url:navInfo.url,pid:navInfo.rendererHelper.pid,navStart:navigationStart,frameIdRef:navInfo.navigationStart.args.frame}]),'breakdown':tr.metrics.sh.createBreakdownDiagnostic(breakdownTree),};return Object.assign(p,{breakdownTree,cpuBreakdownTree,wallHistogramName,wallHistogramDescription,cpuHistogramName,cpuHistogramDescription,diagnostics,});});breakdownList.forEach(p=>{if(!histograms.getHistogramNamed(p.wallHistogramName)){histograms.createHistogram(p.wallHistogramName,timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:p.wallHistogramDescription,summaryOptions:SUMMARY_OPTIONS,});}
+const wallHistogram=histograms.getHistogramNamed(p.wallHistogramName);addSamplesToHistogram(p,p.breakdownTree,wallHistogram,histograms,p.diagnostics);if(!histograms.getHistogramNamed(p.cpuHistogramName)){histograms.createHistogram(p.cpuHistogramName,timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:p.cpuHistogramDescription,summaryOptions:SUMMARY_OPTIONS,});}
+const cpuHistogram=histograms.getHistogramNamed(p.cpuHistogramName);addSamplesToHistogram(p,p.cpuBreakdownTree,cpuHistogram,histograms,p.diagnostics);});});}
+tr.metrics.MetricRegistry.register(persecondMetric);return{persecondMetric,splitOneRangeIntoPerSecondRanges};});'use strict';tr.exportTo('tr.metrics.sh',function(){const CHROME_POWER_GRACE_PERIOD_MS=1;function createEmptyHistogram_(interval,histograms){if(interval.perSecond){return{perSecond:true,energy:histograms.createHistogram(`${interval.name}:power`,tr.b.Unit.byName.powerInWatts_smallerIsBetter,[],{description:`Energy consumption rate for ${interval.description}`,summaryOptions:{avg:true,count:false,max:true,min:true,std:false,sum:false,},}),};}
 return{perSecond:false,energy:histograms.createHistogram(`${interval.name}:energy`,tr.b.Unit.byName.energyInJoules_smallerIsBetter,[],{description:`Energy consumed in ${interval.description}`,summaryOptions:{avg:false,count:false,max:true,min:true,std:false,sum:true,},}),};}
 function createHistograms_(data,interval,histograms){if(data.histograms[interval.name]===undefined){data.histograms[interval.name]=createEmptyHistogram_(interval,histograms);}
 if(data.histograms[interval.name].perSecond){for(const sample of data.model.device.powerSeries.getSamplesWithinRange(interval.bounds.min,interval.bounds.max)){data.histograms[interval.name].energy.addSample(sample.powerInW);}}else{const energyInJ=data.model.device.powerSeries.getEnergyConsumedInJ(interval.bounds.min,interval.bounds.max);data.histograms[interval.name].energy.addSample(energyInJ);}}
@@ -8688,7 +8945,46 @@
 for(const pid in chromeHelper.rendererHelpers){if(chromeHelper.rendererHelpers[pid].mainThread){chromeBounds.addRange(chromeHelper.rendererHelpers[pid].mainThread.bounds);}}
 return chromeBounds;}
 function powerMetric(histograms,model){const data={model,histograms:{}};for(const interval of computeTimeIntervals_(model)){createHistograms_(data,interval,histograms);}}
-tr.metrics.MetricRegistry.register(powerMetric);return{powerMetric};});'use strict';tr.exportTo('tr.metrics.sh',function(){function computeAnimationThroughput(animationExpectation){if(animationExpectation.frameEvents===undefined||animationExpectation.frameEvents.length===0){throw new Error('Animation missing frameEvents '+
+tr.metrics.MetricRegistry.register(powerMetric);return{powerMetric};});'use strict';tr.exportTo('tr.b.math',function(){function earthMoversDistance(firstHistogram,secondHistogram){const buckets=firstHistogram.length;if(secondHistogram.length!==buckets){throw new Error('Histograms have a different number of bins.');}
+const arrSum=arr=>arr.reduce((a,b)=>a+b,0);if(arrSum(firstHistogram)!==arrSum(secondHistogram)){throw new Error('The histograms\' sizes don\'t match.');}
+let total=0;let remainder=0;for(let bucket=0;bucket<buckets;bucket++){remainder+=secondHistogram[bucket]-
+firstHistogram[bucket];total+=Math.abs(remainder);}
+return total;}
+return{earthMoversDistance,};});'use strict';tr.exportTo('tr.e.chrome',function(){const earthMoversDistance=tr.b.math.earthMoversDistance;class SpeedIndex{static getSnapshotsProgress_(timestampedColorHistograms){const numberOfScreenshots=timestampedColorHistograms.length;const firstHistogram=timestampedColorHistograms[0].colorHistogram;const lastHistogram=timestampedColorHistograms[numberOfScreenshots-1].colorHistogram;const totalDistance=earthMoversDistance(firstHistogram[0],lastHistogram[0])+
+earthMoversDistance(firstHistogram[1],lastHistogram[1])+
+earthMoversDistance(firstHistogram[2],lastHistogram[2]);if(totalDistance===0){return[{value:1,ts:timestampedColorHistograms[0].ts}];}
+const snapshotsProgress=new Array(numberOfScreenshots);for(let i=0;i<numberOfScreenshots;i++){const histogram=timestampedColorHistograms[i].colorHistogram;const distance=earthMoversDistance(histogram[0],lastHistogram[0])+
+earthMoversDistance(histogram[1],lastHistogram[1])+
+earthMoversDistance(histogram[2],lastHistogram[2]);const moved=Math.max(totalDistance-distance,0);snapshotsProgress[i]={value:(moved/totalDistance),ts:timestampedColorHistograms[i].ts};}
+return snapshotsProgress;}
+static speedIndexFromSnapshotsProgress_(snapshotsProgress){if(snapshotsProgress.length===0){throw new Error('No snapshots were provided.');}
+let prevSnapshotTimeTaken=0;let prevSnapshotProgress=0;let speedIndex=0;const numberOfScreenshots=snapshotsProgress.length;for(let i=0;i<numberOfScreenshots;i++){const elapsed=snapshotsProgress[i].ts-prevSnapshotTimeTaken;speedIndex+=elapsed*(1.0-prevSnapshotProgress);prevSnapshotTimeTaken=snapshotsProgress[i].ts;prevSnapshotProgress=snapshotsProgress[i].value;}
+return Math.round(speedIndex);}
+static createColorHistogram(imagePixelValues){const n=imagePixelValues.length;const histogram=new Array(3);for(let j=0;j<3;j++){histogram[j]=new Array(256).fill(0);}
+for(let i=0;i<n;i+=4){const r=imagePixelValues[i];const g=imagePixelValues[i+1];const b=imagePixelValues[i+2];histogram[0][r]++;histogram[1][g]++;histogram[2][b]++;}
+return histogram;}
+static calculateSpeedIndex(timestampedColorHistograms){const snapshotsProgress=SpeedIndex.getSnapshotsProgress_(timestampedColorHistograms);return SpeedIndex.speedIndexFromSnapshotsProgress_(snapshotsProgress);}
+static lineSweep(lineSweepRects,viewport){const verticalSweepEdges=[];const horizontalSweepEdges=[];for(let i=0;i<lineSweepRects.length;i++){const rect=lineSweepRects[i];let left=rect.left;let right=rect.right;let top=rect.top;let bottom=rect.bottom;if(left>viewport.x+viewport.width)continue;if(right<viewport.x)continue;if(top>viewport.y+viewport.height)continue;if(bottom<viewport.y)continue;left=Math.max(left,viewport.y);right=Math.min(right,viewport.y+viewport.width);top=Math.max(top,viewport.y);bottom=Math.min(bottom,viewport.y+viewport.height);verticalSweepEdges.push({id:i,value:left,type:'left'},{id:i,value:right,type:'right'});horizontalSweepEdges.push({id:i,value:top,type:'top'},{id:i,value:bottom,type:'bottom'});}
+verticalSweepEdges.sort((a,b)=>a.value-b.value);horizontalSweepEdges.sort((a,b)=>a.value-b.value);const active=new Array(lineSweepRects.length).fill(false);let area=0;active[verticalSweepEdges[0].id]=true;for(let i=1;i<verticalSweepEdges.length;i++){const currentLine=verticalSweepEdges[i];const previousLine=verticalSweepEdges[i-1];const deltaX=currentLine.value-previousLine.value;if(deltaX===0)continue;let count=0;let firstRect;for(let j=0;j<horizontalSweepEdges.length;j++){if(active[horizontalSweepEdges[j].id]===true){if(horizontalSweepEdges[j].type==='top'){if(count===0){firstRect=j;}
+count++;}else{if(count===1){const deltaY=horizontalSweepEdges[j].value-
+horizontalSweepEdges[firstRect].value;area+=deltaX*deltaY;}
+count--;}}}
+active[currentLine.id]=(currentLine.type==='left');}
+return area;}
+static quadToRect(quad){const left=Math.min(quad[0],quad[2],quad[4]);const right=Math.max(quad[0],quad[2],quad[4]);const top=Math.min(quad[1],quad[3],quad[5]);const bottom=Math.max(quad[1],quad[3],quad[5]);return{left,right,top,bottom};}
+static calculateRectsBasedSpeedIndex(timestampedPaintRects,viewport){const numberOfRects=timestampedPaintRects.length;if(numberOfRects===0){throw new Error('Can\'t calculate speed index without any paint '+'rectangles.');}
+const areaAddedAtTimestamp=new Array(numberOfRects);const rects=[];let previousAreaOfUnion=0;let totalAreaOfUnion=0;for(let i=numberOfRects-1;i>=0;i--){rects.push(timestampedPaintRects[i].rect);const currentAreaOfUnion=SpeedIndex.lineSweep(rects,viewport);areaAddedAtTimestamp[numberOfRects-i-1]={value:currentAreaOfUnion-previousAreaOfUnion,ts:timestampedPaintRects[i].ts};totalAreaOfUnion+=areaAddedAtTimestamp[numberOfRects-i-1].value;previousAreaOfUnion=currentAreaOfUnion;}
+const paintProgressAtTimestamp=new Array(numberOfRects);let lastProgressRecorded=0;for(let i=0;i<numberOfRects;i++){paintProgressAtTimestamp[i]={value:areaAddedAtTimestamp[i].value/totalAreaOfUnion+
+lastProgressRecorded,ts:areaAddedAtTimestamp[i].ts};lastProgressRecorded=paintProgressAtTimestamp[i].value;}
+return SpeedIndex.speedIndexFromSnapshotsProgress_(paintProgressAtTimestamp);}}
+return{SpeedIndex,};});'use strict';tr.exportTo('tr.metrics.sh',function(){const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const SpeedIndex=tr.e.chrome.SpeedIndex;const EventFinderUtils=tr.e.chrome.EventFinderUtils;const BIN_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,1e3,20).addLinearBins(3e3,20).addExponentialBins(20e3,20);const SUMMARY_OPTIONS={avg:true,count:false,max:true,min:true,std:true,sum:false,};function addRectsBasedSpeedIndexSample(samples,rendererHelper,navigationStart,loadDuration,frameID){let viewport;for(const event of EventFinderUtils.getMainThreadEvents(rendererHelper,'viewport','loading')){if(event.args.data.frameID===frameID&&event.start<(navigationStart+loadDuration)){viewport=event.args.data;break;}}
+if(!viewport)return;const timestampedPaintRects=[];for(const event of EventFinderUtils.getMainThreadEvents(rendererHelper,'PaintTracker::LayoutObjectPainted','loading')){if(event.start>=navigationStart&&event.start<navigationStart+loadDuration){const paintRect=event.args.data.visible_new_visual_rect;if(!paintRect)continue;timestampedPaintRects.push({rect:SpeedIndex.quadToRect(paintRect),ts:event.start});}}
+const numberOfRects=timestampedPaintRects.length;if(numberOfRects===0)return;samples.push({value:SpeedIndex.calculateRectsBasedSpeedIndex(timestampedPaintRects,viewport)-navigationStart});}
+function collectRectsBasedSpeedIndexSamplesFromLoadExpectations(model,chromeHelper){const rectsBasedSpeedIndexSamples=[];for(const expectation of model.userModel.expectations){if(!(expectation instanceof tr.model.um.LoadExpectation))continue;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
+const rendererHelper=chromeHelper.rendererHelpers[expectation.renderProcess.pid];addRectsBasedSpeedIndexSample(rectsBasedSpeedIndexSamples,rendererHelper,expectation.navigationStart.start,expectation.duration,expectation.navigationStart.args.frame);}
+return rectsBasedSpeedIndexSamples;}
+function rectsBasedSpeedIndexMetric(histograms,model){const rectsBasedSpeedIndexHistogram=histograms.createHistogram('rectsBasedSpeedIndex',timeDurationInMs_smallerIsBetter,[],{binBoundaries:BIN_BOUNDARIES,description:' the average time at which visible parts of the'+' page are displayed (in ms).',summaryOptions:SUMMARY_OPTIONS,});const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const samples=collectRectsBasedSpeedIndexSamplesFromLoadExpectations(model,chromeHelper);for(const sample of samples){rectsBasedSpeedIndexHistogram.addSample(sample.value);}}
+tr.metrics.MetricRegistry.register(rectsBasedSpeedIndexMetric);return{rectsBasedSpeedIndexMetric};});'use strict';tr.exportTo('tr.metrics.sh',function(){function computeAnimationThroughput(animationExpectation){if(animationExpectation.frameEvents===undefined||animationExpectation.frameEvents.length===0){throw new Error('Animation missing frameEvents '+
 animationExpectation.stableId);}
 const durationInS=tr.b.convertUnit(animationExpectation.duration,tr.b.UnitPrefixScale.METRIC.MILLI,tr.b.UnitPrefixScale.METRIC.NONE);return animationExpectation.frameEvents.length/durationInS;}
 function computeAnimationframeTimeDiscrepancy(animationExpectation){if(animationExpectation.frameEvents===undefined||animationExpectation.frameEvents.length===0){throw new Error('Animation missing frameEvents '+
@@ -8702,15 +8998,134 @@
 ue.stableId);}
 frameTimeDiscrepancyNumeric.addSample(frameTimeDiscrepancy,sampleDiagnosticMap);ue.associatedEvents.forEach(function(event){if(!(event instanceof tr.e.cc.InputLatencyAsyncSlice)){return;}
 latencyNumeric.addSample(event.duration,sampleDiagnosticMap);});}else{throw new Error('Unrecognized stage for '+ue.stableId);}});[responseNumeric,throughputNumeric,frameTimeDiscrepancyNumeric,latencyNumeric].forEach(function(numeric){numeric.customizeSummaryOptions({avg:true,max:true,min:true,std:true});});histograms.addHistogram(responseNumeric);histograms.addHistogram(throughputNumeric);histograms.addHistogram(frameTimeDiscrepancyNumeric);histograms.addHistogram(latencyNumeric);}
-tr.metrics.MetricRegistry.register(responsivenessMetric,{supportsRangeOfInterest:true,requiredCategories:['rail'],});return{responsivenessMetric,};});'use strict';tr.exportTo('tr.metrics.sh',function(){function webviewStartupMetric(histograms,model){const startupWallHist=new tr.v.Histogram('webview_startup_wall_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);startupWallHist.description='WebView startup wall time';const startupCPUHist=new tr.v.Histogram('webview_startup_cpu_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);startupCPUHist.description='WebView startup CPU time';const loadWallHist=new tr.v.Histogram('webview_url_load_wall_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);loadWallHist.description='WebView blank URL load wall time';const loadCPUHist=new tr.v.Histogram('webview_url_load_cpu_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);loadCPUHist.description='WebView blank URL load CPU time';for(const slice of model.getDescendantEvents()){if(!(slice instanceof tr.model.ThreadSlice))continue;if(slice.title==='WebViewStartupInterval'){startupWallHist.addSample(slice.duration);startupCPUHist.addSample(slice.cpuDuration);}
+tr.metrics.MetricRegistry.register(responsivenessMetric,{supportsRangeOfInterest:true,requiredCategories:['rail'],});return{responsivenessMetric,};});var JpegImage=(function jpegImage(){"use strict";var dctZigZag=new Int32Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]);var dctCos1=4017
+var dctSin1=799
+var dctCos3=3406
+var dctSin3=2276
+var dctCos6=1567
+var dctSin6=3784
+var dctSqrt2=5793
+var dctSqrt1d2=2896
+function constructor(){}
+function buildHuffmanTable(codeLengths,values){var k=0,code=[],i,j,length=16;while(length>0&&!codeLengths[length-1])
+length--;code.push({children:[],index:0});var p=code[0],q;for(i=0;i<length;i++){for(j=0;j<codeLengths[i];j++){p=code.pop();p.children[p.index]=values[k];while(p.index>0){p=code.pop();}
+p.index++;code.push(p);while(code.length<=i){code.push(q={children:[],index:0});p.children[p.index]=q.children;p=q;}
+k++;}
+if(i+1<length){code.push(q={children:[],index:0});p.children[p.index]=q.children;p=q;}}
+return code[0].children;}
+function decodeScan(data,offset,frame,components,resetInterval,spectralStart,spectralEnd,successivePrev,successive){var precision=frame.precision;var samplesPerLine=frame.samplesPerLine;var scanLines=frame.scanLines;var mcusPerLine=frame.mcusPerLine;var progressive=frame.progressive;var maxH=frame.maxH,maxV=frame.maxV;var startOffset=offset,bitsData=0,bitsCount=0;function readBit(){if(bitsCount>0){bitsCount--;return(bitsData>>bitsCount)&1;}
+bitsData=data[offset++];if(bitsData==0xFF){var nextByte=data[offset++];if(nextByte){throw new Error("unexpected marker: "+((bitsData<<8)|nextByte).toString(16));}}
+bitsCount=7;return bitsData>>>7;}
+function decodeHuffman(tree){var node=tree,bit;while((bit=readBit())!==null){node=node[bit];if(typeof node==='number')
+return node;if(typeof node!=='object')
+throw new Error("invalid huffman sequence");}
+return null;}
+function receive(length){var n=0;while(length>0){var bit=readBit();if(bit===null)return;n=(n<<1)|bit;length--;}
+return n;}
+function receiveAndExtend(length){var n=receive(length);if(n>=1<<(length-1))
+return n;return n+(-1<<length)+1;}
+function decodeBaseline(component,zz){var t=decodeHuffman(component.huffmanTableDC);var diff=t===0?0:receiveAndExtend(t);zz[0]=(component.pred+=diff);var k=1;while(k<64){var rs=decodeHuffman(component.huffmanTableAC);var s=rs&15,r=rs>>4;if(s===0){if(r<15)
+break;k+=16;continue;}
+k+=r;var z=dctZigZag[k];zz[z]=receiveAndExtend(s);k++;}}
+function decodeDCFirst(component,zz){var t=decodeHuffman(component.huffmanTableDC);var diff=t===0?0:(receiveAndExtend(t)<<successive);zz[0]=(component.pred+=diff);}
+function decodeDCSuccessive(component,zz){zz[0]|=readBit()<<successive;}
+var eobrun=0;function decodeACFirst(component,zz){if(eobrun>0){eobrun--;return;}
+var k=spectralStart,e=spectralEnd;while(k<=e){var rs=decodeHuffman(component.huffmanTableAC);var s=rs&15,r=rs>>4;if(s===0){if(r<15){eobrun=receive(r)+(1<<r)-1;break;}
+k+=16;continue;}
+k+=r;var z=dctZigZag[k];zz[z]=receiveAndExtend(s)*(1<<successive);k++;}}
+var successiveACState=0,successiveACNextValue;function decodeACSuccessive(component,zz){var k=spectralStart,e=spectralEnd,r=0;while(k<=e){var z=dctZigZag[k];var direction=zz[z]<0?-1:1;switch(successiveACState){case 0:var rs=decodeHuffman(component.huffmanTableAC);var s=rs&15,r=rs>>4;if(s===0){if(r<15){eobrun=receive(r)+(1<<r);successiveACState=4;}else{r=16;successiveACState=1;}}else{if(s!==1)
+throw new Error("invalid ACn encoding");successiveACNextValue=receiveAndExtend(s);successiveACState=r?2:3;}
+continue;case 1:case 2:if(zz[z])
+zz[z]+=(readBit()<<successive)*direction;else{r--;if(r===0)
+successiveACState=successiveACState==2?3:0;}
+break;case 3:if(zz[z])
+zz[z]+=(readBit()<<successive)*direction;else{zz[z]=successiveACNextValue<<successive;successiveACState=0;}
+break;case 4:if(zz[z])
+zz[z]+=(readBit()<<successive)*direction;break;}
+k++;}
+if(successiveACState===4){eobrun--;if(eobrun===0)
+successiveACState=0;}}
+function decodeMcu(component,decode,mcu,row,col){var mcuRow=(mcu/mcusPerLine)|0;var mcuCol=mcu%mcusPerLine;var blockRow=mcuRow*component.v+row;var blockCol=mcuCol*component.h+col;decode(component,component.blocks[blockRow][blockCol]);}
+function decodeBlock(component,decode,mcu){var blockRow=(mcu/component.blocksPerLine)|0;var blockCol=mcu%component.blocksPerLine;decode(component,component.blocks[blockRow][blockCol]);}
+var componentsLength=components.length;var component,i,j,k,n;var decodeFn;if(progressive){if(spectralStart===0)
+decodeFn=successivePrev===0?decodeDCFirst:decodeDCSuccessive;else
+decodeFn=successivePrev===0?decodeACFirst:decodeACSuccessive;}else{decodeFn=decodeBaseline;}
+var mcu=0,marker;var mcuExpected;if(componentsLength==1){mcuExpected=components[0].blocksPerLine*components[0].blocksPerColumn;}else{mcuExpected=mcusPerLine*frame.mcusPerColumn;}
+if(!resetInterval)resetInterval=mcuExpected;var h,v;while(mcu<mcuExpected){for(i=0;i<componentsLength;i++)
+components[i].pred=0;eobrun=0;if(componentsLength==1){component=components[0];for(n=0;n<resetInterval;n++){decodeBlock(component,decodeFn,mcu);mcu++;}}else{for(n=0;n<resetInterval;n++){for(i=0;i<componentsLength;i++){component=components[i];h=component.h;v=component.v;for(j=0;j<v;j++){for(k=0;k<h;k++){decodeMcu(component,decodeFn,mcu,j,k);}}}
+mcu++;if(mcu===mcuExpected)break;}}
+bitsCount=0;marker=(data[offset]<<8)|data[offset+1];if(marker<0xFF00){throw new Error("marker was not found");}
+if(marker>=0xFFD0&&marker<=0xFFD7){offset+=2;}
+else
+break;}
+return offset-startOffset;}
+function buildComponentData(frame,component){var lines=[];var blocksPerLine=component.blocksPerLine;var blocksPerColumn=component.blocksPerColumn;var samplesPerLine=blocksPerLine<<3;var R=new Int32Array(64),r=new Uint8Array(64);function quantizeAndInverse(zz,dataOut,dataIn){var qt=component.quantizationTable;var v0,v1,v2,v3,v4,v5,v6,v7,t;var p=dataIn;var i;for(i=0;i<64;i++)
+p[i]=zz[i]*qt[i];for(i=0;i<8;++i){var row=8*i;if(p[1+row]==0&&p[2+row]==0&&p[3+row]==0&&p[4+row]==0&&p[5+row]==0&&p[6+row]==0&&p[7+row]==0){t=(dctSqrt2*p[0+row]+512)>>10;p[0+row]=t;p[1+row]=t;p[2+row]=t;p[3+row]=t;p[4+row]=t;p[5+row]=t;p[6+row]=t;p[7+row]=t;continue;}
+v0=(dctSqrt2*p[0+row]+128)>>8;v1=(dctSqrt2*p[4+row]+128)>>8;v2=p[2+row];v3=p[6+row];v4=(dctSqrt1d2*(p[1+row]-p[7+row])+128)>>8;v7=(dctSqrt1d2*(p[1+row]+p[7+row])+128)>>8;v5=p[3+row]<<4;v6=p[5+row]<<4;t=(v0-v1+1)>>1;v0=(v0+v1+1)>>1;v1=t;t=(v2*dctSin6+v3*dctCos6+128)>>8;v2=(v2*dctCos6-v3*dctSin6+128)>>8;v3=t;t=(v4-v6+1)>>1;v4=(v4+v6+1)>>1;v6=t;t=(v7+v5+1)>>1;v5=(v7-v5+1)>>1;v7=t;t=(v0-v3+1)>>1;v0=(v0+v3+1)>>1;v3=t;t=(v1-v2+1)>>1;v1=(v1+v2+1)>>1;v2=t;t=(v4*dctSin3+v7*dctCos3+2048)>>12;v4=(v4*dctCos3-v7*dctSin3+2048)>>12;v7=t;t=(v5*dctSin1+v6*dctCos1+2048)>>12;v5=(v5*dctCos1-v6*dctSin1+2048)>>12;v6=t;p[0+row]=v0+v7;p[7+row]=v0-v7;p[1+row]=v1+v6;p[6+row]=v1-v6;p[2+row]=v2+v5;p[5+row]=v2-v5;p[3+row]=v3+v4;p[4+row]=v3-v4;}
+for(i=0;i<8;++i){var col=i;if(p[1*8+col]==0&&p[2*8+col]==0&&p[3*8+col]==0&&p[4*8+col]==0&&p[5*8+col]==0&&p[6*8+col]==0&&p[7*8+col]==0){t=(dctSqrt2*dataIn[i+0]+8192)>>14;p[0*8+col]=t;p[1*8+col]=t;p[2*8+col]=t;p[3*8+col]=t;p[4*8+col]=t;p[5*8+col]=t;p[6*8+col]=t;p[7*8+col]=t;continue;}
+v0=(dctSqrt2*p[0*8+col]+2048)>>12;v1=(dctSqrt2*p[4*8+col]+2048)>>12;v2=p[2*8+col];v3=p[6*8+col];v4=(dctSqrt1d2*(p[1*8+col]-p[7*8+col])+2048)>>12;v7=(dctSqrt1d2*(p[1*8+col]+p[7*8+col])+2048)>>12;v5=p[3*8+col];v6=p[5*8+col];t=(v0-v1+1)>>1;v0=(v0+v1+1)>>1;v1=t;t=(v2*dctSin6+v3*dctCos6+2048)>>12;v2=(v2*dctCos6-v3*dctSin6+2048)>>12;v3=t;t=(v4-v6+1)>>1;v4=(v4+v6+1)>>1;v6=t;t=(v7+v5+1)>>1;v5=(v7-v5+1)>>1;v7=t;t=(v0-v3+1)>>1;v0=(v0+v3+1)>>1;v3=t;t=(v1-v2+1)>>1;v1=(v1+v2+1)>>1;v2=t;t=(v4*dctSin3+v7*dctCos3+2048)>>12;v4=(v4*dctCos3-v7*dctSin3+2048)>>12;v7=t;t=(v5*dctSin1+v6*dctCos1+2048)>>12;v5=(v5*dctCos1-v6*dctSin1+2048)>>12;v6=t;p[0*8+col]=v0+v7;p[7*8+col]=v0-v7;p[1*8+col]=v1+v6;p[6*8+col]=v1-v6;p[2*8+col]=v2+v5;p[5*8+col]=v2-v5;p[3*8+col]=v3+v4;p[4*8+col]=v3-v4;}
+for(i=0;i<64;++i){var sample=128+((p[i]+8)>>4);dataOut[i]=sample<0?0:sample>0xFF?0xFF:sample;}}
+var i,j;for(var blockRow=0;blockRow<blocksPerColumn;blockRow++){var scanLine=blockRow<<3;for(i=0;i<8;i++)
+lines.push(new Uint8Array(samplesPerLine));for(var blockCol=0;blockCol<blocksPerLine;blockCol++){quantizeAndInverse(component.blocks[blockRow][blockCol],r,R);var offset=0,sample=blockCol<<3;for(j=0;j<8;j++){var line=lines[scanLine+j];for(i=0;i<8;i++)
+line[sample+i]=r[offset++];}}}
+return lines;}
+function clampTo8bit(a){return a<0?0:a>255?255:a;}
+constructor.prototype={load:function load(path){var xhr=new XMLHttpRequest();xhr.open("GET",path,true);xhr.responseType="arraybuffer";xhr.onload=(function(){var data=new Uint8Array(xhr.response||xhr.mozResponseArrayBuffer);this.parse(data);if(this.onload)
+this.onload();}).bind(this);xhr.send(null);},parse:function parse(data){var offset=0,length=data.length;function readUint16(){var value=(data[offset]<<8)|data[offset+1];offset+=2;return value;}
+function readDataBlock(){var length=readUint16();var array=data.subarray(offset,offset+length-2);offset+=array.length;return array;}
+function prepareComponents(frame){var maxH=0,maxV=0;var component,componentId;for(componentId in frame.components){if(frame.components.hasOwnProperty(componentId)){component=frame.components[componentId];if(maxH<component.h)maxH=component.h;if(maxV<component.v)maxV=component.v;}}
+var mcusPerLine=Math.ceil(frame.samplesPerLine/8/maxH);var mcusPerColumn=Math.ceil(frame.scanLines/8/maxV);for(componentId in frame.components){if(frame.components.hasOwnProperty(componentId)){component=frame.components[componentId];var blocksPerLine=Math.ceil(Math.ceil(frame.samplesPerLine/8)*component.h/maxH);var blocksPerColumn=Math.ceil(Math.ceil(frame.scanLines/8)*component.v/maxV);var blocksPerLineForMcu=mcusPerLine*component.h;var blocksPerColumnForMcu=mcusPerColumn*component.v;var blocks=[];for(var i=0;i<blocksPerColumnForMcu;i++){var row=[];for(var j=0;j<blocksPerLineForMcu;j++)
+row.push(new Int32Array(64));blocks.push(row);}
+component.blocksPerLine=blocksPerLine;component.blocksPerColumn=blocksPerColumn;component.blocks=blocks;}}
+frame.maxH=maxH;frame.maxV=maxV;frame.mcusPerLine=mcusPerLine;frame.mcusPerColumn=mcusPerColumn;}
+var jfif=null;var adobe=null;var pixels=null;var frame,resetInterval;var quantizationTables=[],frames=[];var huffmanTablesAC=[],huffmanTablesDC=[];var fileMarker=readUint16();if(fileMarker!=0xFFD8){throw new Error("SOI not found");}
+fileMarker=readUint16();while(fileMarker!=0xFFD9){var i,j,l;switch(fileMarker){case 0xFF00:break;case 0xFFE0:case 0xFFE1:case 0xFFE2:case 0xFFE3:case 0xFFE4:case 0xFFE5:case 0xFFE6:case 0xFFE7:case 0xFFE8:case 0xFFE9:case 0xFFEA:case 0xFFEB:case 0xFFEC:case 0xFFED:case 0xFFEE:case 0xFFEF:case 0xFFFE:var appData=readDataBlock();if(fileMarker===0xFFE0){if(appData[0]===0x4A&&appData[1]===0x46&&appData[2]===0x49&&appData[3]===0x46&&appData[4]===0){jfif={version:{major:appData[5],minor:appData[6]},densityUnits:appData[7],xDensity:(appData[8]<<8)|appData[9],yDensity:(appData[10]<<8)|appData[11],thumbWidth:appData[12],thumbHeight:appData[13],thumbData:appData.subarray(14,14+3*appData[12]*appData[13])};}}
+if(fileMarker===0xFFEE){if(appData[0]===0x41&&appData[1]===0x64&&appData[2]===0x6F&&appData[3]===0x62&&appData[4]===0x65&&appData[5]===0){adobe={version:appData[6],flags0:(appData[7]<<8)|appData[8],flags1:(appData[9]<<8)|appData[10],transformCode:appData[11]};}}
+break;case 0xFFDB:var quantizationTablesLength=readUint16();var quantizationTablesEnd=quantizationTablesLength+offset-2;while(offset<quantizationTablesEnd){var quantizationTableSpec=data[offset++];var tableData=new Int32Array(64);if((quantizationTableSpec>>4)===0){for(j=0;j<64;j++){var z=dctZigZag[j];tableData[z]=data[offset++];}}else if((quantizationTableSpec>>4)===1){for(j=0;j<64;j++){var z=dctZigZag[j];tableData[z]=readUint16();}}else
+throw new Error("DQT: invalid table spec");quantizationTables[quantizationTableSpec&15]=tableData;}
+break;case 0xFFC0:case 0xFFC1:case 0xFFC2:readUint16();frame={};frame.extended=(fileMarker===0xFFC1);frame.progressive=(fileMarker===0xFFC2);frame.precision=data[offset++];frame.scanLines=readUint16();frame.samplesPerLine=readUint16();frame.components={};frame.componentsOrder=[];var componentsCount=data[offset++],componentId;var maxH=0,maxV=0;for(i=0;i<componentsCount;i++){componentId=data[offset];var h=data[offset+1]>>4;var v=data[offset+1]&15;var qId=data[offset+2];frame.componentsOrder.push(componentId);frame.components[componentId]={h:h,v:v,quantizationIdx:qId};offset+=3;}
+prepareComponents(frame);frames.push(frame);break;case 0xFFC4:var huffmanLength=readUint16();for(i=2;i<huffmanLength;){var huffmanTableSpec=data[offset++];var codeLengths=new Uint8Array(16);var codeLengthSum=0;for(j=0;j<16;j++,offset++)
+codeLengthSum+=(codeLengths[j]=data[offset]);var huffmanValues=new Uint8Array(codeLengthSum);for(j=0;j<codeLengthSum;j++,offset++)
+huffmanValues[j]=data[offset];i+=17+codeLengthSum;((huffmanTableSpec>>4)===0?huffmanTablesDC:huffmanTablesAC)[huffmanTableSpec&15]=buildHuffmanTable(codeLengths,huffmanValues);}
+break;case 0xFFDD:readUint16();resetInterval=readUint16();break;case 0xFFDA:var scanLength=readUint16();var selectorsCount=data[offset++];var components=[],component;for(i=0;i<selectorsCount;i++){component=frame.components[data[offset++]];var tableSpec=data[offset++];component.huffmanTableDC=huffmanTablesDC[tableSpec>>4];component.huffmanTableAC=huffmanTablesAC[tableSpec&15];components.push(component);}
+var spectralStart=data[offset++];var spectralEnd=data[offset++];var successiveApproximation=data[offset++];var processed=decodeScan(data,offset,frame,components,resetInterval,spectralStart,spectralEnd,successiveApproximation>>4,successiveApproximation&15);offset+=processed;break;case 0xFFFF:if(data[offset]!==0xFF){offset--;}
+break;default:if(data[offset-3]==0xFF&&data[offset-2]>=0xC0&&data[offset-2]<=0xFE){offset-=3;break;}
+throw new Error("unknown JPEG marker "+fileMarker.toString(16));}
+fileMarker=readUint16();}
+if(frames.length!=1)
+throw new Error("only single frame JPEGs supported");for(var i=0;i<frames.length;i++){var cp=frames[i].components;for(var j in cp){cp[j].quantizationTable=quantizationTables[cp[j].quantizationIdx];delete cp[j].quantizationIdx;}}
+this.width=frame.samplesPerLine;this.height=frame.scanLines;this.jfif=jfif;this.adobe=adobe;this.components=[];for(var i=0;i<frame.componentsOrder.length;i++){var component=frame.components[frame.componentsOrder[i]];this.components.push({lines:buildComponentData(frame,component),scaleX:component.h/frame.maxH,scaleY:component.v/frame.maxV});}},getData:function getData(width,height){var scaleX=this.width/width,scaleY=this.height/height;var component1,component2,component3,component4;var component1Line,component2Line,component3Line,component4Line;var x,y;var offset=0;var Y,Cb,Cr,K,C,M,Ye,R,G,B;var colorTransform;var dataLength=width*height*this.components.length;var data=new Uint8Array(dataLength);switch(this.components.length){case 1:component1=this.components[0];for(y=0;y<height;y++){component1Line=component1.lines[0|(y*component1.scaleY*scaleY)];for(x=0;x<width;x++){Y=component1Line[0|(x*component1.scaleX*scaleX)];data[offset++]=Y;}}
+break;case 2:component1=this.components[0];component2=this.components[1];for(y=0;y<height;y++){component1Line=component1.lines[0|(y*component1.scaleY*scaleY)];component2Line=component2.lines[0|(y*component2.scaleY*scaleY)];for(x=0;x<width;x++){Y=component1Line[0|(x*component1.scaleX*scaleX)];data[offset++]=Y;Y=component2Line[0|(x*component2.scaleX*scaleX)];data[offset++]=Y;}}
+break;case 3:colorTransform=true;if(this.adobe&&this.adobe.transformCode)
+colorTransform=true;else if(typeof this.colorTransform!=='undefined')
+colorTransform=!!this.colorTransform;component1=this.components[0];component2=this.components[1];component3=this.components[2];for(y=0;y<height;y++){component1Line=component1.lines[0|(y*component1.scaleY*scaleY)];component2Line=component2.lines[0|(y*component2.scaleY*scaleY)];component3Line=component3.lines[0|(y*component3.scaleY*scaleY)];for(x=0;x<width;x++){if(!colorTransform){R=component1Line[0|(x*component1.scaleX*scaleX)];G=component2Line[0|(x*component2.scaleX*scaleX)];B=component3Line[0|(x*component3.scaleX*scaleX)];}else{Y=component1Line[0|(x*component1.scaleX*scaleX)];Cb=component2Line[0|(x*component2.scaleX*scaleX)];Cr=component3Line[0|(x*component3.scaleX*scaleX)];R=clampTo8bit(Y+1.402*(Cr-128));G=clampTo8bit(Y-0.3441363*(Cb-128)-0.71413636*(Cr-128));B=clampTo8bit(Y+1.772*(Cb-128));}
+data[offset++]=R;data[offset++]=G;data[offset++]=B;}}
+break;case 4:if(!this.adobe)
+throw new Error('Unsupported color mode (4 components)');colorTransform=false;if(this.adobe&&this.adobe.transformCode)
+colorTransform=true;else if(typeof this.colorTransform!=='undefined')
+colorTransform=!!this.colorTransform;component1=this.components[0];component2=this.components[1];component3=this.components[2];component4=this.components[3];for(y=0;y<height;y++){component1Line=component1.lines[0|(y*component1.scaleY*scaleY)];component2Line=component2.lines[0|(y*component2.scaleY*scaleY)];component3Line=component3.lines[0|(y*component3.scaleY*scaleY)];component4Line=component4.lines[0|(y*component4.scaleY*scaleY)];for(x=0;x<width;x++){if(!colorTransform){C=component1Line[0|(x*component1.scaleX*scaleX)];M=component2Line[0|(x*component2.scaleX*scaleX)];Ye=component3Line[0|(x*component3.scaleX*scaleX)];K=component4Line[0|(x*component4.scaleX*scaleX)];}else{Y=component1Line[0|(x*component1.scaleX*scaleX)];Cb=component2Line[0|(x*component2.scaleX*scaleX)];Cr=component3Line[0|(x*component3.scaleX*scaleX)];K=component4Line[0|(x*component4.scaleX*scaleX)];C=255-clampTo8bit(Y+1.402*(Cr-128));M=255-clampTo8bit(Y-0.3441363*(Cb-128)-0.71413636*(Cr-128));Ye=255-clampTo8bit(Y+1.772*(Cb-128));}
+data[offset++]=255-C;data[offset++]=255-M;data[offset++]=255-Ye;data[offset++]=255-K;}}
+break;default:throw new Error('Unsupported color mode');}
+return data;},copyToImageData:function copyToImageData(imageData){var width=imageData.width,height=imageData.height;var imageDataArray=imageData.data;var data=this.getData(width,height);var i=0,j=0,x,y;var Y,K,C,M,R,G,B;switch(this.components.length){case 1:for(y=0;y<height;y++){for(x=0;x<width;x++){Y=data[i++];imageDataArray[j++]=Y;imageDataArray[j++]=Y;imageDataArray[j++]=Y;imageDataArray[j++]=255;}}
+break;case 3:for(y=0;y<height;y++){for(x=0;x<width;x++){R=data[i++];G=data[i++];B=data[i++];imageDataArray[j++]=R;imageDataArray[j++]=G;imageDataArray[j++]=B;imageDataArray[j++]=255;}}
+break;case 4:for(y=0;y<height;y++){for(x=0;x<width;x++){C=data[i++];M=data[i++];Y=data[i++];K=data[i++];R=255-clampTo8bit(C*(1-K/255)+K);G=255-clampTo8bit(M*(1-K/255)+K);B=255-clampTo8bit(Y*(1-K/255)+K);imageDataArray[j++]=R;imageDataArray[j++]=G;imageDataArray[j++]=B;imageDataArray[j++]=255;}}
+break;default:throw new Error('Unsupported color mode');}}};return constructor;})();global.jpegDecode=decode;function decode(jpegData,opts){var defaultOpts={useTArray:false,colorTransform:true};if(opts){if(typeof opts==='object'){opts={useTArray:(typeof opts.useTArray==='undefined'?defaultOpts.useTArray:opts.useTArray),colorTransform:(typeof opts.colorTransform==='undefined'?defaultOpts.colorTransform:opts.colorTransform)};}else{opts=defaultOpts;opts.useTArray=true;}}else{opts=defaultOpts;}
+var arr=new Uint8Array(jpegData);var decoder=new JpegImage();decoder.parse(arr);decoder.colorTransform=opts.colorTransform;var image={width:decoder.width,height:decoder.height,data:opts.useTArray?new Uint8Array(decoder.width*decoder.height*4):new Buffer(decoder.width*decoder.height*4)};decoder.copyToImageData(image);return image;}'use strict';tr.exportTo('tr.metrics.sh',function(){const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const SpeedIndex=tr.e.chrome.SpeedIndex;const LOADING_METRIC_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(0,1e3,20).addLinearBins(3e3,20).addExponentialBins(20e3,20);const SUMMARY_OPTIONS={avg:true,count:false,max:true,min:true,std:true,sum:false,};function addSpeedIndexScreenshotsBasedSample(samples,navigationStart,browserHelper){const screenshotObjects=browserHelper.process.objects.getAllInstancesNamed('Screenshot');if(!screenshotObjects)return;for(let i=0;i<screenshotObjects.length;i++){const snapshots=screenshotObjects[i].snapshots;const timestampedColorHistograms=[];snapshots.map(snapshot=>{if(snapshot.ts>=navigationStart.start){timestampedColorHistograms.push({colorHistogram:SpeedIndex.createColorHistogram(getPixelData(snapshot.args)),ts:snapshot.ts});}});samples.push({value:SpeedIndex.calculateSpeedIndex(timestampedColorHistograms)-
+navigationStart.start});}}
+function getPixelData(base64JpegImage){const binaryString=atob(base64JpegImage);const bytes=new DataView(new ArrayBuffer(base64JpegImage.length));tr.b.Base64.DecodeToTypedArray(base64JpegImage,bytes);const rawImageData=jpegDecode(bytes.buffer,{useTArray:true});return rawImageData.data;}
+function collectSpeedIndexSamplesFromLoadExpectations(model,chromeHelper){const speedIndexScreenshotsBasedSamples=[];for(const expectation of model.userModel.expectations){if(!(expectation instanceof tr.model.um.LoadExpectation))continue;if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
+const rendererHelper=chromeHelper.rendererHelpers[expectation.renderProcess.pid];addSpeedIndexScreenshotsBasedSample(speedIndexScreenshotsBasedSamples,expectation.navigationStart,chromeHelper.browserHelper);}
+return speedIndexScreenshotsBasedSamples;}
+function screenshotsBasedSpeedIndexMetric(histograms,model){const speedIndexScreenshotsBasedHistogram=histograms.createHistogram('speedIndexScreenshotsBased',timeDurationInMs_smallerIsBetter,[],{binBoundaries:LOADING_METRIC_BOUNDARIES,description:'The average time at which visible parts of the'+' page are displayed.',summaryOptions:SUMMARY_OPTIONS,});const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);const samples=collectSpeedIndexSamplesFromLoadExpectations(model,chromeHelper);for(const sample of samples){speedIndexScreenshotsBasedHistogram.addSample(sample.value);}}
+tr.metrics.MetricRegistry.register(screenshotsBasedSpeedIndexMetric);return{screenshotsBasedSpeedIndexMetric};});'use strict';tr.exportTo('tr.metrics.sh',function(){function webviewStartupMetric(histograms,model){const startupWallHist=new tr.v.Histogram('webview_startup_wall_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);startupWallHist.description='WebView startup wall time';const startupCPUHist=new tr.v.Histogram('webview_startup_cpu_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);startupCPUHist.description='WebView startup CPU time';const loadWallHist=new tr.v.Histogram('webview_url_load_wall_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);loadWallHist.description='WebView blank URL load wall time';const loadCPUHist=new tr.v.Histogram('webview_url_load_cpu_time',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);loadCPUHist.description='WebView blank URL load CPU time';for(const slice of model.getDescendantEvents()){if(!(slice instanceof tr.model.ThreadSlice))continue;if(slice.title==='WebViewStartupInterval'){startupWallHist.addSample(slice.duration);startupCPUHist.addSample(slice.cpuDuration);}
 if(slice.title==='WebViewBlankUrlLoadInterval'){loadWallHist.addSample(slice.duration);loadCPUHist.addSample(slice.cpuDuration);}}
 histograms.addHistogram(startupWallHist);histograms.addHistogram(startupCPUHist);histograms.addHistogram(loadWallHist);histograms.addHistogram(loadCPUHist);}
 tr.metrics.MetricRegistry.register(webviewStartupMetric);return{webviewStartupMetric,};});'use strict';tr.exportTo('tr.metrics.tabs',function(){function tabsMetric(histograms,model,opt_options){const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);if(!chromeHelper){return;}
-const tabSwitchLatencies=[];const TAB_SWITCHING_SLICE_TITLE='TabSwitching::Latency';function extractLatencyFromHelpers(helpers,legacy){for(const helper of helpers){if(!helper.mainThread){continue;}
-const thread=helper.mainThread;for(const slice of thread.asyncSliceGroup.slices){if(slice.title===TAB_SWITCHING_SLICE_TITLE&&(legacy||slice.args.latency)){tabSwitchLatencies.push(legacy?slice.duration:slice.args.latency);}}}}
+const tabSwitchRequestDelays=[];const TAB_SWITCHING_REQUEST_TITLE='TabSwitchVisibilityRequest';let startTabSwitchVisibilityRequest=Number.MAX_SAFE_INTEGER;for(const helper of chromeHelper.browserHelpers){if(!helper.mainThread)continue;for(const slice of helper.mainThread.asyncSliceGroup.slices){if(slice.title===TAB_SWITCHING_REQUEST_TITLE&&!slice.error){tabSwitchRequestDelays.push(slice.duration);if(slice.start<startTabSwitchVisibilityRequest){startTabSwitchVisibilityRequest=slice.start;}}}}
+histograms.createHistogram('tab_switching_request_delay',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,tabSwitchRequestDelays,{description:'Delay before tab-request is made',summaryOptions:{sum:false}});const tabSwitchLatencies=[];const TAB_SWITCHING_SLICE_TITLE='TabSwitching::Latency';function extractLatencyFromHelpers(helpers,legacy){for(const helper of helpers){if(!helper.mainThread){continue;}
+const thread=helper.mainThread;for(const slice of thread.asyncSliceGroup.slices){if(slice.title===TAB_SWITCHING_SLICE_TITLE&&(legacy||slice.args.latency)&&slice.start>startTabSwitchVisibilityRequest){tabSwitchLatencies.push(legacy?slice.duration:slice.args.latency);}}}}
 extractLatencyFromHelpers(chromeHelper.browserHelpers);extractLatencyFromHelpers(Object.values(chromeHelper.rendererHelpers));if(tabSwitchLatencies.length===0){extractLatencyFromHelpers(chromeHelper.browserHelpers,true);}
-histograms.createHistogram('tab_switching_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,tabSwitchLatencies,{description:'Tab switching time in ms',summaryOptions:{sum:false}});const tabSwitchRequestDelays=[];const TAB_SWITCHING_REQUEST_TITLE='TabSwitchVisibilityRequest';for(const helper of chromeHelper.browserHelpers){if(!helper.mainThread)continue;for(const slice of helper.mainThread.asyncSliceGroup.slices){if(slice.title===TAB_SWITCHING_REQUEST_TITLE&&!slice.error){tabSwitchRequestDelays.push(slice.duration);}}}
-histograms.createHistogram('tab_switching_request_delay',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,tabSwitchRequestDelays,{description:'Delay before tab-request is made',summaryOptions:{sum:false}});}
+histograms.createHistogram('tab_switching_latency',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,tabSwitchLatencies,{description:'Tab switching time in ms',summaryOptions:{sum:false}});}
 tr.metrics.MetricRegistry.register(tabsMetric,{supportsRangeOfInterest:false,});return{tabsMetric,};});'use strict';tr.exportTo('tr.metrics',function(){const MEMORY_INFRA_TRACING_CATEGORY='disabled-by-default-memory-infra';const TIME_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1e-3,1e5,30);const BYTE_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1,1e9,30);const COUNT_BOUNDARIES=tr.v.HistogramBinBoundaries.createExponential(1,1e5,30);const SUMMARY_OPTIONS=tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS;function addMemoryInfraHistograms(histograms,model,categoryNamesToTotalEventSizes){const memoryDumpCount=model.globalMemoryDumps.length;if(memoryDumpCount===0)return;let totalOverhead=0;let nonMemoryInfraThreadOverhead=0;const overheadByProvider={};for(const process of Object.values(model.processes)){for(const thread of Object.values(process.threads)){for(const slice of Object.values(thread.sliceGroup.slices)){if(slice.category!==MEMORY_INFRA_TRACING_CATEGORY)continue;totalOverhead+=slice.duration;if(thread.name!=='MemoryInfra'){nonMemoryInfraThreadOverhead+=slice.duration;}
 if(slice.args&&slice.args['dump_provider.name']){const providerName=slice.args['dump_provider.name'];let durationAndCount=overheadByProvider[providerName];if(durationAndCount===undefined){overheadByProvider[providerName]=durationAndCount={duration:0,count:0};}
 durationAndCount.duration+=slice.duration;durationAndCount.count++;}}}}
@@ -8721,7 +9136,27 @@
 maxEventCountPerSec=Math.max(maxEventCountPerSec,runningEventNumPerSec);maxEventBytesPerSec=Math.max(maxEventBytesPerSec,runningEventBytesPerSec);}
 const stats=model.stats.allTraceEventStats;const categoryNamesToTotalEventSizes=(stats.reduce((map,stat)=>(map.set(stat.category,((map.get(stat.category)||0)+
 stat.totalEventSizeinBytes))),new Map()));const maxCatNameAndBytes=Array.from(categoryNamesToTotalEventSizes.entries()).reduce((a,b)=>((b[1]>=a[1])?b:a));const maxEventBytesPerCategory=maxCatNameAndBytes[1];const categoryWithMaxEventBytes=maxCatNameAndBytes[0];const maxEventCountPerSecValue=new tr.v.Histogram('peak_event_rate',tr.b.Unit.byName.count_smallerIsBetter,COUNT_BOUNDARIES);maxEventCountPerSecValue.description='Max number of events per second';maxEventCountPerSecValue.customizeSummaryOptions(SUMMARY_OPTIONS);maxEventCountPerSecValue.addSample(maxEventCountPerSec);const maxEventBytesPerSecValue=new tr.v.Histogram('peak_event_size_rate',tr.b.Unit.byName.sizeInBytes_smallerIsBetter,BYTE_BOUNDARIES);maxEventBytesPerSecValue.description='Max event size in bytes per second';maxEventBytesPerSecValue.customizeSummaryOptions(SUMMARY_OPTIONS);maxEventBytesPerSecValue.addSample(maxEventBytesPerSec);const totalTraceBytesValue=new tr.v.Histogram('trace_size',tr.b.Unit.byName.sizeInBytes_smallerIsBetter,BYTE_BOUNDARIES);totalTraceBytesValue.customizeSummaryOptions(SUMMARY_OPTIONS);totalTraceBytesValue.addSample(totalTraceBytes);const biggestCategory={name:categoryWithMaxEventBytes,size_in_bytes:maxEventBytesPerCategory};totalTraceBytesValue.diagnostics.set('category_with_max_event_size',new tr.v.d.GenericSet([biggestCategory]));histograms.addHistogram(totalTraceBytesValue);maxEventCountPerSecValue.diagnostics.set('category_with_max_event_size',new tr.v.d.GenericSet([biggestCategory]));histograms.addHistogram(maxEventCountPerSecValue);maxEventBytesPerSecValue.diagnostics.set('category_with_max_event_size',new tr.v.d.GenericSet([biggestCategory]));histograms.addHistogram(maxEventBytesPerSecValue);addMemoryInfraHistograms(histograms,model,categoryNamesToTotalEventSizes);}
-tr.metrics.MetricRegistry.register(tracingMetric);return{tracingMetric,MEMORY_INFRA_TRACING_CATEGORY,};});'use strict';tr.exportTo('tr.metrics.v8',function(){const CUSTOM_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(4,200,100);function computeExecuteMetrics(histograms,model){const cpuTotalExecution=new tr.v.Histogram('v8_execution_cpu_total',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);cpuTotalExecution.description='cpu total time spent in script execution';const wallTotalExecution=new tr.v.Histogram('v8_execution_wall_total',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);wallTotalExecution.description='wall total time spent in script execution';const cpuSelfExecution=new tr.v.Histogram('v8_execution_cpu_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);cpuSelfExecution.description='cpu self time spent in script execution';const wallSelfExecution=new tr.v.Histogram('v8_execution_wall_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);wallSelfExecution.description='wall self time spent in script execution';for(const e of model.findTopmostSlicesNamed('V8.Execute')){cpuTotalExecution.addSample(e.cpuDuration);wallTotalExecution.addSample(e.duration);cpuSelfExecution.addSample(e.cpuSelfTime);wallSelfExecution.addSample(e.selfTime);}
+tr.metrics.MetricRegistry.register(tracingMetric);return{tracingMetric,MEMORY_INFRA_TRACING_CATEGORY,};});'use strict';tr.exportTo('tr.metrics',function(){function parseBuckets_(event,processName){const len=tr.b.Base64.getDecodedBufferLength(event.args.buckets);const buffer=new ArrayBuffer(len);const dataView=new DataView(buffer);tr.b.Base64.DecodeToTypedArray(event.args.buckets,dataView);const decoded=new Uint32Array(buffer);const sum=decoded[1]+decoded[2]*0x100000000;const bins=[];let position=4;while(position<=decoded.length-4){const min=decoded[position++];const max=decoded[position++]+decoded[position++]*0x100000000;const count=decoded[position++];const processes=new tr.v.d.Breakdown();processes.set(processName,count);const events=new tr.v.d.RelatedEventSet([event]);bins.push({min,max,count,processes,events});}
+return{sum,bins};}
+function mergeBins_(x,y){x.sum+=y.sum;const allBins=[...x.bins,...y.bins];allBins.sort((a,b)=>a.min-b.min);x.bins=[];let last=undefined;for(const bin of allBins){if(last!==undefined&&bin.min===last.min){if(last.max!==bin.max)throw new Error('Incompatible bins');if(bin.count===0)continue;last.count+=bin.count;for(const event of bin.events){last.events.add(event);}
+last.processes.addDiagnostic(bin.processes);}else{if(last!==undefined&&bin.min<last.max){throw new Error('Incompatible bins');}
+x.bins.push(bin);last=bin;}}}
+function subtractBins_(x,y){x.sum-=y.sum;let p1=0;let p2=0;while(p2<y.bins.length){while(p1<x.bins.length&&x.bins[p1].min!==y.bins[p2].min){p1++;}
+if(p1===x.bins.length)throw new Error('Cannot subtract');if(x.bins[p1].max!==y.bins[p2].max){throw new Error('Incompatible bins');}
+if(x.bins[p1].count<y.bins[p2].count){throw new Error('Cannot subtract');}
+x.bins[p1].count-=y.bins[p2].count;for(const event of y.bins[p2].events){x.bins[p1].events.add(event);}
+const processName=tr.b.getOnlyElement(x.bins[p1].processes)[0];x.bins[p1].processes.set(processName,x.bins[p1].count);p2++;}}
+function getHistogramUnit_(name){return tr.b.Unit.byName.unitlessNumber_smallerIsBetter;}
+function getHistogramBoundaries_(name){if(name.startsWith('Event.Latency.Scroll')){return tr.v.HistogramBinBoundaries.createExponential(1e3,1e5,50);}
+if(name.startsWith('Graphics.Smoothness.Throughput')){return tr.v.HistogramBinBoundaries.createLinear(0,100,101);}
+return tr.v.HistogramBinBoundaries.createExponential(1e-3,1e3,50);}
+function umaMetric(histograms,model){const histogramValues=new Map();const nameCounts=new Map();for(const process of model.getAllProcesses()){const histogramEvents=new Map();for(const event of process.instantEvents){if(event.title!=='UMAHistogramSamples')continue;const name=event.args.name;const events=histogramEvents.get(name)||[];if(!histogramEvents.has(name))histogramEvents.set(name,events);events.push(event);}
+let processName=tr.e.chrome.chrome_processes.canonicalizeProcessName(process.name);nameCounts.set(processName,(nameCounts.get(processName)||0)+1);processName=`${processName}_${nameCounts.get(processName)}`;for(const[name,events]of histogramEvents){const values=histogramValues.get(name)||{sum:0,bins:[]};if(!histogramValues.has(name))histogramValues.set(name,values);const endValues=parseBuckets_(events[events.length-1],processName);if(events.length===1){mergeBins_(values,endValues);}else if(events.length===2){subtractBins_(endValues,parseBuckets_(events[0],processName));mergeBins_(values,endValues);}else{throw new Error('There should be at most two snapshots of an UMA '+'histogram in each process');}}}
+for(const[name,values]of histogramValues){const histogram=new tr.v.Histogram(name,getHistogramUnit_(name),getHistogramBoundaries_(name));let sumOfMiddles=0;let sumOfBinLengths=0;for(const bin of values.bins){sumOfMiddles+=bin.count*(bin.min+bin.max)/2;sumOfBinLengths+=bin.count*(bin.max-bin.min);}
+const shift=(values.sum-sumOfMiddles)/sumOfBinLengths;if(Math.abs(shift)>0.5)throw new Error('Samples sum is wrong');for(const bin of values.bins){if(bin.count===0)continue;const shiftedValue=(bin.min+bin.max)/2+shift*(bin.max-bin.min);for(const[processName,count]of bin.processes){bin.processes.set(processName,shiftedValue*count/bin.count);}
+for(let i=0;i<bin.count;i++){histogram.addSample(shiftedValue,{processes:bin.processes,events:bin.events});}}
+histograms.addHistogram(histogram);}}
+tr.metrics.MetricRegistry.register(umaMetric,{requiredCategories:['benchmark'],});return{umaMetric,};});'use strict';tr.exportTo('tr.metrics.v8',function(){const CUSTOM_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(4,200,100);function computeExecuteMetrics(histograms,model){const cpuTotalExecution=new tr.v.Histogram('v8_execution_cpu_total',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);cpuTotalExecution.description='cpu total time spent in script execution';const wallTotalExecution=new tr.v.Histogram('v8_execution_wall_total',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);wallTotalExecution.description='wall total time spent in script execution';const cpuSelfExecution=new tr.v.Histogram('v8_execution_cpu_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);cpuSelfExecution.description='cpu self time spent in script execution';const wallSelfExecution=new tr.v.Histogram('v8_execution_wall_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);wallSelfExecution.description='wall self time spent in script execution';for(const e of model.findTopmostSlicesNamed('V8.Execute')){cpuTotalExecution.addSample(e.cpuDuration);wallTotalExecution.addSample(e.duration);cpuSelfExecution.addSample(e.cpuSelfTime);wallSelfExecution.addSample(e.selfTime);}
 histograms.addHistogram(cpuTotalExecution);histograms.addHistogram(wallTotalExecution);histograms.addHistogram(cpuSelfExecution);histograms.addHistogram(wallSelfExecution);}
 function computeParseLazyMetrics(histograms,model){const cpuSelfParseLazy=new tr.v.Histogram('v8_parse_lazy_cpu_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);cpuSelfParseLazy.description='cpu self time spent performing lazy parsing';const wallSelfParseLazy=new tr.v.Histogram('v8_parse_lazy_wall_self',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,CUSTOM_BOUNDARIES);wallSelfParseLazy.description='wall self time spent performing lazy parsing';for(const e of model.findTopmostSlicesNamed('V8.ParseLazyMicroSeconds')){cpuSelfParseLazy.addSample(e.cpuSelfTime);wallSelfParseLazy.addSample(e.selfTime);}
 for(const e of model.findTopmostSlicesNamed('V8.ParseLazy')){cpuSelfParseLazy.addSample(e.cpuSelfTime);wallSelfParseLazy.addSample(e.selfTime);}
@@ -8770,30 +9205,35 @@
 for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){addHistogramsForRuntimeGroup(runtimeGroup);}
 const blinkGroupCollection=runtimeGroupCollection.blinkRCSGroupCollection;if(blinkGroupCollection.totalTime>0){blinkGroupCollection.runtimeGroups.forEach(addDetailedHistogramsForRuntimeGroup);}}
 function runtimeStatsMetric(histograms,model){const interactiveTime=computeInteractiveTime_(model);const domContentLoadedTime=computeDomContentLoadedTime_(model);const endTime=Math.max(interactiveTime,domContentLoadedTime);const slices=[...model.getDescendantEvents()].filter(event=>event instanceof tr.e.v8.V8ThreadSlice&&event.start<=endTime);computeRuntimeStats(histograms,slices);}
-function addDurationHistogram(railStageName,runtimeGroupName,sampleValue,histograms,durationRelatedHistsByGroupName){const durationHistogram=histograms.createHistogram(`${railStageName}_${runtimeGroupName}:duration`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,convertMicroToMilli_(sampleValue),{binBoundaries:DURATION_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});if(durationRelatedHistsByGroupName.get(runtimeGroupName)===undefined){const durationHistogramMap=new tr.v.d.RelatedHistogramMap();durationHistogramMap.set(railStageName,durationHistogram);durationRelatedHistsByGroupName.set(runtimeGroupName,durationHistogramMap);}else{durationRelatedHistsByGroupName.get(runtimeGroupName).set(railStageName,durationHistogram);}}
-function addCountHistogram(railStageName,runtimeGroupName,sampleValue,histograms,countRelatedHistsByGroupName){const countHistogram=histograms.createHistogram(`${railStageName}_${runtimeGroupName}:count`,tr.b.Unit.byName.count_smallerIsBetter,sampleValue,{binBoundaries:COUNT_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});if(countRelatedHistsByGroupName.get(runtimeGroupName)===undefined){const countHistogramMap=new tr.v.d.RelatedHistogramMap();countHistogramMap.set(railStageName,countHistogram);countRelatedHistsByGroupName.set(runtimeGroupName,countHistogramMap);}else{countRelatedHistsByGroupName.get(runtimeGroupName).set(railStageName,countHistogram);}}
-function addTotalDurationHistogram(histogramName,time,histograms,durationRelatedHistsByGroupName){const durationHistogram=histograms.createHistogram(`${histogramName}:duration`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,convertMicroToMilli_(time),{binBoundaries:DURATION_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});const durationRelatedHistogram=durationRelatedHistsByGroupName.get(histogramName);if(durationRelatedHistogram!==undefined){durationHistogram.diagnostics.set('RAIL stages',durationRelatedHistogram);}}
-function addTotalCountHistogram(histogramName,count,histograms,countRelatedHistsByGroupName){const countHistogram=histograms.createHistogram(`${histogramName}:count`,tr.b.Unit.byName.count_smallerIsBetter,count,{binBoundaries:COUNT_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});const countRelatedHistogram=countRelatedHistsByGroupName.get(histogramName);if(countRelatedHistogram!==undefined){countHistogram.diagnostics.set('RAIL stages',countRelatedHistogram);}}
-function computeRuntimeStatsBucketOnUE(histograms,slices,v8SlicesBucketOnUEMap){const durationRelatedHistsByGroupName=new Map();const countRelatedHistsByGroupName=new Map();for(const[name,slicesUE]of v8SlicesBucketOnUEMap){const runtimeGroupCollection=new tr.e.v8.RuntimeStatsGroupCollection();runtimeGroupCollection.addSlices(slicesUE);let overallV8Time=runtimeGroupCollection.totalTime;let overallV8Count=runtimeGroupCollection.totalCount;for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){addDurationHistogram(name,runtimeGroup.name,runtimeGroup.time,histograms,durationRelatedHistsByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Time-=runtimeGroup.time;}
-addCountHistogram(name,runtimeGroup.name,runtimeGroup.count,histograms,countRelatedHistsByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Count-=runtimeGroup.count;}}
-if(runtimeGroupCollection.blinkRCSGroupCollection.totalTime>0){const blinkRCSGroupCollection=runtimeGroupCollection.blinkRCSGroupCollection;for(const group of blinkRCSGroupCollection.runtimeGroups){addDurationHistogram(name,group.name,group.time,histograms,durationRelatedHistsByGroupName);addCountHistogram(name,group.name,group.count,histograms,countRelatedHistsByGroupName);}}
-addDurationHistogram(name,'V8-Only',overallV8Time,histograms,durationRelatedHistsByGroupName);addCountHistogram(name,'V8-Only',overallV8Count,histograms,countRelatedHistsByGroupName);}
-const runtimeGroupCollection=new tr.e.v8.RuntimeStatsGroupCollection();runtimeGroupCollection.addSlices(slices);let overallV8Time=runtimeGroupCollection.totalTime;let overallV8Count=runtimeGroupCollection.totalCount;for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){addTotalDurationHistogram(runtimeGroup.name,runtimeGroup.time,histograms,durationRelatedHistsByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Time-=runtimeGroup.time;}
-addTotalCountHistogram(runtimeGroup.name,runtimeGroup.count,histograms,countRelatedHistsByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Count-=runtimeGroup.count;}}
-if(runtimeGroupCollection.blinkRCSGroupCollection.totalTime>0){const blinkRCSGroupCollection=runtimeGroupCollection.blinkRCSGroupCollection;for(const group of blinkRCSGroupCollection.runtimeGroups){addTotalDurationHistogram(group.name,group.time,histograms,durationRelatedHistsByGroupName);addTotalCountHistogram(group.name,group.count,histograms,countRelatedHistsByGroupName);}}
-addTotalDurationHistogram('V8-Only',overallV8Time,histograms,durationRelatedHistsByGroupName);addTotalCountHistogram('V8-Only',overallV8Count,histograms,countRelatedHistsByGroupName);}
+function addDurationHistogram(railStageName,runtimeGroupName,sampleValue,histograms,durationNamesByGroupName){const histName=`${railStageName}_${runtimeGroupName}:duration`;histograms.createHistogram(histName,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,convertMicroToMilli_(sampleValue),{binBoundaries:DURATION_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});let relatedNames=durationNamesByGroupName.get(runtimeGroupName);if(!relatedNames){relatedNames=new tr.v.d.RelatedNameMap();durationNamesByGroupName.set(runtimeGroupName,relatedNames);}
+relatedNames.set(railStageName,histName);}
+function addCountHistogram(railStageName,runtimeGroupName,sampleValue,histograms,countNamesByGroupName){const histName=`${railStageName}_${runtimeGroupName}:count`;histograms.createHistogram(histName,tr.b.Unit.byName.count_smallerIsBetter,sampleValue,{binBoundaries:COUNT_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,});let relatedNames=countNamesByGroupName.get(runtimeGroupName);if(!relatedNames){relatedNames=new tr.v.d.RelatedNameMap();countNamesByGroupName.set(runtimeGroupName,relatedNames);}
+relatedNames.set(railStageName,histName);}
+function addTotalDurationHistogram(histogramName,time,histograms,relatedNames){const value=convertMicroToMilli_(time);const breakdown=new tr.v.d.Breakdown();if(relatedNames){for(const[cat,histName]of relatedNames){breakdown.set(cat,histograms.getHistogramNamed(histName).average);}}
+histograms.createHistogram(`${histogramName}:duration`,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,{value,diagnostics:{'RAIL stages':breakdown}},{binBoundaries:DURATION_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,diagnostics:{'RAIL stages':relatedNames},});}
+function addTotalCountHistogram(histogramName,value,histograms,relatedNames){const breakdown=new tr.v.d.Breakdown();if(relatedNames){for(const[cat,histName]of relatedNames){breakdown.set(cat,histograms.getHistogramNamed(histName).average);}}
+histograms.createHistogram(`${histogramName}:count`,tr.b.Unit.byName.count_smallerIsBetter,{value,diagnostics:{'RAIL stages':breakdown}},{binBoundaries:COUNT_CUSTOM_BOUNDARIES,summaryOptions:SUMMARY_OPTIONS,diagnostics:{'RAIL stages':relatedNames},});}
+function computeRuntimeStatsBucketOnUE(histograms,slices,v8SlicesBucketOnUEMap){const durationNamesByGroupName=new Map();const countNamesByGroupName=new Map();for(const[name,slicesUE]of v8SlicesBucketOnUEMap){const runtimeGroupCollection=new tr.e.v8.RuntimeStatsGroupCollection();runtimeGroupCollection.addSlices(slicesUE);let overallV8Time=runtimeGroupCollection.totalTime;let overallV8Count=runtimeGroupCollection.totalCount;for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){addDurationHistogram(name,runtimeGroup.name,runtimeGroup.time,histograms,durationNamesByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Time-=runtimeGroup.time;}
+addCountHistogram(name,runtimeGroup.name,runtimeGroup.count,histograms,countNamesByGroupName);if(runtimeGroup.name==='Blink C++'){overallV8Count-=runtimeGroup.count;}}
+if(runtimeGroupCollection.blinkRCSGroupCollection.totalTime>0){const blinkRCSGroupCollection=runtimeGroupCollection.blinkRCSGroupCollection;for(const group of blinkRCSGroupCollection.runtimeGroups){addDurationHistogram(name,group.name,group.time,histograms,durationNamesByGroupName);addCountHistogram(name,group.name,group.count,histograms,countNamesByGroupName);}}
+addDurationHistogram(name,'V8-Only',overallV8Time,histograms,durationNamesByGroupName);addCountHistogram(name,'V8-Only',overallV8Count,histograms,countNamesByGroupName);}
+const runtimeGroupCollection=new tr.e.v8.RuntimeStatsGroupCollection();runtimeGroupCollection.addSlices(slices);let overallV8Time=runtimeGroupCollection.totalTime;let overallV8Count=runtimeGroupCollection.totalCount;for(const runtimeGroup of runtimeGroupCollection.runtimeGroups){addTotalDurationHistogram(runtimeGroup.name,runtimeGroup.time,histograms,durationNamesByGroupName.get(runtimeGroup.name));if(runtimeGroup.name==='Blink C++'){overallV8Time-=runtimeGroup.time;}
+addTotalCountHistogram(runtimeGroup.name,runtimeGroup.count,histograms,countNamesByGroupName.get(runtimeGroup.name));if(runtimeGroup.name==='Blink C++'){overallV8Count-=runtimeGroup.count;}}
+if(runtimeGroupCollection.blinkRCSGroupCollection.totalTime>0){const blinkRCSGroupCollection=runtimeGroupCollection.blinkRCSGroupCollection;for(const group of blinkRCSGroupCollection.runtimeGroups){addTotalDurationHistogram(group.name,group.time,histograms,durationNamesByGroupName.get(group.name));addTotalCountHistogram(group.name,group.count,histograms,countNamesByGroupName.get(group.name));}}
+addTotalDurationHistogram('V8-Only',overallV8Time,histograms,durationNamesByGroupName.get('V8-Only'));addTotalCountHistogram('V8-Only',overallV8Count,histograms,countNamesByGroupName.get('V8-Only'));}
 function runtimeStatsTotalMetric(histograms,model){const v8ThreadSlices=[...model.getDescendantEvents()].filter(event=>event instanceof tr.e.v8.V8ThreadSlice).sort((e1,e2)=>e1.start-e2.start);const v8SlicesBucketOnUEMap=new Map();for(const expectation of model.userModel.expectations){if(tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)){continue;}
 const slices=expectation.range.filterArray(v8ThreadSlices,event=>event.start);if(slices.length===0)continue;const lastSlice=slices[slices.length-1];if(!expectation.range.intersectsRangeExclusive(lastSlice.range)){slices.pop();}
 if(v8SlicesBucketOnUEMap.get(expectation.stageTitle)===undefined){v8SlicesBucketOnUEMap.set(expectation.stageTitle,slices);}else{const totalSlices=v8SlicesBucketOnUEMap.get(expectation.stageTitle).concat(slices);v8SlicesBucketOnUEMap.set(expectation.stageTitle,totalSlices);}}
 computeRuntimeStatsBucketOnUE(histograms,v8ThreadSlices,v8SlicesBucketOnUEMap);}
 tr.metrics.MetricRegistry.register(runtimeStatsTotalMetric);tr.metrics.MetricRegistry.register(runtimeStatsMetric);return{runtimeStatsMetric,runtimeStatsTotalMetric,};});'use strict';tr.exportTo('tr.metrics.v8',function(){function v8AndMemoryMetrics(histograms,model){tr.metrics.v8.executionMetric(histograms,model);tr.metrics.v8.gcMetric(histograms,model);tr.metrics.sh.memoryMetric(histograms,model,{rangeOfInterest:tr.metrics.v8.utils.rangeForMemoryDumps(model)});}
-tr.metrics.MetricRegistry.register(v8AndMemoryMetrics);return{v8AndMemoryMetrics,};});'use strict';tr.exportTo('tr.metrics.vr',function(){function createHistograms(histograms,name,options,hasCpuTime){const createdHistograms={wall:histograms.createHistogram(name+'_wall',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],options)};if(hasCpuTime){createdHistograms.cpu=histograms.createHistogram(name+'_cpu',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],options);}
+tr.metrics.MetricRegistry.register(v8AndMemoryMetrics);return{v8AndMemoryMetrics,};});'use strict';tr.exportTo('tr.metrics.vr',function(){const VR_GL_THREAD_NAME='VrShellGL';function createHistograms(histograms,name,options,hasCpuTime){const createdHistograms={wall:histograms.createHistogram(name+'_wall',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],options)};if(hasCpuTime){createdHistograms.cpu=histograms.createHistogram(name+'_cpu',tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],options);}
 return createdHistograms;}
-function frameCycleDurationMetric(histograms,model,opt_options){const histogramsByEventTitle=new Map();histogramsByEventTitle.set('Vr.DrawFrame',createHistograms(histograms,'draw_frame',{description:'Duration to render one frame'},true));histogramsByEventTitle.set('Vr.AcquireGvrFrame',createHistograms(histograms,'acquire_frame',{description:'Duration acquire a frame from GVR'},true));histogramsByEventTitle.set('Vr.ProcessControllerInput',createHistograms(histograms,'update_controller',{description:'Duration to query input from the controller'},true));histogramsByEventTitle.set('Vr.ProcessControllerInputForWebXr',createHistograms(histograms,'update_controller_webxr',{description:'Duration to query input from the controller '+'for WebXR'},true));histogramsByEventTitle.set('Vr.SubmitFrameNow',createHistograms(histograms,'submit_frame',{description:'Duration to submit a frame to GVR'},true));histogramsByEventTitle.set('Vr.PostSubmitDrawOnGpu',createHistograms(histograms,'post_submit_draw_on_gpu',{description:'Duration to draw a frame on GPU post submit to '+'GVR. Note this duration may include time spent on '+'reprojection'},false));histogramsByEventTitle.set('VrShellGl::DrawFrame',histogramsByEventTitle.get('Vr.DrawFrame'));histogramsByEventTitle.set('VrShellGl::AcquireFrame',histogramsByEventTitle.get('Vr.AcquireGvrFrame'));histogramsByEventTitle.set('VrShellGl::UpdateController',histogramsByEventTitle.get('Vr.ProcessControllerInput'));histogramsByEventTitle.set('VrShellGl::DrawFrameSubmitNow',histogramsByEventTitle.get('Vr.SubmitFrameNow'));histogramsByEventTitle.set('VrShellGl::PostSubmitDrawOnGpu',histogramsByEventTitle.get('Vr.PostSubmitDrawOnGpu'));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateAnimationsAndOpacity',createHistograms(histograms,'update_animations_and_opacity',{description:'Duration to apply animation and opacity changes'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateBindings',createHistograms(histograms,'update_bindings',{description:'Duration to push binding values'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateLayout',createHistograms(histograms,'update_layout',{description:'Duration to compute element sizes, layout and textures'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateWorldSpaceTransform',createHistograms(histograms,'update_world_space_transforms',{description:'Duration to calculate element transforms in world space'},true));histogramsByEventTitle.set('UiRenderer::DrawUiView',createHistograms(histograms,'draw_ui',{description:'Duration to draw the UI'},true));histogramsByEventTitle.set('UiElementRenderer::DrawTexturedQuad',createHistograms(histograms,'draw_textured_quad',{description:'Duration to draw a textured element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawGradientQuad',createHistograms(histograms,'draw_gradient_quad',{description:'Duration to draw a gradient element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawGradientGridQuad',createHistograms(histograms,'draw_gradient_grid_quad',{description:'Duration to draw a gradient grid element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawController',createHistograms(histograms,'draw_controller',{description:'Duration to draw the controller'},true));histogramsByEventTitle.set('UiElementRenderer::DrawLaser',createHistograms(histograms,'draw_laser',{description:'Duration to draw the laser'},true));histogramsByEventTitle.set('UiElementRenderer::DrawReticle',createHistograms(histograms,'draw_reticle',{description:'Duration to draw the reticle'},true));histogramsByEventTitle.set('UiElementRenderer::DrawShadow',createHistograms(histograms,'draw_shadow',{description:'Duration to draw a shadow element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawStars',createHistograms(histograms,'draw_stars',{description:'Duration to draw the stars'},true));histogramsByEventTitle.set('UiElementRenderer::DrawBackground',createHistograms(histograms,'draw_background',{description:'Duration to draw the textured background'},true));histogramsByEventTitle.set('UiElementRenderer::DrawKeyboard',createHistograms(histograms,'draw_keyboard',{description:'Duration to draw the keyboard'},true));const drawUiSubSlicesMap=new Map();const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);let rangeOfInterest=model.bounds;const userExpectationsOfInterest=[tr.model.um.AnimationExpectation];if(opt_options&&opt_options.rangeOfInterest){rangeOfInterest=opt_options.rangeOfInterest;userExpectationsOfInterest.push(tr.model.um.ResponseExpectation);}
+function frameCycleDurationMetric(histograms,model,opt_options){const histogramsByEventTitle=new Map();const expectationEvents=tr.importer.VR_EXPECTATION_EVENTS;for(const eventName in expectationEvents){const extraInfo=expectationEvents[eventName];histogramsByEventTitle.set(eventName,createHistograms(histograms,extraInfo.histogramName,{description:extraInfo.description},extraInfo.hasCpuTime));}
+histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateAnimationsAndOpacity',createHistograms(histograms,'update_animations_and_opacity',{description:'Duration to apply animation and opacity changes'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateBindings',createHistograms(histograms,'update_bindings',{description:'Duration to push binding values'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateLayout',createHistograms(histograms,'update_layout',{description:'Duration to compute element sizes, layout and textures'},true));histogramsByEventTitle.set('UiScene::OnBeginFrame.UpdateWorldSpaceTransform',createHistograms(histograms,'update_world_space_transforms',{description:'Duration to calculate element transforms in world space'},true));histogramsByEventTitle.set('UiRenderer::DrawUiView',createHistograms(histograms,'draw_ui',{description:'Duration to draw the UI'},true));histogramsByEventTitle.set('UiElementRenderer::DrawTexturedQuad',createHistograms(histograms,'draw_textured_quad',{description:'Duration to draw a textured element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawGradientQuad',createHistograms(histograms,'draw_gradient_quad',{description:'Duration to draw a gradient element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawGradientGridQuad',createHistograms(histograms,'draw_gradient_grid_quad',{description:'Duration to draw a gradient grid element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawController',createHistograms(histograms,'draw_controller',{description:'Duration to draw the controller'},true));histogramsByEventTitle.set('UiElementRenderer::DrawLaser',createHistograms(histograms,'draw_laser',{description:'Duration to draw the laser'},true));histogramsByEventTitle.set('UiElementRenderer::DrawReticle',createHistograms(histograms,'draw_reticle',{description:'Duration to draw the reticle'},true));histogramsByEventTitle.set('UiElementRenderer::DrawShadow',createHistograms(histograms,'draw_shadow',{description:'Duration to draw a shadow element'},true));histogramsByEventTitle.set('UiElementRenderer::DrawStars',createHistograms(histograms,'draw_stars',{description:'Duration to draw the stars'},true));histogramsByEventTitle.set('UiElementRenderer::DrawBackground',createHistograms(histograms,'draw_background',{description:'Duration to draw the textured background'},true));histogramsByEventTitle.set('UiElementRenderer::DrawKeyboard',createHistograms(histograms,'draw_keyboard',{description:'Duration to draw the keyboard'},true));const drawUiSubSlicesMap=new Map();const chromeHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);let rangeOfInterest=model.bounds;const userExpectationsOfInterest=[tr.model.um.AnimationExpectation];if(opt_options&&opt_options.rangeOfInterest){rangeOfInterest=opt_options.rangeOfInterest;userExpectationsOfInterest.push(tr.model.um.ResponseExpectation);}
 for(const ue of model.userModel.expectations){if(ue.initiatorType!==tr.model.um.INITIATOR_TYPE.VR){continue;}
 if(!userExpectationsOfInterest.some(function(ueOfInterest){return ue instanceof ueOfInterest;})){continue;}
 if(!rangeOfInterest.intersectsExplicitRangeInclusive(ue.start,ue.end)){continue;}
-for(const helper of chromeHelper.browserHelpers){const glThreads=helper.process.findAllThreadsMatching(thread=>!thread.name);for(const glThread of glThreads){for(const event of glThread.getDescendantEvents()){if(!(histogramsByEventTitle.has(event.title))){continue;}
+for(const helper of chromeHelper.browserHelpers){const glThreads=helper.process.findAllThreadsNamed(VR_GL_THREAD_NAME);for(const glThread of glThreads){for(const event of glThread.getDescendantEvents()){if(!(histogramsByEventTitle.has(event.title))){continue;}
 if(event.start<ue.start||event.end>ue.end){continue;}
 if(event.start<rangeOfInterest.min||event.end>rangeOfInterest.max){continue;}
 if(event.parentSlice&&event.parentSlice.title==='UiRenderer::DrawUiView'){const guid=event.parentSlice.guid;if(!drawUiSubSlicesMap.has(guid)){drawUiSubSlicesMap.set(guid,[]);}
@@ -8811,7 +9251,18 @@
 if(!('value'in WEBVR_COUNTERS.get('gpu.WebVR FPS').samples)){WEBVR_COUNTERS.get('gpu.WebVR FPS').samples.value=[0];}
 for(const[key,value]of WEBVR_COUNTERS){for(const[seriesName,samples]of Object.entries(value.samples)){let histogramName=value.name;if(seriesName!=='value'){histogramName=`${histogramName}_${seriesName}`;}
 histograms.createHistogram(histogramName,value.unit,samples,value.options);}}}
-tr.metrics.MetricRegistry.register(webvrMetric,{supportsRangeOfInterest:true,});return{webvrMetric,};});'use strict';tr.exportTo('tr.metrics.webrtc',function(){const DISPLAY_HERTZ=60.0;const VSYNC_DURATION_US=1e6/DISPLAY_HERTZ;const SEVERITY=3;const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD=6;const WEB_MEDIA_PLAYER_UPDATE_TITLE='UpdateCurrentFrame';const IDEAL_RENDER_INSTANT_NAME='Ideal Render Instant';const ACTUAL_RENDER_BEGIN_NAME='Actual Render Begin';const ACTUAL_RENDER_END_NAME='Actual Render End';const STREAM_ID_NAME='Serial';const REQUIRED_EVENT_ARGS_NAMES=[IDEAL_RENDER_INSTANT_NAME,ACTUAL_RENDER_BEGIN_NAME,ACTUAL_RENDER_END_NAME,STREAM_ID_NAME];const SUMMARY_OPTIONS=tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS;const count_smallerIsBetter=tr.b.Unit.byName.count_smallerIsBetter;const percentage_biggerIsBetter=tr.b.Unit.byName.normalizedPercentage_biggerIsBetter;const percentage_smallerIsBetter=tr.b.Unit.byName.normalizedPercentage_smallerIsBetter;const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const unitlessNumber_biggerIsBetter=tr.b.Unit.byName.unitlessNumber_biggerIsBetter;function isValidEvent(event){if(event.title!==WEB_MEDIA_PLAYER_UPDATE_TITLE||!event.args){return false;}
+tr.metrics.MetricRegistry.register(webvrMetric,{supportsRangeOfInterest:true,});return{webvrMetric,};});'use strict';tr.exportTo('tr.metrics.vr',function(){function webxrMetric(histograms,model,opt_options){const DEFAULT_BIN_BOUNDARIES=tr.v.HistogramBinBoundaries.createLinear(20,120,25);const counterHistogramsByTitle=new Map();counterHistogramsByTitle.set('gpu.WebXR FPS',histograms.createHistogram('webxr_fps',tr.b.Unit.byName.count_biggerIsBetter,[],{description:'WebXR frames per second',binBoundaries:DEFAULT_BIN_BOUNDARIES,}));const instantHistogramsByTitle=new Map();const expectationEvents=tr.importer.WEBXR_INSTANT_EVENTS;for(const[eventName,eventData]of Object.entries(expectationEvents)){const argsToHistograms={};for(const[argName,argData]of Object.entries(eventData)){argsToHistograms[argName]=histograms.createHistogram(argData.histogramName,tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,[],{description:argData.description,binBoundaries:DEFAULT_BIN_BOUNDARIES,});}
+instantHistogramsByTitle.set(eventName,argsToHistograms);}
+const rangeOfInterestEnabled=opt_options&&opt_options.rangeOfInterest;const rangeOfInterest=(rangeOfInterestEnabled?opt_options.rangeOfInterest:tr.b.math.Range.fromExplicitRange(-Infinity,Infinity));for(const ue of model.userModel.expectations){if(!rangeOfInterest.intersectsExplicitRangeInclusive(ue.start,ue.end)){continue;}
+if(ue.initiatorType!==tr.model.um.INITIATOR_TYPE.VR)continue;if(!rangeOfInterestEnabled){if(!(ue instanceof tr.model.um.AnimationExpectation))continue;}else{if(!(ue instanceof tr.model.um.AnimationExpectation||ue instanceof tr.model.um.ResponseExpectation))continue;}
+for(const counter of model.getAllCounters()){if(!(counterHistogramsByTitle.has(counter.id)))continue;for(const series of counter.series){for(const sample of series.samples){if(sample.timestamp<ue.start||sample.timestamp>=ue.end){continue;}
+if(!rangeOfInterest.intersectsExplicitRangeInclusive(sample.timestamp,sample.timestamp)){continue;}
+counterHistogramsByTitle.get(counter.id).addSample(sample.value);}}}
+for(const event of ue.associatedEvents.asSet()){if(!(instantHistogramsByTitle.has(event.title))){continue;}
+if(!rangeOfInterest.intersectsExplicitRangeInclusive(event.start,event.start)){continue;}
+const eventHistograms=instantHistogramsByTitle.get(event.title);for(const[key,value]of Object.entries(event.args)){if(key in eventHistograms){eventHistograms[key].addSample(value,{event:new tr.v.d.RelatedEventSet(event)});}}}}
+if(counterHistogramsByTitle.get('gpu.WebXR FPS').numValues===0){counterHistogramsByTitle.get('gpu.WebXR FPS').addSample(0);}}
+tr.metrics.MetricRegistry.register(webxrMetric,{supportsRangeOfInterest:true,});return{webxrMetric,};});'use strict';tr.exportTo('tr.metrics.webrtc',function(){const DISPLAY_HERTZ=60.0;const VSYNC_DURATION_US=1e6/DISPLAY_HERTZ;const SEVERITY=3;const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD=6;const WEB_MEDIA_PLAYER_UPDATE_TITLE='UpdateCurrentFrame';const IDEAL_RENDER_INSTANT_NAME='Ideal Render Instant';const ACTUAL_RENDER_BEGIN_NAME='Actual Render Begin';const ACTUAL_RENDER_END_NAME='Actual Render End';const STREAM_ID_NAME='Serial';const REQUIRED_EVENT_ARGS_NAMES=[IDEAL_RENDER_INSTANT_NAME,ACTUAL_RENDER_BEGIN_NAME,ACTUAL_RENDER_END_NAME,STREAM_ID_NAME];const SUMMARY_OPTIONS=tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS;const count_smallerIsBetter=tr.b.Unit.byName.count_smallerIsBetter;const percentage_biggerIsBetter=tr.b.Unit.byName.normalizedPercentage_biggerIsBetter;const percentage_smallerIsBetter=tr.b.Unit.byName.normalizedPercentage_smallerIsBetter;const timeDurationInMs_smallerIsBetter=tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;const unitlessNumber_biggerIsBetter=tr.b.Unit.byName.unitlessNumber_biggerIsBetter;function isValidEvent(event){if(event.title!==WEB_MEDIA_PLAYER_UPDATE_TITLE||!event.args){return false;}
 for(const parameter of REQUIRED_EVENT_ARGS_NAMES){if(!(parameter in event.args)){return false;}}
 return true;}
 function webrtcRenderingMetric(histograms,model){const modelHelper=model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);let webMediaPlayerMSEvents=[];for(const rendererPid in modelHelper.rendererHelpers){const rendererHelper=modelHelper.rendererHelpers[rendererPid];const compositorThread=rendererHelper.compositorThread;if(compositorThread!==undefined){webMediaPlayerMSEvents=webMediaPlayerMSEvents.concat(compositorThread.sliceGroup.slices.filter(isValidEvent));}}
@@ -9095,18 +9546,19 @@
 this.autoDataRange_.reset();for(const datum of this.data_){this.autoDataRange_.addValue(datum.percentile_0);this.autoDataRange_.addValue(datum.percentile_100);}},updateScales_(){super.updateScales_();this.xScale_.domain([0,this.data_.length]);},get xAxisTickOffset(){return 0.5;},updateDataRange_(){if(this.overrideDataRange_!==undefined)return;this.autoDataRange_.reset();for(const datum of this.data_){this.autoDataRange_.addValue(datum.percentile_0);this.autoDataRange_.addValue(datum.percentile_100);}},updateXAxis_(xAxis){xAxis.selectAll('*').remove();if(this.hideXAxis)return;tr.ui.b.NameColumnChart.prototype.updateXAxis_.call(this,xAxis);const baseline=xAxis.selectAll('path').data([this]);baseline.enter().append('line').attr('stroke','black').attr('x1',this.xScale_(0)).attr('x2',this.xScale_(this.data_.length)).attr('y1',this.graphHeight).attr('y2',this.graphHeight);baseline.exit().remove();},updateDataContents_(dataSel){dataSel.selectAll('*').remove();const boxesSel=dataSel.selectAll('path');for(let index=0;index<this.data_.length;++index){const datum=this.data_[index];const color=datum.color||'black';let sel=boxesSel.data([datum]);sel.enter().append('rect').attr('fill',color).attr('x',this.xScale_(index+0.2)).attr('width',this.xScale_(index+0.8)-this.xScale_(index+0.2)).attr('y',this.yScale_(datum.percentile_75)).attr('height',this.yScale_(datum.percentile_25)-
 this.yScale_(datum.percentile_75));sel.exit().remove();sel=boxesSel.data([datum]);sel.enter().append('line').attr('stroke',color).attr('x1',this.xScale_(index)).attr('x2',this.xScale_(index+1)).attr('y1',this.yScale_(datum.percentile_50)).attr('y2',this.yScale_(datum.percentile_50));sel.exit().remove();sel=boxesSel.data([datum]);sel.enter().append('line').attr('stroke',color).attr('x1',this.xScale_(index+0.4)).attr('x2',this.xScale_(index+0.6)).attr('y1',this.yScale_(datum.percentile_0)).attr('y2',this.yScale_(datum.percentile_0));sel.exit().remove();sel=boxesSel.data([datum]);sel.enter().append('line').attr('stroke',color).attr('x1',this.xScale_(index+0.4)).attr('x2',this.xScale_(index+0.6)).attr('y1',this.yScale_(datum.percentile_100)).attr('y2',this.yScale_(datum.percentile_100));sel.exit().remove();sel=boxesSel.data([datum]);sel.enter().append('line').attr('stroke',color).attr('x1',this.xScale_(index+0.5)).attr('x2',this.xScale_(index+0.5)).attr('y1',this.yScale_(datum.percentile_100)).attr('y2',this.yScale_(datum.percentile_0));sel.exit().remove();}}};return{BoxChart,};});'use strict';tr.exportTo('tr.ui.b',function(){const BarChart=tr.ui.b.define('bar-chart',tr.ui.b.ColumnChart);BarChart.prototype={__proto__:tr.ui.b.ColumnChart.prototype,decorate(){super.decorate();this.verticalScale_=undefined;this.horizontalScale_=undefined;this.isWaterfall_=false;},updateScales_(){super.updateScales_();this.yScale_.range([this.graphWidth,0]);this.xScale_.range([0,this.graphHeight]);this.verticalScale_=this.isYLogScale_?d3.scale.log(10):d3.scale.linear();this.verticalScale_.domain(this.xScale_.domain());this.verticalScale_.range([this.graphHeight,0]);this.horizontalScale_=d3.scale.linear();this.horizontalScale_.domain(this.yScale_.domain());this.horizontalScale_.range([0,this.graphWidth]);},set isWaterfall(waterfall){this.isWaterfall_=waterfall;if(waterfall){this.getDataSeries('hide').color='transparent';}
 this.updateContents_();},get isWaterfall(){return this.isWaterfall_;},get defaultGraphHeight(){return Math.max(20,10*this.data_.length);},get defaultGraphWidth(){return 100;},get barHeight(){return this.graphHeight/this.data.length;},drawBrush_(brushRectsSel){brushRectsSel.attr('x',0).attr('width',this.graphWidth).attr('y',d=>this.verticalScale_(d.max)).attr('height',d=>this.verticalScale_(d.min)-this.verticalScale_(d.max)).attr('fill','rgb(213, 236, 229)');},getDataPointAtChartPoint_(chartPoint){const flippedPoint={x:this.graphHeight-chartPoint.y,y:this.graphWidth-chartPoint.x};return super.getDataPointAtChartPoint_(flippedPoint);},drawXAxis_(xAxis){xAxis.attr('transform','translate(0,'+this.graphHeight+')').call(d3.svg.axis().scale(this.horizontalScale_).orient('bottom'));},get yAxisWidth(){return this.computeScaleTickWidth_(this.verticalScale_);},drawYAxis_(yAxis){const axisModifier=d3.svg.axis().scale(this.verticalScale_).orient('left');yAxis.call(axisModifier);},drawHoverValueBox_(rect){const rectHoverEvent=new tr.b.Event('rect-mouseenter');rectHoverEvent.rect=rect;this.dispatchEvent(rectHoverEvent);if(!this.enableHoverBox||(this.isWaterfall_&&rect.key==='hide')){return;}
-const seriesKeys=[...this.seriesByKey_.keys()];const chartAreaSel=d3.select(this.chartAreaElement);chartAreaSel.selectAll('.hover').remove();let keyWidthPx=0;let keyHeightPx=0;let xWidthPx=0;let xHeightPx=0;let groupWidthPx=0;let groupHeightPx=0;if(seriesKeys.length>1&&!this.isWaterfall_){keyWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,rect.key).width;keyHeightPx=this.textHeightPx_;}
+const seriesKeys=[...this.seriesByKey_.keys()];const chartAreaSel=d3.select(this.chartAreaElement);chartAreaSel.selectAll('.hover').remove();let keyWidthPx=0;let keyHeightPx=0;let xWidthPx=0;let xHeightPx=0;let groupWidthPx=0;let groupHeightPx=0;if(seriesKeys.length>1&&!this.isGrouped&&!this.isWaterfall_){keyWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,rect.key).width;keyHeightPx=this.textHeightPx_;}
 if(this.data.length>1&&!this.isWaterfall_){xWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,''+rect.datum.x).width;xHeightPx=this.textHeightPx_;}
 if(this.isGrouped&&rect.datum.group!==undefined){groupWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,rect.datum.group).width;groupHeightPx=this.textHeightPx_;}
-const valueWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,rect.value).width;const valueHeightPx=this.textHeightPx_;const maxWidthPx=Math.max(keyWidthPx,xWidthPx,groupWidthPx,valueWidthPx)+5;const hoverWidthPx=this.isGrouped?maxWidthPx:Math.min(maxWidthPx,Math.max(50,rect.widthPx));const hoverTopPx=rect.topPx+(rect.heightPx/2);const hoverLeftPx=rect.leftPx+rect.widthPx-hoverWidthPx;chartAreaSel.append('rect').attr('class','hover').attr('fill','white').attr('x',hoverLeftPx).attr('y',hoverTopPx).attr('width',hoverWidthPx).attr('height',keyHeightPx+xHeightPx+
-valueHeightPx+groupHeightPx);if(seriesKeys.length>1&&!this.isWaterfall_){chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx-3).text(rect.key);}
-if(this.data.length>1&&!this.isWaterfall_){chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx+valueHeightPx-3).text(''+rect.datum.x);}
-if(this.isGrouped&&rect.datum.group!==undefined){chartAreaSel.append('text').on('mouseleave',()=>this.clearHoverValueBox_(rect)).attr('class','hover').attr('fill',rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx+xHeightPx+groupHeightPx-3).text(rect.datum.group);}
-chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+xHeightPx+keyHeightPx+
+const valueWidthPx=tr.ui.b.getSVGTextSize(this.chartAreaElement,rect.value).width;const valueHeightPx=this.textHeightPx_;const maxWidthPx=Math.max(keyWidthPx,xWidthPx,groupWidthPx,valueWidthPx)+5;const hoverWidthPx=this.isGrouped?maxWidthPx:Math.min(maxWidthPx,Math.max(50,rect.widthPx));let hoverTopPx=rect.topPx;hoverTopPx=Math.min(hoverTopPx,this.getBoundingClientRect().height-
+valueHeightPx);let hoverLeftPx=rect.leftPx+(rect.widthPx/2);hoverLeftPx=Math.max(hoverLeftPx-hoverWidthPx,-this.margin.left);chartAreaSel.append('rect').attr('class','hover').attr('fill','white').attr('x',hoverLeftPx).attr('y',hoverTopPx).attr('width',hoverWidthPx).attr('height',keyHeightPx+xHeightPx+
+valueHeightPx+groupHeightPx);if(seriesKeys.length>1&&!this.isGrouped&&!this.isWaterfall_){chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color==='transparent'?'#000000':rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx-3).text(rect.key);}
+if(this.data.length>1&&!this.isWaterfall_){chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color==='transparent'?'#000000':rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx+valueHeightPx-3).text(''+rect.datum.x);}
+if(this.isGrouped&&rect.datum.group!==undefined){chartAreaSel.append('text').on('mouseleave',()=>this.clearHoverValueBox_(rect)).attr('class','hover').attr('fill',rect.color==='transparent'?'#000000':rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+keyHeightPx+xHeightPx+groupHeightPx-3).text(rect.datum.group);}
+chartAreaSel.append('text').attr('class','hover').attr('fill',rect.color==='transparent'?'#000000':rect.color).attr('x',hoverLeftPx+2).attr('y',hoverTopPx+xHeightPx+keyHeightPx+
 groupHeightPx+valueHeightPx-3).text(rect.value);},flipRect_(rect){return{datum:rect.datum,index:rect.index,key:rect.key,value:rect.value,color:rect.color,topPx:this.graphHeight-rect.leftPx-rect.widthPx,leftPx:this.graphWidth-rect.topPx-rect.heightPx,widthPx:rect.heightPx,heightPx:rect.widthPx,underflow:rect.underflow,overflow:rect.overflow,};},drawRect_(rect,sel){super.drawRect_(this.flipRect_(rect),sel);},drawUnderflow_(rect,rectsSel){let sel=rectsSel.data([rect]);sel.enter().append('text').text('*').attr('fill',rect.color).attr('x',0).attr('y',this.graphHeight-rect.leftPx+
 3+(rect.widthPx/2));sel.exit().remove();sel=rectsSel.data([rect]);sel.enter().append('rect').attr('fill','rgba(0, 0, 0, 0)').attr('x',0).attr('y',this.graphHeight-rect.leftPx-rect.widthPx).attr('width',10).attr('height',rect.widthPx).on('mouseenter',()=>this.drawHoverValueBox_(this.flipRect_(rect))).on('mouseleave',()=>this.clearHoverValueBox_(rect));sel.exit().remove();},drawOverflow_(rect,sel){sel=sel.data([rect]);sel.enter().append('text').text('*').attr('fill',rect.color).attr('x',this.graphWidth).attr('y',this.graphHeight-rect.leftPx+
 3+(rect.widthPx/2));sel.exit().remove();}};return{BarChart,};});'use strict';tr.exportTo('tr.ui.b',function(){const NameBarChart=tr.ui.b.define('name-bar-chart',tr.ui.b.BarChart);const Y_AXIS_PADDING=2;NameBarChart.prototype={__proto__:tr.ui.b.BarChart.prototype,getDataPointAtChartPoint_(chartPoint){return{x:tr.ui.b.BarChart.prototype.getDataPointAtChartPoint_.call(this,chartPoint).x,y:parseInt(Math.floor((this.graphHeight-chartPoint.y)/this.barHeight))};},getXForDatum_(datum,index){return index;},get yAxisWidth(){if(this.data.length===0)return 0;return Y_AXIS_PADDING+tr.b.math.Statistics.max(this.data_,d=>tr.ui.b.getSVGTextSize(this,d.x).width);},get defaultGraphHeight(){return(3+this.textHeightPx_)*this.data.length;},updateYAxis_(yAxis){if(tr.ui.b.getSVGTextSize(this,'test').width===0){tr.b.requestAnimationFrame(()=>this.updateYAxis_(yAxis));return;}
-yAxis.selectAll('*').remove();const nameTexts=yAxis.selectAll('text').data(this.data_);nameTexts.enter().append('text').attr('x',d=>-(tr.ui.b.getSVGTextSize(this,d.x).width+Y_AXIS_PADDING)).attr('y',(d,index)=>this.verticalScale_(index)).text(d=>d.x);nameTexts.exit().remove();let previousTop=undefined;for(const text of nameTexts[0]){const bbox=text.getBBox();if((previousTop===undefined)||(previousTop>(bbox.y+bbox.height))){previousTop=bbox.y;}else{text.style.opacity=0;}}}};return{NameBarChart,};});'use strict';tr.exportTo('tr.v.ui',function(){const DIAGNOSTIC_SPAN_BEHAVIOR={created(){this.diagnostic_=undefined;this.name_=undefined;this.histogram_=undefined;},attached(){if(this.diagnostic_)this.updateContents_();},get diagnostic(){return this.diagnostic_;},build(diagnostic,name,histogram){this.diagnostic_=diagnostic;this.name_=name;this.histogram_=histogram;if(this.isAttached)this.updateContents_();},updateContents_(){throw new Error('dom-modules must override updateContents_()');}};return{DIAGNOSTIC_SPAN_BEHAVIOR,};});'use strict';tr.exportTo('tr.v.ui',function(){const DEFAULT_COLOR_SCHEME=new tr.b.SinebowColorGenerator();function getHistogramName(histogram,diagnosticName,key){if(histogram===undefined)return undefined;const nameMap=histogram.diagnostics.get(diagnosticName);if(nameMap===undefined)return undefined;return nameMap.get(key);}
+yAxis.selectAll('*').remove();if(this.hideYAxis)return;const nameTexts=yAxis.selectAll('text').data(this.data_);nameTexts.enter().append('text').attr('x',d=>-(tr.ui.b.getSVGTextSize(this,d.x).width+Y_AXIS_PADDING)).attr('y',(d,index)=>this.verticalScale_(index)).text(d=>d.x);nameTexts.exit().remove();let previousTop=undefined;for(const text of nameTexts[0]){const bbox=text.getBBox();if((previousTop===undefined)||(previousTop>(bbox.y+bbox.height))){previousTop=bbox.y;}else{text.style.opacity=0;}}}};return{NameBarChart,};});'use strict';tr.exportTo('tr.v.ui',function(){const DIAGNOSTIC_SPAN_BEHAVIOR={created(){this.diagnostic_=undefined;this.name_=undefined;this.histogram_=undefined;},attached(){if(this.diagnostic_)this.updateContents_();},get diagnostic(){return this.diagnostic_;},build(diagnostic,name,histogram){this.diagnostic_=diagnostic;this.name_=name;this.histogram_=histogram;if(this.isAttached)this.updateContents_();},updateContents_(){throw new Error('dom-modules must override updateContents_()');}};return{DIAGNOSTIC_SPAN_BEHAVIOR,};});'use strict';tr.exportTo('tr.v.ui',function(){const DEFAULT_COLOR_SCHEME=new tr.b.SinebowColorGenerator();function getHistogramName(histogram,diagnosticName,key){if(histogram===undefined)return undefined;const nameMap=histogram.diagnostics.get(diagnosticName);if(nameMap===undefined)return undefined;return nameMap.get(key);}
 class BreakdownTableSummaryRow{constructor(displayElement,histogramNames){this.displayElement_=displayElement;this.histogramNames_=histogramNames;this.keySpan_=undefined;}
 get numberValue(){return undefined;}
 get keySpan(){if(this.keySpan_===undefined){if(this.histogramNames_.length){this.keySpan_=document.createElement('tr-ui-a-analysis-link');this.keySpan_.setSelectionAndContent(this.histogramNames_,'Select All');}else{this.keySpan_='Sum';}}
@@ -9133,9 +9585,8 @@
 if(this.numberValue===other.numberValue){return this.name.localeCompare(other.name);}
 return other.numberValue-this.numberValue;}}
 Polymer({is:'tr-v-ui-breakdown-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],created(){this.chart_=new tr.ui.b.ColumnChart();this.chart_.graphHeight=130;this.chart_.isStacked=true;this.chart_.hideXAxis=true;this.chart_.hideLegend=true;this.chart_.enableHoverBox=false;this.chart_.addEventListener('rect-mouseenter',event=>this.onRectMouseEnter_(event));this.chart_.addEventListener('rect-mouseleave',event=>this.onRectMouseLeave_(event));},onRectMouseEnter_(event){for(const row of this.$.table.tableRows){if(row.name===event.rect.key){row.displayElement.style.background=event.rect.color;row.keySpan.scrollIntoViewIfNeeded();}else{row.displayElement.style.background='';}}},onRectMouseLeave_(event){for(const row of this.$.table.tableRows){row.displayElement.style.background='';}},ready(){Polymer.dom(this.$.container).appendChild(this.chart_);this.$.table.zebra=true;this.$.table.showHeader=false;this.$.table.tableColumns=[{value:row=>row.keySpan,},{value:row=>row.displayElement,align:tr.ui.b.TableFormat.ColumnAlignment.RIGHT,},{value:row=>row.stringPercent,align:tr.ui.b.TableFormat.ColumnAlignment.RIGHT,},];},updateContents_(){this.$.container.style.display='none';this.$.table.style.display='none';this.$.empty.style.display='block';if(!this.diagnostic_){this.chart_.data=[];return;}
-if(this.histogram_)this.chart_.unit=this.histogram_.unit;let colorScheme=undefined;if(this.diagnostic.colorScheme===tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER){colorScheme=(name)=>{let cat=name.split(' ');cat=cat[cat.length-1];return tr.e.chrome.ChromeUserFriendlyCategoryDriver.getColor(cat);};}else if(this.diagnostic.colorScheme!==undefined){colorScheme=(name)=>tr.b.FixedColorSchemeRegistry.lookUp(this.diagnostic.colorScheme).getColor(name);}else{colorScheme=(name)=>DEFAULT_COLOR_SCHEME.colorForKey(name);}
-const tableRows=[];let tableSum=0;const histogramNames=[];for(const[key,value]of this.diagnostic){let histogramName;let row;if(value instanceof tr.v.Histogram){histogramName=value.name;row=new BreakdownTableRow(key,value.sum,histogramName,value.unit,colorScheme(key));}else{histogramName=getHistogramName(this.histogram_,this.name_,key);row=new BreakdownTableRow(key,value,histogramName,this.chart_.unit,colorScheme(key));}
-tableRows.push(row);if(row.numberValue!==undefined)tableSum+=row.numberValue;if(histogramName){histogramNames.push(histogramName);}}
+if(this.histogram_)this.chart_.unit=this.histogram_.unit;let colorScheme=undefined;if(this.diagnostic.colorScheme===tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER){colorScheme=(name)=>{let cat=name.split(' ');cat=cat[cat.length-1];return tr.e.chrome.ChromeUserFriendlyCategoryDriver.getColor(cat);};}else if(this.diagnostic.colorScheme){colorScheme=(name)=>tr.b.FixedColorSchemeRegistry.lookUp(this.diagnostic.colorScheme).getColor(name);}else{colorScheme=(name)=>DEFAULT_COLOR_SCHEME.colorForKey(name);}
+const tableRows=[];let tableSum=0;const histogramNames=[];for(const[key,value]of this.diagnostic){const histogramName=getHistogramName(this.histogram_,this.name_,key);const row=new BreakdownTableRow(key,value,histogramName,this.chart_.unit,colorScheme(key));tableRows.push(row);if(row.numberValue!==undefined)tableSum+=row.numberValue;if(histogramName){histogramNames.push(histogramName);}}
 tableRows.sort((x,y)=>x.compare(y));if(tableSum>0){let summaryDisplayElement=tableSum;if(this.chart_.unit!==undefined){summaryDisplayElement=this.chart_.unit.format(tableSum);}
 summaryDisplayElement=tr.ui.b.createSpan({textContent:summaryDisplayElement,});tableRows.unshift(new BreakdownTableSummaryRow(summaryDisplayElement,histogramNames));}
 const chartData={x:0};for(const row of tableRows){if(row.numberValue===undefined)continue;row.tableSum=tableSum;chartData[row.name]=row.numberValue;const dataSeries=this.chart_.getDataSeries(row.name);dataSeries.color=row.color;dataSeries.highlightedColor=row.highlightedColor;}
@@ -9143,18 +9594,15 @@
 if(Object.keys(chartData).length>1){this.$.container.style.display='block';this.$.empty.style.display='none';this.chart_.data=[chartData];}}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-collected-related-event-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){Polymer.dom(this).textContent='';for(const[canonicalUrl,events]of this.diagnostic){const link=document.createElement('a');if(events.length===1){const event=tr.b.getOnlyElement(events);link.textContent=event.title+' '+
 tr.b.Unit.byName.timeDurationInMs.format(event.duration);}else{link.textContent=events.length+' events';}
 link.href=canonicalUrl;Polymer.dom(this).appendChild(link);Polymer.dom(this).appendChild(document.createElement('br'));}}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-date-range-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){if(this.diagnostic===undefined){Polymer.dom(this).textContent='';return;}
-Polymer.dom(this).textContent=this.diagnostic.toString();}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-generic-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){this.$.generic.style.display='none';this.$.links.textContent='';if(this.diagnostic===undefined)return;const values=Array.from(this.diagnostic);let areAllStrings=true;let areAllNumbers=true;for(const value of values){if(typeof value!=='number'){areAllNumbers=false;if(typeof value!=='string'){areAllStrings=false;break;}}}
+Polymer.dom(this).textContent=this.diagnostic.toString();}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){function isLinkTuple(value){return((value instanceof Array)&&(value.length===2)&&(typeof value[0]==='string')&&tr.b.isUrl(value[1]));}
+Polymer({is:'tr-v-ui-generic-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){this.$.generic.style.display='none';this.$.links.textContent='';if(this.diagnostic===undefined)return;const values=Array.from(this.diagnostic);let areAllStrings=true;let areAllNumbers=true;for(const value of values){if(typeof value!=='number'){areAllNumbers=false;if(typeof value!=='string'&&!isLinkTuple(value)){areAllStrings=false;break;}}}
 if(!areAllStrings){this.$.generic.style.display='';this.$.generic.object=values;return;}
 if(areAllNumbers){values.sort((x,y)=>x-y);}else{values.sort();}
-for(const value of values){const link={textContent:''+value};if(tr.b.isUrl(value))link.href=value;if(this.name_===tr.v.d.RESERVED_NAMES.TRACE_URLS){link.textContent=value.substr(1+value.lastIndexOf('/'));}
+for(const value of values){const link={textContent:''+value};if(isLinkTuple(value)){link.textContent=value[0];link.href=value[1];}else if(tr.b.isUrl(value)){link.href=value;}
+if(this.name_===tr.v.d.RESERVED_NAMES.TRACE_URLS){link.textContent=value.substr(1+value.lastIndexOf('/'));}
 const linkEl=tr.ui.b.createLink(link);if(link.href){linkEl.target='_blank';linkEl.addEventListener('click',e=>e.stopPropagation());}
 this.$.links.appendChild(linkEl);}}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-related-event-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){Polymer.dom(this).textContent='';const events=new tr.model.EventSet([...this.diagnostic]);const link=document.createElement('tr-ui-a-analysis-link');let label=events.length+' events';if(events.length===1){const event=tr.b.getOnlyElement(events);label=event.title+' ';label+=tr.b.Unit.byName.timeDurationInMs.format(event.duration);}
-link.setSelectionAndContent(events,label);Polymer.dom(this).appendChild(link);}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-related-histogram-map-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],ready(){this.$.table.showHeader=false;this.$.table.tableColumns=[{value:row=>row[0]},{value:row=>row[1]},];},updateContents_(){Polymer.dom(this).textContent='';const rows=[];const histogramNames=new Set();for(const[name,hist]of this.diagnostic){histogramNames.add(hist.name);}
-if(histogramNames.size>1){const link=document.createElement('tr-ui-a-analysis-link');link.setSelectionAndContent(Array.from(histogramNames),'Select All');rows.push([link,'']);}
-for(const[name,hist]of this.diagnostic){const link=document.createElement('tr-ui-a-analysis-link');link.setSelectionAndContent([hist.name],name);const scalarSpan=tr.v.ui.createScalarSpan(hist);rows.push([link,scalarSpan]);}
-this.$.table.tableRows=rows;this.$.table.rebuild();}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-scalar-diagnostic-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){this.$.scalar.setValueAndUnit(this.diagnostic.value.value,this.diagnostic.value.unit);}});return{};});'use strict';Polymer({is:'tr-v-ui-tag-map-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){if(this.diagnostic===undefined){this.$.generic.object=undefined;return;}
-const obj={};for(const[tag,stories]of this.diagnostic.tagsToStoryNames){obj[tag]=Array.from(stories);}
-this.$.generic.object=obj;},onShow_(){this.$.show.style.display='none';this.$.hide.style.display='block';this.$.generic.style.display='block';},onHide_(){this.$.show.style.display='block';this.$.hide.style.display='none';this.$.generic.style.display='none';},});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-unmergeable-diagnostic-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){Polymer.dom(this).textContent='';for(const diagnostic of this.diagnostic){const div=document.createElement('div');div.appendChild(tr.v.ui.createDiagnosticSpan(diagnostic,this.name_,this.histogram_));Polymer.dom(this).appendChild(div);}}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){function findElementNameForDiagnostic(diagnostic){let typeInfo=undefined;let curProto=diagnostic.constructor.prototype;while(curProto){typeInfo=tr.v.d.Diagnostic.findTypeInfo(curProto.constructor);if(typeInfo&&typeInfo.metadata.elementName)break;typeInfo=undefined;curProto=curProto.__proto__;}
+link.setSelectionAndContent(events,label);Polymer.dom(this).appendChild(link);}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-scalar-diagnostic-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){this.$.scalar.setValueAndUnit(this.diagnostic.value.value,this.diagnostic.value.unit);}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-unmergeable-diagnostic-set-span',behaviors:[tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],updateContents_(){Polymer.dom(this).textContent='';for(const diagnostic of this.diagnostic){if(diagnostic instanceof tr.v.d.RelatedNameMap)continue;const div=document.createElement('div');div.appendChild(tr.v.ui.createDiagnosticSpan(diagnostic,this.name_,this.histogram_));Polymer.dom(this).appendChild(div);}}});return{};});'use strict';tr.exportTo('tr.v.ui',function(){function findElementNameForDiagnostic(diagnostic){let typeInfo=undefined;let curProto=diagnostic.constructor.prototype;while(curProto){typeInfo=tr.v.d.Diagnostic.findTypeInfo(curProto.constructor);if(typeInfo&&typeInfo.metadata.elementName)break;typeInfo=undefined;curProto=curProto.__proto__;}
 if(typeInfo===undefined){throw new Error(diagnostic.constructor.name+' or a base class must have a registered elementName');}
 const tagName=typeInfo.metadata.elementName;if(tr.ui.b.isUnknownElementName(tagName)){throw new Error('Element not registered: '+tagName);}
 return tagName;}
@@ -9162,7 +9610,7 @@
 return{createDiagnosticSpan,};});'use strict';tr.exportTo('tr.v.ui',function(){function makeColumn(title,histogram){return{title,value(map){const diagnostic=map.get(title);if(!diagnostic)return'';return tr.v.ui.createDiagnosticSpan(diagnostic,title,histogram);}};}
 Polymer({is:'tr-v-ui-diagnostic-map-table',created(){this.diagnosticMaps_=undefined;this.histogram_=undefined;this.isMetadata_=false;},set histogram(h){this.histogram_=h;},set isMetadata(m){this.isMetadata_=m;this.$.table.showHeader=!this.isMetadata_;},set diagnosticMaps(maps){this.diagnosticMaps_=maps;this.updateContents_();},get diagnosticMaps(){return this.diagnosticMaps_;},updateContents_(){if(this.isMetadata_&&this.diagnosticMaps_.length!==1){throw new Error('Metadata diagnostic-map-tables require exactly 1 DiagnosticMap');}
 if(this.diagnosticMaps_===undefined||this.diagnosticMaps_.length===0){this.$.table.tableRows=[];this.$.table.tableColumns=[];return;}
-let names=new Set();for(const map of this.diagnosticMaps_){for(const[name,diagnostic]of map){if(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet)continue;if(diagnostic instanceof tr.v.d.CollectedRelatedEventSet)continue;if(diagnostic instanceof tr.v.d.GroupingPath)continue;names.add(name);}}
+let names=new Set();for(const map of this.diagnosticMaps_){for(const[name,diagnostic]of map){if(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet)continue;if(diagnostic instanceof tr.v.d.CollectedRelatedEventSet)continue;names.add(name);}}
 names=Array.from(names).sort();const histogram=this.histogram_;if(this.isMetadata_){const diagnosticMap=this.diagnosticMaps_[0];this.$.table.tableColumns=[{value(name){return name.name;}},{value(name){const diagnostic=diagnosticMap.get(name.name);if(!diagnostic)return'';return tr.v.ui.createDiagnosticSpan(diagnostic,name.name,histogram);}},];this.$.table.tableRows=names.map(name=>{return{name};});}else{this.$.table.tableColumns=names.map(name=>makeColumn(name,histogram));this.$.table.tableRows=this.diagnosticMaps_;}
 this.$.table.rebuild();}});return{};});'use strict';tr.exportTo('tr.b',function(){class Serializable{constructor(){Object.defineProperty(this,'properties_',{configurable:false,enumerable:false,value:new Map(),});}
 define(name,initialValue){if(this[name]!==undefined){throw new Error(`"${name}" is already defined.`);}
@@ -9222,7 +9670,7 @@
 maps=[merged];}
 const mark=tr.b.Timing.mark('histogram-span',(this.viewState.mergeSampleDiagnostics?'merge':'split')+'SampleDiagnostics');this.$.sample_diagnostics.diagnosticMaps=maps;mark.end();if(this.anySampleDiagnostics_){this.$.diagnostics.selectedSubView=this.$.sample_diagnostics_container;}},get histogram(){return this.histogram_;},get referenceHistogram(){return this.referenceHistogram_;},getDeltaScalars_(statNames,scalarMap){if(!this.histogram.canCompare(this.referenceHistogram))return;for(const deltaStatName of tr.v.Histogram.getDeltaStatisticsNames(statNames)){if(IGNORE_DELTA_STATISTICS_NAMES.includes(deltaStatName))continue;const scalar=this.histogram.getStatisticScalar(deltaStatName,this.referenceHistogram,this.mwuResult_);if(scalar===undefined)continue;scalarMap.set(deltaStatName,scalar);}},set isYLogScale(logScale){this.chart_.isYLogScale=logScale;},async updateContents_(){this.$.chart.style.display='none';this.$.drag_handle.style.display='none';this.$.container.style.justifyContent='';while(Polymer.dom(this.$.chart).lastChild){Polymer.dom(this.$.chart).removeChild(Polymer.dom(this.$.chart).lastChild);}
 if(!this.histogram)return;this.$.container.style.display='';const scalarMap=new Map();this.getDeltaScalars_(this.histogram.statisticsNames,scalarMap);for(const[name,scalar]of this.histogram.statisticsScalars){scalarMap.set(name,scalar);}
-this.$.stats.scalarMap=scalarMap;this.updateSignificance_();const metricDiagnosticMap=new tr.v.d.DiagnosticMap();const metadataDiagnosticMap=new tr.v.d.DiagnosticMap();for(const[key,diagnostic]of this.histogram.diagnostics){if(key===tr.v.d.RESERVED_NAMES.MERGED_FROM)continue;if(key===tr.v.d.RESERVED_NAMES.MERGED_TO)continue;if(diagnostic instanceof tr.v.d.GroupingPath)continue;if(diagnostic instanceof tr.v.d.RelatedNameMap)continue;if(tr.v.d.RESERVED_NAMES_SET.has(key)){metadataDiagnosticMap.set(key,diagnostic);}else{metricDiagnosticMap.set(key,diagnostic);}}
+this.$.stats.scalarMap=scalarMap;this.updateSignificance_();const metricDiagnosticMap=new tr.v.d.DiagnosticMap();const metadataDiagnosticMap=new tr.v.d.DiagnosticMap();for(const[key,diagnostic]of this.histogram.diagnostics){if(diagnostic instanceof tr.v.d.RelatedNameMap)continue;if(tr.v.d.RESERVED_NAMES_SET.has(key)){metadataDiagnosticMap.set(key,diagnostic);}else{metricDiagnosticMap.set(key,diagnostic);}}
 const diagnosticTabs=[];if(metricDiagnosticMap.size){this.$.metric_diagnostics.diagnosticMaps=[metricDiagnosticMap];diagnosticTabs.push(this.$.metric_diagnostics);}
 if(this.anySampleDiagnostics_){diagnosticTabs.push(this.$.sample_diagnostics_container);}
 if(metadataDiagnosticMap.size){this.$.metadata_diagnostics.diagnosticMaps=[metadataDiagnosticMap];diagnosticTabs.push(this.$.metadata_diagnostics);}
@@ -9237,7 +9685,7 @@
 this.chart_.overrideDataRange=dataRange;this.chart_.data=chartData;this.$.stats_container.style.maxHeight=this.chart_.getBoundingClientRect().height+'px';}});});'use strict';tr.exportTo('tr.ui.analysis',function(){const EVENT_FIELD=[{key:'start',label:'Start'},{key:'cpuDuration',label:'CPU Duration'},{key:'duration',label:'Duration'},{key:'cpuSelfTime',label:'CPU Self Time'},{key:'selfTime',label:'Self Time'}];function buildDiagnostics_(slice){const diagnostics={};for(const item of EVENT_FIELD){const fieldName=item.key;if(slice[fieldName]===undefined)continue;diagnostics[fieldName]=new tr.v.d.Scalar(new tr.b.Scalar(tr.b.Unit.byName.timeDurationInMs,slice[fieldName]));}
 diagnostics.args=new tr.v.d.GenericSet([slice.args]);diagnostics.event=new tr.v.d.RelatedEventSet(slice);return diagnostics;}
 Polymer({is:'tr-ui-a-multi-event-sub-view',behaviors:[tr.ui.analysis.AnalysisSubView],created(){this.currentSelection_=undefined;this.eventsHaveDuration_=true;this.eventsHaveSubRows_=true;},ready(){this.$.radioPicker.style.display='none';this.$.radioPicker.items=EVENT_FIELD;this.$.radioPicker.select('cpuSelfTime');this.$.radioPicker.addEventListener('change',()=>{if(this.isAttached)this.updateContents_();});this.$.histogramSpan.graphWidth=400;this.$.histogramSpan.canMergeSampleDiagnostics=false;this.$.histogramContainer.style.display='none';},attached(){if(this.currentSelection_!==undefined)this.updateContents_();},set selection(selection){if(selection.length<=1){throw new Error('Only supports multiple items');}
-this.setSelectionWithoutErrorChecks(selection);},get selection(){return this.currentSelection_;},setSelectionWithoutErrorChecks(selection){this.currentSelection_=selection;if(this.isAttached)this.updateContents_();},get eventsHaveDuration(){return this.eventsHaveDuration_;},set eventsHaveDuration(eventsHaveDuration){this.eventsHaveDuration_=eventsHaveDuration;if(this.isAttached)this.updateContents_();},get eventsHaveSubRows(){return this.eventsHaveSubRows_;},set eventsHaveSubRows(eventsHaveSubRows){this.eventsHaveSubRows_=eventsHaveSubRows;if(this.isAttached)this.updateContents_();},buildHistogram_(selectedKey){let leftBoundary=Number.MAX_VALUE;let rightBoundary=tr.b.math.Statistics.percentile(this.currentSelection_,0.95,function(value){leftBoundary=Math.min(leftBoundary,value[selectedKey]);return value[selectedKey];});if(leftBoundary===rightBoundary)rightBoundary+=1;const histogram=new tr.v.Histogram('',tr.b.Unit.byName.timeDurationInMs,tr.v.HistogramBinBoundaries.createLinear(leftBoundary,rightBoundary,Math.ceil(Math.sqrt(this.currentSelection_.length))));histogram.customizeSummaryOptions({sum:false});for(const slice of this.currentSelection_){histogram.addSample(slice[selectedKey],buildDiagnostics_(slice));}
+this.setSelectionWithoutErrorChecks(selection);},get selection(){return this.currentSelection_;},setSelectionWithoutErrorChecks(selection){this.currentSelection_=selection;if(this.isAttached)this.updateContents_();},get eventsHaveDuration(){return this.eventsHaveDuration_;},set eventsHaveDuration(eventsHaveDuration){this.eventsHaveDuration_=eventsHaveDuration;if(this.isAttached)this.updateContents_();},get eventsHaveSubRows(){return this.eventsHaveSubRows_;},set eventsHaveSubRows(eventsHaveSubRows){this.eventsHaveSubRows_=eventsHaveSubRows;if(this.isAttached)this.updateContents_();},buildHistogram_(selectedKey){let leftBoundary=Number.MAX_VALUE;let rightBoundary=tr.b.math.Statistics.percentile(this.currentSelection_,0.95,function(value){leftBoundary=Math.min(leftBoundary,value[selectedKey]);return value[selectedKey];});if(leftBoundary===rightBoundary)rightBoundary+=1;const histogram=new tr.v.Histogram('',tr.b.Unit.byName.timeDurationInMs,tr.v.HistogramBinBoundaries.createLinear(leftBoundary,rightBoundary,Math.ceil(Math.sqrt(this.currentSelection_.length))));histogram.customizeSummaryOptions({sum:false,percentile:[0.5,0.9],});for(const slice of this.currentSelection_){histogram.addSample(slice[selectedKey],buildDiagnostics_(slice));}
 return histogram;},updateContents_(){const selection=this.currentSelection_;if(!selection)return;const eventsByTitle=selection.getEventsOrganizedByTitle();const numTitles=Object.keys(eventsByTitle).length;this.$.eventSummaryTable.configure({showTotals:numTitles>1,eventsByTitle,eventsHaveDuration:this.eventsHaveDuration_,eventsHaveSubRows:this.eventsHaveSubRows_});this.$.selectionSummaryTable.selection=this.currentSelection_;if(numTitles===1){this.$.radioPicker.style.display='block';this.$.histogramContainer.style.display='flex';this.$.histogramSpan.build(this.buildHistogram_(this.$.radioPicker.selectedKey));if(this.$.histogramSpan.histogram.numValues===0){this.$.histogramContainer.style.display='none';}}else{this.$.radioPicker.style.display='none';this.$.histogramContainer.style.display='none';}}});return{};});'use strict';tr.exportTo('tr.ui.analysis',function(){const FLOW_IN=0x1;const FLOW_OUT=0x2;const FLOW_IN_OUT=FLOW_IN|FLOW_OUT;function FlowClassifier(){this.numEvents_=0;this.eventsByGUID_={};}
 FlowClassifier.prototype={getFS_(event){let fs=this.eventsByGUID_[event.guid];if(fs===undefined){this.numEvents_++;fs={state:0,event};this.eventsByGUID_[event.guid]=fs;}
 return fs;},addInFlow(event){const fs=this.getFS_(event);fs.state|=FLOW_IN;return event;},addOutFlow(event){const fs=this.getFS_(event);fs.state|=FLOW_OUT;return event;},hasEvents(){return this.numEvents_>0;},get inFlowEvents(){const selection=new tr.model.EventSet();for(const guid in this.eventsByGUID_){const fs=this.eventsByGUID_[guid];if(fs.state===FLOW_IN){selection.push(fs.event);}}
@@ -9544,7 +9992,7 @@
 this.drawFlowArrow_(ctx,events[i],canvasBounds);}},drawFlowArrow_(ctx,flowEvent,canvasBounds){const dt=this.viewport.currentDisplayTransform;const pixelRatio=window.devicePixelRatio||1;const startTrack=this.viewport.trackForEvent(flowEvent.startSlice);const endTrack=this.viewport.trackForEvent(flowEvent.endSlice);if(startTrack===undefined||endTrack===undefined)return;const startBounds=startTrack.getBoundingClientRect();const endBounds=endTrack.getBoundingClientRect();if(flowEvent.selectionState===SelectionState.SELECTED){ctx.shadowBlur=1;ctx.shadowColor='red';ctx.shadowOffsety=2;ctx.strokeStyle=tr.b.ColorScheme.colorsAsStrings[tr.b.ColorScheme.getVariantColorId(flowEvent.colorId,tr.b.ColorScheme.properties.brightenedOffsets[0])];}else if(flowEvent.selectionState===SelectionState.HIGHLIGHTED){ctx.shadowBlur=1;ctx.shadowColor='red';ctx.shadowOffsety=2;ctx.strokeStyle=tr.b.ColorScheme.colorsAsStrings[tr.b.ColorScheme.getVariantColorId(flowEvent.colorId,tr.b.ColorScheme.properties.brightenedOffsets[0])];}else if(flowEvent.selectionState===SelectionState.DIMMED){ctx.shadowBlur=0;ctx.shadowOffsetX=0;ctx.strokeStyle=tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];}else{let hasBoost=false;const startSlice=flowEvent.startSlice;hasBoost|=startSlice.selectionState===SelectionState.SELECTED;hasBoost|=startSlice.selectionState===SelectionState.HIGHLIGHTED;const endSlice=flowEvent.endSlice;hasBoost|=endSlice.selectionState===SelectionState.SELECTED;hasBoost|=endSlice.selectionState===SelectionState.HIGHLIGHTED;if(hasBoost){ctx.shadowBlur=1;ctx.shadowColor='rgba(255, 0, 0, 0.4)';ctx.shadowOffsety=2;ctx.strokeStyle=tr.b.ColorScheme.colorsAsStrings[tr.b.ColorScheme.getVariantColorId(flowEvent.colorId,tr.b.ColorScheme.properties.brightenedOffsets[0])];}else{ctx.shadowBlur=0;ctx.shadowOffsetX=0;ctx.strokeStyle=tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];}}
 const startSize=startBounds.left+startBounds.top+
 startBounds.bottom+startBounds.right;const endSize=endBounds.left+endBounds.top+
-endBounds.bottom+endBounds.right;if(startSize===0&&endSize===0)return;const startY=this.calculateTrackY_(startTrack,canvasBounds);const endY=this.calculateTrackY_(endTrack,canvasBounds);const pixelStartY=pixelRatio*startY;const pixelEndY=pixelRatio*endY;const startXView=dt.xWorldToView(flowEvent.start);const endXView=dt.xWorldToView(flowEvent.end);const midXView=(startXView+endXView)/2;ctx.beginPath();ctx.moveTo(startXView,pixelStartY);ctx.bezierCurveTo(midXView,pixelStartY,midXView,pixelEndY,endXView,pixelEndY);ctx.stroke();const arrowWidth=5*pixelRatio;const distance=endXView-startXView;if(distance<=(2*arrowWidth))return;const tipX=endXView;const tipY=pixelEndY;const arrowHeight=(endBounds.height/4)*pixelRatio;tr.ui.b.drawTriangle(ctx,tipX,tipY,tipX-arrowWidth,tipY-arrowHeight,tipX-arrowWidth,tipY+arrowHeight);ctx.fill();},drawVSyncHighlight(ctx,dt,viewLWorld,viewRWorld,viewHeight){if(!this.viewport_.highlightVSync){return;}
+endBounds.bottom+endBounds.right;if(startSize===0&&endSize===0)return;const startY=this.calculateTrackY_(startTrack,canvasBounds);const endY=this.calculateTrackY_(endTrack,canvasBounds);const worldOffset=this.getBoundingClientRect().top-canvasBounds.top;const pixelStartY=pixelRatio*(startY-worldOffset);const pixelEndY=pixelRatio*(endY-worldOffset);const startXView=dt.xWorldToView(flowEvent.start);const endXView=dt.xWorldToView(flowEvent.end);const midXView=(startXView+endXView)/2;ctx.beginPath();ctx.moveTo(startXView,pixelStartY);ctx.bezierCurveTo(midXView,pixelStartY,midXView,pixelEndY,endXView,pixelEndY);ctx.stroke();const arrowWidth=5*pixelRatio;const distance=endXView-startXView;if(distance<=(2*arrowWidth))return;const tipX=endXView;const tipY=pixelEndY;const arrowHeight=(endBounds.height/4)*pixelRatio;tr.ui.b.drawTriangle(ctx,tipX,tipY,tipX-arrowWidth,tipY-arrowHeight,tipX-arrowWidth,tipY+arrowHeight);ctx.fill();},drawVSyncHighlight(ctx,dt,viewLWorld,viewRWorld,viewHeight){if(!this.viewport_.highlightVSync){return;}
 const stripes=ModelTrack.generateStripes_(this.vSyncTimes_,viewLWorld,viewRWorld);if(stripes.length===0){return;}
 const vSyncHighlightColor=new tr.b.Color(ColorScheme.getColorForReservedNameAsString('vsync_highlight_color'));const stripeRange=stripes[stripes.length-1].max-stripes[0].min;const stripeDensity=stripeRange?stripes.length/(dt.scaleX*stripeRange):0;const clampedStripeDensity=tr.b.math.clamp(stripeDensity,ModelTrack.VSYNC_DENSITY_OPAQUE,ModelTrack.VSYNC_DENSITY_TRANSPARENT);const opacity=(ModelTrack.VSYNC_DENSITY_TRANSPARENT-clampedStripeDensity)/ModelTrack.VSYNC_DENSITY_RANGE;if(opacity===0){return;}
 ctx.fillStyle=vSyncHighlightColor.toStringWithAlphaOverride(ModelTrack.VSYNC_HIGHLIGHT_ALPHA*opacity);for(let i=0;i<stripes.length;i++){const xLeftView=dt.xWorldToView(stripes[i].min);const xRightView=dt.xWorldToView(stripes[i].max);ctx.fillRect(xLeftView,0,xRightView-xLeftView,viewHeight);}},calculateTrackY_(track,canvasBounds){const bounds=track.getBoundingClientRect();const size=bounds.left+bounds.top+bounds.bottom+bounds.right;if(size===0){return this.calculateTrackY_(Polymer.dom(track).parentNode,canvasBounds);}
@@ -9707,10 +10155,10 @@
 function validateTraceCategories(requiredCategories,categories){if(!requiredCategories)return;if(!categories)throw new Error('Missing trace config metadata');for(const cat of requiredCategories){const isDisabledByDefault=(cat.indexOf('disabled-by-default')===0);let missing=false;if(isDisabledByDefault){if(!categories.included.includes(cat)){missing=true;}}else if(categories.excluded.includes(cat)){missing=true;}
 if(missing){throw new Error(`Trace is missing required category "${cat}"`);}}}
 function validateDiagnosticNames(histograms){for(const hist of histograms){for(const name of hist.diagnostics.keys()){if(tr.v.d.RESERVED_NAMES_SET.has(name)){throw new Error(`Illegal diagnostic name "${name}" on Histogram "${hist.name}"`);}}}}
-function addTelemetryInfo(histograms,model){for(const metadata of model.metadata){if(!metadata.value||!metadata.value.telemetry)continue;const traceUrls=metadata.value.telemetry[tr.v.d.RESERVED_NAMES.TRACE_URLS];if(traceUrls&&model.canonicalUrl!==traceUrls[0]){throw new Error(`canonicalUrl "${model.canonicalUrl}" != `+`traceUrl "${traceUrls[0]}"`);}
-for(const[name,value]of Object.entries(metadata.value.telemetry)){const type=tr.v.d.RESERVED_NAMES_TO_TYPES.get(name);if(type===undefined){throw new Error(`Unexpected telemetry.${name}`);}
+function addTelemetryInfo(histograms,model){for(const metadata of model.metadata){if(!metadata.value||!metadata.value.telemetry)continue;for(const[name,value]of Object.entries(metadata.value.telemetry)){const type=tr.v.d.RESERVED_NAMES_TO_TYPES.get(name);if(type===undefined){throw new Error(`Unexpected telemetry.${name}`);}
 histograms.addSharedDiagnosticToAllHistograms(name,new type(value));}}}
-function metricMapFunction(result,model,options){const histograms=runMetrics(model,options,result.addFailure.bind(result));addTelemetryInfo(histograms,model);result.addPair('histograms',histograms.asDicts());const scalarDicts=[];for(const value of histograms){for(const[statName,scalar]of value.statisticsScalars){scalarDicts.push({name:value.name+'_'+statName,numeric:scalar.asDict(),description:value.description,});}}
+function metricMapFunction(result,model,options){const histograms=runMetrics(model,options,result.addFailure.bind(result));addTelemetryInfo(histograms,model);if(model.canonicalUrl!==undefined){const info=tr.v.d.RESERVED_INFOS.TRACE_URLS;histograms.addSharedDiagnosticToAllHistograms(info.name,new info.type([model.canonicalUrl]));}
+result.addPair('histograms',histograms.asDicts());const scalarDicts=[];for(const value of histograms){for(const[statName,scalar]of value.statisticsScalars){scalarDicts.push({name:value.name+'_'+statName,numeric:scalar.asDict(),description:value.description,});}}
 result.addPair('scalars',scalarDicts);}
 tr.mre.FunctionRegistry.register(metricMapFunction);return{metricMapFunction,runMetrics,};});'use strict';tr.exportTo('tr.mre',function(){class MreResult{constructor(failures,pairs){if(failures===undefined){failures=[];}
 if(pairs===undefined){pairs={};}
@@ -9766,7 +10214,7 @@
 str+=cell;}
 str+='\n';}
 return str;}}
-return{CSVBuilder,};});'use strict';tr.exportTo('tr.v',function(){const getDisplayLabel=tr.v.HistogramGrouping.DISPLAY_LABEL.callback;const DEFAULT_POSSIBLE_GROUPS=[];DEFAULT_POSSIBLE_GROUPS.push(new tr.v.HistogramGrouping(tr.v.HistogramGrouping.HISTOGRAM_NAME.key,h=>h.shortName||h.name));const EXCLUDED_GROUPING_KEYS=[tr.v.HistogramGrouping.HISTOGRAM_NAME.key,tr.v.HistogramGrouping.DISPLAY_LABEL.key,];for(const group of tr.v.HistogramGrouping.BY_KEY.values()){if(EXCLUDED_GROUPING_KEYS.includes(group.key))continue;DEFAULT_POSSIBLE_GROUPS.push(group);}
+return{CSVBuilder,};});'use strict';tr.exportTo('tr.v',function(){const getDisplayLabel=tr.v.HistogramGrouping.DISPLAY_LABEL.callback;const DEFAULT_POSSIBLE_GROUPS=[];const EXCLUDED_GROUPING_KEYS=[tr.v.HistogramGrouping.DISPLAY_LABEL.key,];for(const group of tr.v.HistogramGrouping.BY_KEY.values()){if(EXCLUDED_GROUPING_KEYS.includes(group.key))continue;DEFAULT_POSSIBLE_GROUPS.push(group);}
 class HistogramParameterCollector{constructor(){this.statisticNames_=new Set(['avg']);this.labelsToStartTimes_=new Map();this.keysToGroupings_=new Map(DEFAULT_POSSIBLE_GROUPS.map(g=>[g.key,g]));this.keysToValues_=new Map(DEFAULT_POSSIBLE_GROUPS.map(g=>[g.key,new Set()]));this.keysToValues_.delete(tr.v.HistogramGrouping.HISTOGRAM_NAME.key);}
 process(histograms){const allStoryTags=new Set();let maxSampleCount=0;for(const hist of histograms){maxSampleCount=Math.max(maxSampleCount,hist.numValues);for(const statName of hist.statisticsNames){this.statisticNames_.add(statName);}
 let startTime=hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START);if(startTime!==undefined)startTime=startTime.minDate.getTime();const displayLabel=getDisplayLabel(hist);if(this.labelsToStartTimes_.has(displayLabel)){startTime=Math.min(startTime,this.labelsToStartTimes_.get(displayLabel));}
@@ -9805,35 +10253,20 @@
 if(names.includes(this.viewState.displayStatisticName)){this.displayStatisticName=this.viewState.displayStatisticName;this.$.statistic.value=this.displayStatisticName;}else{this.viewState.displayStatisticName=names[0]||'';}},get anyOverviewCharts_(){for(const row of tr.v.ui.HistogramSetTableRowState.walkAll(this.viewState.tableRowStates.values())){if(row.isOverviewed)return true;}
 return false;},async toggleOverviewLineCharts_(){const showOverviews=!this.anyOverviewCharts_;const mark=tr.b.Timing.mark('histogram-set-controls',(showOverviews?'show':'hide')+'OverviewCharts');for(const row of tr.v.ui.HistogramSetTableRowState.walkAll(this.viewState.tableRowStates.values())){await row.update({isOverviewed:showOverviews});}
 this.$.hide_overview.style.display=showOverviews?'inline':'none';this.$.show_overview.style.display=showOverviews?'none':'inline';await tr.b.animationFrame();mark.end();},set helpHref(href){this.$.help.href=href;this.$.help.style.display='inline';},set feedbackHref(href){this.$.feedback.href=href;this.$.feedback.style.display='inline';},clearSearch_(){this.set('searchQuery','');this.$.search.focus();},getAlphaString_(alphaIndex){return(''+ALPHA_OPTIONS[alphaIndex]).substr(0,5);},openAlphaSlider_(){const alphaButtonRect=this.$.alpha.getBoundingClientRect();this.$.alpha_slider_container.style.display='flex';this.$.alpha_slider_container.style.top=alphaButtonRect.bottom+'px';this.$.alpha_slider_container.style.left=alphaButtonRect.left+'px';this.$.alpha_slider.focus();},closeAlphaSlider_(){this.$.alpha_slider_container.style.display='';},updateAlpha_(){this.alphaIndex=this.$.alpha_slider.value;},getAlphaIndexFromViewState_(){for(let i=0;i<ALPHA_OPTIONS.length;++i){if(ALPHA_OPTIONS[i]>=this.viewState.alpha)return i;}
-return ALPHA_OPTIONS.length-1;},set enableVisualization(enable){this.$.show_visualization.style.display=enable?'inline':'none';},loadVisualization_(){tr.b.dispatchSimpleEvent(this,'loadVisualization',true,true,{});},});return{};});'use strict';tr.exportTo('tr.v',function(){function deleteMergedToDiagnostics(histogramArrayMap){for(const[name,histograms]of histogramArrayMap){if(histograms instanceof Array){for(const histogram of histograms){histogram.diagnostics.delete(tr.v.d.RESERVED_NAMES.MERGED_TO);}}else if(histograms instanceof Map){deleteMergedToDiagnostics(histograms);}}}
-class HistogramSetHierarchy{constructor(name){this.name=name;this.description='';this.depth=0;this.subRows=[];this.columns=new Map();this.mergeRelationshipsForColumn_=new Map();}*walk(){yield this;for(const row of this.subRows)yield*row.walk();}
+return ALPHA_OPTIONS.length-1;},set enableVisualization(enable){this.$.show_visualization.style.display=enable?'inline':'none';},loadVisualization_(){tr.b.dispatchSimpleEvent(this,'loadVisualization',true,true,{});},});return{};});'use strict';tr.exportTo('tr.v',function(){class HistogramSetHierarchy{constructor(name){this.name=name;this.description='';this.depth=0;this.subRows=[];this.columns=new Map();}*walk(){yield this;for(const row of this.subRows)yield*row.walk();}
 static*walkAll(rootRows){for(const rootRow of rootRows)yield*rootRow.walk();}
 static build(histogramArrayMap){const rootRows=[];HistogramSetHierarchy.buildInternal_(histogramArrayMap,[],rootRows);const histograms=new tr.v.HistogramSet();for(const row of HistogramSetHierarchy.walkAll(rootRows)){for(const hist of row.columns.values()){if(!(hist instanceof tr.v.Histogram))continue;histograms.addHistogram(hist);}}
-histograms.deduplicateDiagnostics();for(const row of HistogramSetHierarchy.walkAll(rootRows)){for(const[name,hist]of row.columns){if(!(hist instanceof tr.v.Histogram))continue;if(!row.mergeRelationshipsForColumn_.get(name))continue;hist.diagnostics.mergeRelationships(hist);}}
-deleteMergedToDiagnostics(histogramArrayMap);for(const row of HistogramSetHierarchy.walkAll(rootRows)){row.maybeRebin_();}
+histograms.deduplicateDiagnostics();for(const row of HistogramSetHierarchy.walkAll(rootRows)){row.maybeRebin_();}
 return rootRows;}
 maybeRebin_(){const dataRange=new tr.b.math.Range();for(const hist of this.columns.values()){if(!(hist instanceof tr.v.Histogram))continue;if(hist.allBins.length>1)return;if(hist.numValues===0)continue;dataRange.addValue(hist.min);dataRange.addValue(hist.max);}
 dataRange.addValue(tr.b.math.lesserWholeNumber(dataRange.min));dataRange.addValue(tr.b.math.greaterWholeNumber(dataRange.max));if(dataRange.min===dataRange.max)return;const boundaries=tr.v.HistogramBinBoundaries.createLinear(dataRange.min,dataRange.max,tr.v.DEFAULT_REBINNED_COUNT);for(const[name,hist]of this.columns){if(!(hist instanceof tr.v.Histogram))continue;this.columns.set(name,hist.rebin(boundaries));}}
-static mergeHistogramDownHierarchy_(histogram,hierarchy,columnName){let groupingPath=undefined;for(const row of hierarchy){if(groupingPath!==undefined){groupingPath.push(row.name);}else if(row.name===histogram.name){groupingPath=[];}
-if(!row.description){row.description=histogram.description;}
-const existing=row.columns.get(columnName);if(existing===undefined){const clone=histogram.clone();if(groupingPath!==undefined){new tr.v.d.GroupingPath(groupingPath).addToHistogram(clone);}
-row.columns.set(columnName,clone);row.mergeRelationshipsForColumn_.set(columnName,true);continue;}
+static mergeHistogramDownHierarchy_(histogram,hierarchy,columnName){for(const row of hierarchy){if(!row.description){row.description=histogram.description;}
+const existing=row.columns.get(columnName);if(existing===undefined){row.columns.set(columnName,histogram.clone());continue;}
 if(existing instanceof tr.v.HistogramSet){existing.addHistogram(histogram);continue;}
-if(!existing.canAddHistogram(histogram)){const unmergeableHistograms=new tr.v.HistogramSet([histogram]);const mergedFrom=existing.diagnostics.get(tr.v.d.RESERVED_NAMES.MERGED_FROM);if(mergedFrom!==undefined){for(const[unusedName,origHist]of mergedFrom){unmergeableHistograms.addHistogram(origHist);}}
-row.columns.set(columnName,unmergeableHistograms);continue;}
-if(existing.name!==histogram.name){row.mergeRelationshipsForColumn_.set(name,false);}
+if(!existing.canAddHistogram(histogram)){const unmergeableHistograms=new tr.v.HistogramSet([histogram]);row.columns.set(columnName,unmergeableHistograms);continue;}
 existing.addHistogram(histogram);}}
-static buildInternal_(histogramArrayMap,hierarchy,rootRows){for(const[name,histograms]of histogramArrayMap){if(histograms instanceof Array){for(const histogram of histograms){HistogramSetHierarchy.mergeHistogramDownHierarchy_(histogram,hierarchy,name);}}else if(histograms instanceof Map){const row=new HistogramSetHierarchy(name);row.depth=hierarchy.length;hierarchy.push(row);HistogramSetHierarchy.buildInternal_(histograms,hierarchy,rootRows);hierarchy.pop();if(hierarchy.length===0){rootRows.push(row);}else{const parentRow=hierarchy[hierarchy.length-1];parentRow.subRows.push(row);}}}}
-static filter(rows,histograms){const results=[];for(const row of rows){let filteredSubRows=[];if(row.subRows.length>0){filteredSubRows=HistogramSetHierarchy.filter(row.subRows,histograms);if(filteredSubRows.length===0)continue;}else{let found=false;for(const testHist of row.columns.values()){if(testHist instanceof tr.v.HistogramSet){for(const origHist of testHist){if(histograms.lookupHistogram(origHist.guid)!==undefined){found=true;break;}}
-if(found)break;continue;}
-if(!(testHist instanceof tr.v.Histogram)){throw new Error('Cells can only contain Histogram or HistogramSet');}
-if(histograms.lookupHistogram(testHist.guid)!==undefined){found=true;break;}
-const mergedFrom=testHist.diagnostics.get(tr.v.d.RESERVED_NAMES.MERGED_FROM);if(mergedFrom!==undefined){for(const[unusedName,origHist]of mergedFrom){if(histograms.lookupHistogram(origHist.guid)!==undefined){found=true;break;}}}
-if(found)break;}
-if(!found)continue;}
-const clone=new HistogramSetHierarchy(row.name);clone.description=row.description;clone.depth=row.depth;clone.subRows=filteredSubRows;clone.columns=row.columns;results.push(clone);}
-return results;}}
-return{HistogramSetHierarchy,};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-histogram-set-table-cell',created(){this.viewState_=undefined;this.rootListener_=this.onRootStateUpdate_.bind(this);this.row_=undefined;this.displayLabel_='';this.histogram_=undefined;this.histogramSpan_=undefined;this.overviewChart_=undefined;this.mwuResult_=undefined;},ready(){this.addEventListener('click',this.onClick_.bind(this));},attached(){if(this.row){this.row.rootViewState.addUpdateListener(this.rootListener_);}},detached(){this.row.rootViewState.removeUpdateListener(this.rootListener_);},updateMwu_(){const referenceHistogram=this.referenceHistogram;this.mwuResult_=undefined;if(!(this.histogram instanceof tr.v.Histogram))return;if(!this.histogram.canCompare(referenceHistogram))return;this.mwuResult_=tr.b.math.Statistics.mwu(this.histogram.sampleValues,referenceHistogram.sampleValues,this.row.rootViewState.alpha);},build(row,displayLabel,viewState){this.row_=row;this.displayLabel_=displayLabel;this.viewState_=viewState;this.histogram_=this.row.columns.get(displayLabel);if(this.viewState){this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));}
+static buildInternal_(histogramArrayMap,hierarchy,rootRows){for(const[name,histograms]of histogramArrayMap){if(histograms instanceof Array){for(const histogram of histograms){HistogramSetHierarchy.mergeHistogramDownHierarchy_(histogram,hierarchy,name);}}else if(histograms instanceof Map){const row=new HistogramSetHierarchy(name);row.depth=hierarchy.length;hierarchy.push(row);HistogramSetHierarchy.buildInternal_(histograms,hierarchy,rootRows);hierarchy.pop();if(hierarchy.length===0){rootRows.push(row);}else{const parentRow=hierarchy[hierarchy.length-1];parentRow.subRows.push(row);}}}}}
+return{HistogramSetHierarchy};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-histogram-set-table-cell',created(){this.viewState_=undefined;this.rootListener_=this.onRootStateUpdate_.bind(this);this.row_=undefined;this.displayLabel_='';this.histogram_=undefined;this.histogramSpan_=undefined;this.overviewChart_=undefined;this.mwuResult_=undefined;},ready(){this.addEventListener('click',this.onClick_.bind(this));},attached(){if(this.row){this.row.rootViewState.addUpdateListener(this.rootListener_);}},detached(){this.row.rootViewState.removeUpdateListener(this.rootListener_);},updateMwu_(){const referenceHistogram=this.referenceHistogram;this.mwuResult_=undefined;if(!(this.histogram instanceof tr.v.Histogram))return;if(!this.histogram.canCompare(referenceHistogram))return;this.mwuResult_=tr.b.math.Statistics.mwu(this.histogram.sampleValues,referenceHistogram.sampleValues,this.row.rootViewState.alpha);},build(row,displayLabel,viewState){this.row_=row;this.displayLabel_=displayLabel;this.viewState_=viewState;this.histogram_=this.row.columns.get(displayLabel);if(this.viewState){this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));}
 this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this));if(this.isAttached){this.row.rootViewState.addUpdateListener(this.rootListener_);}
 this.updateMwu_();this.updateContents_();},updateSignificance_(){if(!this.mwuResult_)return;this.$.scalar.significance=this.mwuResult_.significance;},get viewState(){return this.viewState_;},get row(){return this.row_;},get histogram(){return this.histogram_;},get referenceHistogram(){const referenceDisplayLabel=this.row.rootViewState.referenceDisplayLabel;if(!referenceDisplayLabel)return undefined;if(referenceDisplayLabel===this.displayLabel_)return undefined;return this.row.columns.get(referenceDisplayLabel);},get isHistogramOpen(){return(this.histogramSpan_!==undefined)&&(this.$.histogram.style.display==='block');},set isHistogramOpen(open){if(!(this.histogram instanceof tr.v.Histogram)||(this.histogram.numValues===0)){return;}
 this.$.scalar.style.display=open?'none':'flex';this.$.open_histogram.style.display=open?'none':'block';this.$.close_histogram.style.display=open?'block':'none';this.$.histogram.style.display=open?'block':'none';if(open&&this.histogramSpan_===undefined){this.histogramSpan_=document.createElement('tr-v-ui-histogram-span');this.histogramSpan_.viewState=this.viewState;this.histogramSpan_.rowState=this.row.viewState;this.histogramSpan_.rootState=this.row.rootViewState;this.histogramSpan_.build(this.histogram,this.referenceHistogram);this.$.histogram.appendChild(this.histogramSpan_);}
@@ -9893,53 +10326,71 @@
 for(const row of this.subRows){const previousState=vs.subRows.get(row.name);if(!previousState)continue;await row.restoreState(previousState);}}
 sortSubRows(){const sortColumn=this.baseTable_.tableColumns[this.rootViewState_.sortColumnIndex];if(sortColumn===undefined)return;this.subRows_.sort(sortColumn.cmp);if(this.rootViewState_.sortDescending){this.subRows_.reverse();}}}
 return{HistogramSetTableRow,};});'use strict';tr.exportTo('tr.v.ui',function(){const MIDLINE_HORIZONTAL_ELLIPSIS=String.fromCharCode(0x22ef);function escapeRegExp(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,'\\$&');}
-Polymer({is:'tr-v-ui-histogram-set-table',created(){this.viewState_=undefined;this.progress_=()=>Promise.resolve();this.nameColumnTitle_=undefined;this.displayLabels_=[];this.histograms_=undefined;this.sourceHistograms_=undefined;this.groupedHistograms_=undefined;this.hierarchies_=undefined;this.tableRows_=undefined;this.sortColumnChangedListener_=e=>this.onSortColumnChanged_(e);},ready(){this.$.table.zebra=true;this.addEventListener('sort-column-changed',this.sortColumnChangedListener_);this.addEventListener('requestSelectionChange',this.onRequestSelectionChange_.bind(this));this.addEventListener('row-expanded-changed',this.onRowExpandedChanged_.bind(this));},get viewState(){return this.viewState_;},set viewState(vs){if(this.viewState_){throw new Error('viewState must be set exactly once.');}
-this.viewState_=vs;this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));},get histograms(){return this.histograms_;},async build(histograms,sourceHistograms,displayLabels,opt_progress){this.histograms_=histograms;this.sourceHistograms_=sourceHistograms;this.groupedHistograms_=undefined;this.displayLabels_=displayLabels;if(opt_progress!==undefined)this.progress_=opt_progress;if(histograms.length===0){throw new Error('histogram-set-table requires non-empty HistogramSet.');}
+Polymer({is:'tr-v-ui-histogram-set-table',created(){this.viewState_=undefined;this.progress_=()=>Promise.resolve();this.nameColumnTitle_=undefined;this.displayLabels_=[];this.histograms_=undefined;this.sourceHistograms_=undefined;this.filteredHistograms_=undefined;this.groupedHistograms_=undefined;this.hierarchies_=undefined;this.tableRows_=undefined;this.sortColumnChangedListener_=e=>this.onSortColumnChanged_(e);},ready(){this.$.table.zebra=true;this.addEventListener('sort-column-changed',this.sortColumnChangedListener_);this.addEventListener('requestSelectionChange',this.onRequestSelectionChange_.bind(this));this.addEventListener('row-expanded-changed',this.onRowExpandedChanged_.bind(this));},get viewState(){return this.viewState_;},set viewState(vs){if(this.viewState_){throw new Error('viewState must be set exactly once.');}
+this.viewState_=vs;this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));},get histograms(){return this.histograms_;},async build(histograms,sourceHistograms,displayLabels,opt_progress){this.histograms_=histograms;this.sourceHistograms_=sourceHistograms;this.filteredHistograms_=undefined;this.groupedHistograms_=undefined;this.displayLabels_=displayLabels;if(opt_progress!==undefined)this.progress_=opt_progress;if(histograms.length===0){throw new Error('histogram-set-table requires non-empty HistogramSet.');}
 await this.progress_('Building columns...');this.$.table.tableColumns=[{title:this.buildNameColumnTitle_(),value:row=>row.nameCell,cmp:(a,b)=>a.compareNames(b),}].concat(displayLabels.map(l=>this.buildColumn_(l)));tr.b.Timing.instant('histogram-set-table','columnCount',this.$.table.tableColumns.length);await this.updateContents_();this.fire('display-ready');this.progress_=()=>Promise.resolve();this.checkNameColumnOverflow_(tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows));},buildNameColumnTitle_(){this.nameColumnTitle_=document.createElement('span');this.nameColumnTitle_.style.display='inline-flex';const nameEl=document.createElement('span');nameEl.textContent='Name';this.nameColumnTitle_.appendChild(nameEl);const toggleWidthEl=document.createElement('span');toggleWidthEl.style.fontWeight='bold';toggleWidthEl.style.background='#bbb';toggleWidthEl.style.color='#333';toggleWidthEl.style.padding='0px 3px';toggleWidthEl.style.marginRight='8px';toggleWidthEl.style.display='none';toggleWidthEl.textContent=MIDLINE_HORIZONTAL_ELLIPSIS;toggleWidthEl.addEventListener('click',this.toggleNameColumnWidth_.bind(this));this.nameColumnTitle_.appendChild(toggleWidthEl);return this.nameColumnTitle_;},toggleNameColumnWidth_(opt_event){this.viewState.update({constrainNameColumn:!this.viewState.constrainNameColumn,});if(opt_event!==undefined){opt_event.stopPropagation();opt_event.preventDefault();tr.b.Timing.instant('histogram-set-table','nameColumn'+
-(this.viewState.constrainNameColumn?'Constrained':'Unconstrained'));}},buildColumn_(displayLabel){const title=document.createElement('span');title.textContent=displayLabel;title.style.whiteSpace='pre';return{displayLabel,title,value:row=>row.getCell(displayLabel),cmp:(rowA,rowB)=>rowA.compareCells(rowB,displayLabel),};},async updateContents_(){if(this.groupedHistograms_===undefined){await this.progress_('Grouping Histograms...');this.groupHistograms_();}
-if(this.hierarchies_===undefined){await this.progress_('Merging Histograms...');this.hierarchies_=tr.v.HistogramSetHierarchy.build(this.groupedHistograms_);this.tableRows_=undefined;}
-const tableRowsDirty=this.tableRows_===undefined;const previousRowStates=this.viewState.tableRowStates;if(tableRowsDirty){await this.progress_('Filtering rows...');let filteredHistograms=this.viewState.showAll?this.histograms:this.sourceHistograms_;if(this.viewState.searchQuery){let query=undefined;try{query=new RegExp(this.viewState.searchQuery);}catch(e){}
-if(query!==undefined){filteredHistograms=new tr.v.HistogramSet([...filteredHistograms].filter(hist=>hist.name.match(query)));if(filteredHistograms.length===0&&!this.viewState.showAll){await this.viewState.update({showAll:true});return;}}}
-const filteredHierarchies=tr.v.HistogramSetHierarchy.filter(this.hierarchies_,filteredHistograms);this.tableRows_=filteredHierarchies.map(hierarchy=>new tr.v.ui.HistogramSetTableRow(hierarchy,this.$.table,this.viewState));tr.b.Timing.instant('histogram-set-table','rootRowCount',this.tableRows_.length);const namesToRowStates=new Map();for(const row of this.tableRows_){namesToRowStates.set(row.name,row.viewState);}
+(this.viewState.constrainNameColumn?'Constrained':'Unconstrained'));}},buildColumn_(displayLabel){const title=document.createElement('span');title.textContent=displayLabel;title.style.whiteSpace='pre';return{displayLabel,title,value:row=>row.getCell(displayLabel),cmp:(rowA,rowB)=>rowA.compareCells(rowB,displayLabel),};},async updateContents_(){const previousRowStates=this.viewState.tableRowStates;if(!this.filteredHistograms_){await this.progress_('Filtering rows...');this.filteredHistograms_=this.viewState.showAll?this.histograms:this.sourceHistograms_;if(this.viewState.searchQuery){let query;try{query=new RegExp(this.viewState.searchQuery);}catch(e){}
+if(query!==undefined){this.filteredHistograms_=new tr.v.HistogramSet([...this.filteredHistograms_].filter(hist=>hist.name.match(query)));if(this.filteredHistograms_.length===0&&!this.viewState.showAll){await this.viewState.update({showAll:true});return;}}}
+this.groupedHistograms_=undefined;}
+if(!this.groupedHistograms_){await this.progress_('Grouping Histograms...');this.groupHistograms_();}
+if(!this.hierarchies_){await this.progress_('Merging Histograms...');this.hierarchies_=tr.v.HistogramSetHierarchy.build(this.groupedHistograms_);this.tableRows_=undefined;}
+const tableRowsDirty=this.tableRows_===undefined;if(tableRowsDirty){this.tableRows_=this.hierarchies_.map(hierarchy=>new tr.v.ui.HistogramSetTableRow(hierarchy,this.$.table,this.viewState));tr.b.Timing.instant('histogram-set-table','rootRowCount',this.tableRows_.length);const namesToRowStates=new Map();for(const row of this.tableRows_){namesToRowStates.set(row.name,row.viewState);}
 await this.viewState.update({tableRowStates:namesToRowStates});}
 await this.progress_('Configuring table...');this.nameColumnTitle_.children[1].style.filter=this.viewState.constrainNameColumn?'invert(100%)':'';const referenceDisplayLabelIndex=this.displayLabels_.indexOf(this.viewState.referenceDisplayLabel);this.$.table.selectedTableColumnIndex=(referenceDisplayLabelIndex<0)?undefined:(1+referenceDisplayLabelIndex);this.removeEventListener('sort-column-changed',this.sortColumnChangedListener_);this.$.table.sortColumnIndex=this.viewState.sortColumnIndex;this.$.table.sortDescending=this.viewState.sortDescending;this.addEventListener('sort-column-changed',this.sortColumnChangedListener_);if(tableRowsDirty){await this.progress_('Building DOM...');this.$.table.tableRows=this.tableRows_;for(const row of this.tableRows_){const previousState=previousRowStates.get(row.name);if(!previousState)continue;await row.restoreState(previousState);}}
 this.$.table.rebuild();},async onRowExpandedChanged_(event){event.row.viewState.isExpanded=this.$.table.getExpandedForTableRow(event.row);tr.b.Timing.instant('histogram-set-table','row'+(event.row.viewState.isExpanded?'Expanded':'Collapsed'));if(this.nameColumnTitle_.children[1].style.display==='block')return;await tr.b.animationFrame();this.checkNameColumnOverflow_(event.row.subRows);},checkNameColumnOverflow_(rows){for(const row of rows){if(!row.nameCell.isOverflowing)continue;const[nameSpan,dots]=this.nameColumnTitle_.children;dots.style.display='block';const labelWidthPx=tr.v.ui.NAME_COLUMN_WIDTH_PX-
 dots.getBoundingClientRect().width;nameSpan.style.width=labelWidthPx+'px';return;}},groupHistograms_(){const groupings=this.viewState.groupings.slice();groupings.push(tr.v.HistogramGrouping.DISPLAY_LABEL);function canSkipGrouping(grouping,groupedHistograms){if(groupedHistograms.size>1)return false;if(grouping.key===groupings[0].key)return false;if(grouping.key===tr.v.HistogramGrouping.DISPLAY_LABEL.key){return false;}
 return true;}
-this.groupedHistograms_=this.histograms.groupHistogramsRecursively(groupings,canSkipGrouping);this.hierarchies_=undefined;},async onViewStateUpdate_(event){if(this.histograms_===undefined)return;if(event.delta.groupings!==undefined){this.groupedHistograms_=undefined;}
-if(event.delta.searchQuery!==undefined||event.delta.showAll!==undefined){this.tableRows_=undefined;}
+this.groupedHistograms_=this.filteredHistograms_.groupHistogramsRecursively(groupings,canSkipGrouping);this.hierarchies_=undefined;},async onViewStateUpdate_(event){if(this.histograms_===undefined)return;if(event.delta.searchQuery!==undefined||event.delta.showAll!==undefined){this.filteredHistograms_=undefined;}
+if(event.delta.groupings!==undefined){this.groupedHistograms_=undefined;}
 if(event.delta.displayStatistic!==undefined&&this.$.table.sortColumnIndex>0){this.$.table.sortColumnIndex=undefined;}
 if(event.delta.referenceDisplayLabel!==undefined||event.delta.displayStatisticName!==undefined){this.$.table.tableRows=this.$.table.tableRows;}
 if(event.delta.tableRowStates){if(this.tableRows_.length!==this.viewState.tableRowStates.size){throw new Error('Only histogram-set-table may update tableRowStates');}
-for(const row of this.tableRows_){if(this.viewState.tableRowStates.get(row.name)!==row.viewState){throw new Error('Only histogram-set-table may update tableRowStates');}}}
+for(const row of this.tableRows_){if(this.viewState.tableRowStates.get(row.name)!==row.viewState){throw new Error('Only histogram-set-table may update tableRowStates');}}
+return;}
 await this.updateContents_();},onSortColumnChanged_(event){tr.b.Timing.instant('histogram-set-table','sortColumn');this.viewState.update({sortColumnIndex:event.sortColumnIndex,sortDescending:event.sortDescending,});},onRequestSelectionChange_(event){if(event.selection instanceof tr.model.EventSet)return;event.stopPropagation();tr.b.Timing.instant('histogram-set-table','selectHistogramNames');let histogramNames=event.selection;histogramNames.sort();histogramNames=histogramNames.map(escapeRegExp).join('|');this.viewState.update({showAll:true,searchQuery:`^(${histogramNames})$`,});},get leafHistograms(){const histograms=new tr.v.HistogramSet();for(const row of
 tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)){if(row.subRows.length)continue;for(const hist of row.columns.values()){if(!(hist instanceof tr.v.Histogram))continue;histograms.addHistogram(hist);}}
-return histograms;}});return{MIDLINE_HORIZONTAL_ELLIPSIS,};});'use strict';const COLORS=[['#FFD740','#FFC400','#FFAB00','#E29800'],['#FF6E40','#FF3D00','#DD2C00','#A32000'],['#40C4FF','#00B0FF','#0091EA','#006DAF'],['#89C641','#54B503','#4AA510','#377A0D'],['#B388FF','#7C4DFF','#651FFF','#6200EA'],['#FF80AB','#FF4081','#F50057','#C51162'],['#FFAB40','#FF9100','#FF6D00','#D65C02'],['#8C9EFF','#536DFE','#3D5AFE','#304FFE']];const METRICS=new Map([['Pipeline',['pipeline:begin_frame_transport','pipeline:begin_frame_to_frame_submission','pipeline:frame_submission_to_display','pipeline:draw']],['Thread',['thread_browser_cpu_time_per_frame','thread_display_compositor_cpu_time_per_frame','thread_GPU_cpu_time_per_frame','thread_IO_cpu_time_per_frame','thread_other_cpu_time_per_frame','thread_raster_cpu_time_per_frame','thread_renderer_compositor_cpu_time_per_frame','thread_renderer_main_cpu_time_per_frame']]]);const STATISTIC_KEY='statistic';Polymer({is:'tr-v-ui-metrics-visualization',created(){this.sortedPages_=new Map();this.displayLabels_=new Map();this.groups_=new Map();this.charts_=new Map();this.subMetricNames_=new Map();},build(leafHistograms,histograms){if(!leafHistograms||leafHistograms.length<1||!histograms||histograms.length<1){this.$.data_error.style.display='block';return;}
-this.histograms=histograms;for(const key of METRICS.keys()){const newAggregateChart=this.initializeColumnChart(key);Polymer.dom(this.$.container).appendChild(newAggregateChart);this.populateChart_(leafHistograms,key,newAggregateChart,true);newAggregateChart.toolTipCallBack=()=>this.openMetricsChart_(key);}
-if(this.groups_.size<1){this.$.data_error.style.display='block';return;}},populateChart_(histograms,metricName,chart,aggregate){const storiesGrouping=tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES);const benchmarkStartGrouping=tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START);const labels=new Map();const groups=new Map();for(const metric of METRICS.get(metricName)){const metricHistograms=histograms.getHistogramsNamed(metric);for(const histogram of metricHistograms){const benchmarkStart=benchmarkStartGrouping.callback(histogram);const page=storiesGrouping.callback(histogram);const displayLabel=tr.v.HistogramGrouping.DISPLAY_LABEL.callback(histogram);const average=histogram.average===undefined?0:histogram.average;labels.set(displayLabel,Date.parse(benchmarkStart));let pageMap=new Map();let labelMap=new Map();if(groups.get(page)===undefined){groups.set(page,pageMap);pageMap.set(displayLabel,labelMap);}else{pageMap=groups.get(page);if(pageMap.get(displayLabel)===undefined){pageMap.set(displayLabel,labelMap);}else{labelMap=pageMap.get(displayLabel);}}
-let metricMap=new Map();let statistic=new tr.b.math.RunningStatistics();if(labelMap.has(metric)){metricMap=labelMap.get(metric);statistic=metricMap.get(STATISTIC_KEY);}
-statistic.add(average);metricMap.set(STATISTIC_KEY,statistic);labelMap.set(metric,metricMap);const merged=new tr.v.d.DiagnosticMap();for(const bin of histogram.allBins){for(const map of bin.diagnosticMaps){merged.addDiagnostics(map);}}
-const subMetrics=[];if(merged.get('breakdown')===undefined){metricMap.set(metric,statistic);subMetrics.push(metric);}else{for(const[subMetric,total]of merged.get('breakdown')){let subStatistic=new tr.b.math.RunningStatistics();if(metricMap.has(subMetric)){subStatistic=metricMap.get(subMetric);}
-subStatistic.add(total);metricMap.set(subMetric,subStatistic);subMetrics.push(subMetric);}}
-const prevSubMetrics=this.subMetricNames_.get(metric);if(!prevSubMetrics||subMetrics.length>prevSubMetrics.length){this.subMetricNames_.set(metric,subMetrics);}}}
-const displayLabels=this.getSortedDisplayLabels_(labels);const sortedPages=[...groups.keys()].sort((a,b)=>this.sortGroups_(a,b,groups,displayLabels,METRICS.get(metricName)));this.setChartColors_(METRICS.get(metricName),displayLabels,chart);this.setChartSize_(groups.size,displayLabels.length,chart);this.setChartData_(groups,sortedPages,displayLabels,chart);let mapName=metricName;if(aggregate){mapName='Aggregate '+mapName;}
-this.groups_.set(mapName,groups);this.displayLabels_.set(mapName,displayLabels);this.sortedPages_.set(mapName,sortedPages);},getSortedDisplayLabels_(labels){return Array.from(labels.keys()).sort((a,b)=>labels.get(a)-labels.get(b));},sortGroups_(a,b,groups,displayLabels,mainMetricNames){let aValue=0;const aMap=groups.get(a);if(aMap.get(displayLabels[0])!==undefined){for(const metricName of mainMetricNames){const aMetricMap=aMap.get(displayLabels[0]).get(metricName);aValue+=aMetricMap.get(STATISTIC_KEY).mean;}}
-let bValue=0;const bMap=groups.get(b);if(bMap.get(displayLabels[0])!==undefined){for(const metricName of mainMetricNames){const bMetricMap=bMap.get(displayLabels[0]).get(metricName);bValue+=bMetricMap.get(STATISTIC_KEY).mean;}}
-if(aValue===bValue)return 0;return(aValue<bValue)?-1:1;},setChartData_(groups,sortedPages,displayLabels,chart){const chartData=[];for(const page of sortedPages){const pageMap=groups.get(page);for(const label of displayLabels){const data={x:label,group:page};if(!pageMap.has(label))continue;const labelMap=pageMap.get(label);for(const[metric,metricMap]of labelMap){const key=this.getSeriesKey_(metric,label);const mean=metricMap.get(STATISTIC_KEY).mean;data[key]=Math.round(mean*100)/100;if(label===displayLabels[0]){chart.getDataSeries(key).title=metric;}else{chart.getDataSeries(key).title='';}}
-chartData.push(data);}
+return histograms;}});return{MIDLINE_HORIZONTAL_ELLIPSIS,};});'use strict';tr.exportTo('tr.v.ui',function(){const PAGE_BREAKDOWN_KEY='pageBreakdown';Polymer({is:'tr-v-ui-metrics-visualization',created(){this.charts_=new Map();},ready(){this.$.start.addEventListener('keydown',(e)=>{if(e.key==='Enter')this.filterByPercentile_();});this.$.end.addEventListener('keydown',(e)=>{if(e.key==='Enter')this.filterByPercentile_();});this.$.search_page.addEventListener('keydown',(e)=>{if(e.key==='Enter')this.searchByPage_();});},build(chartData){this.title_=chartData.title;this.aggregateData_=chartData.aggregate;this.data_=chartData.page;this.submetricsData_=chartData.submetrics;this.benchmarkCount_=chartData.aggregate.length;const aggregateChart=this.initializeColumnChart(this.title_);Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart);this.charts_.set(tr.v.ui.AGGREGATE_KEY,aggregateChart);this.setChartColors_(tr.v.ui.AGGREGATE_KEY);aggregateChart.data=chartData.aggregate;this.setChartSize_(tr.v.ui.AGGREGATE_KEY);const newChart=this.initializeColumnChart(this.title_+' Breakdown');newChart.enableToolTip=true;newChart.toolTipCallBack=(rect)=>this.openChildChart_(rect);Polymer.dom(this.$.pageByPageContainer).appendChild(newChart);this.charts_.set(PAGE_BREAKDOWN_KEY,newChart);this.setChartColors_(PAGE_BREAKDOWN_KEY);newChart.data=this.data_;this.setChartSize_(PAGE_BREAKDOWN_KEY);},setChartSize_(page){const chart=this.charts_.get(page);const pageCount=chart.data.length;chart.graphHeight=tr.b.math.clamp(pageCount*20,400,600);chart.graphWidth=tr.b.math.clamp(pageCount*30,200,1000);},setChartColors_(page){const chart=this.charts_.get(page);const metrics=tr.v.ui.METRICS.get(this.title_);for(let i=0;i<this.benchmarkCount_;++i){for(let j=0;j<metrics.length;++j){const mainColorIndex=j%tr.v.ui.COLORS.length;const subColorIndex=i%tr.v.ui.COLORS[mainColorIndex].length;const color=tr.v.ui.COLORS[mainColorIndex][subColorIndex];const series=metrics[j]+'-'+this.aggregateData_[i].x;chart.getDataSeries(series).color=color;if(i===0){chart.getDataSeries(series).title=metrics[j];}else{chart.getDataSeries(series).title='';}}}},initializeColumnChart(title){const newChart=new tr.ui.b.NameColumnChart();newChart.hideLegend=false;newChart.isStacked=true;newChart.yAxisLabel='ms';newChart.hideXAxis=true;newChart.displayXInHover=true;newChart.isGrouped=true;newChart.showTitleInLegend=true;newChart.chartTitle=title;newChart.titleHeight='14pt';return newChart;},initializeChildChart_(title,height,width){const div=document.createElement('div');div.classList.add('container');Polymer.dom(this.$.submetricsContainer).insertBefore(div,this.$.submetricsContainer.firstChild);const childChart=new tr.ui.b.NameBarChart();childChart.xAxisLabel='ms';childChart.chartTitle=title;childChart.graphHeight=height;childChart.graphWidth=width;childChart.titleHeight='14pt';childChart.isStacked=true;childChart.hideLegend=true;childChart.isGrouped=true;childChart.isWaterfall=true;div.appendChild(childChart);const button=this.initializeCloseButton_(div,this.$.submetricsContainer);div.appendChild(button);return childChart;},initializeCloseButton_(div,parent){const button=this.$.close.cloneNode(true);button.style.display='inline-block';button.addEventListener('click',()=>{Polymer.dom(parent).removeChild(div);});return button;},openChildChart_(rect){const metrics=tr.v.ui.METRICS.get(this.title_);let metric;let metricIndex;for(let i=0;i<metrics.length;++i){if(rect.key.startsWith(metrics[i])){metric=metrics[i];metricIndex=i;break;}}
+const page=rect.datum.group;const title=this.title_+' '+metric+': '+page;const submetrics=this.submetricsData_.get(page).get(metric);const width=tr.b.math.clamp(submetrics.size*150,300,700);const height=tr.b.math.clamp(submetrics.size*this.benchmarkCount_*50,300,700);const childChart=this.initializeChildChart_(title,height,width);childChart.data=this.processSubmetrics_(childChart,submetrics,0,metricIndex).data;},processSubmetrics_(chart,submetrics,hideValue,metricIndex){const finalData=[];let submetricIndex=0;for(const submetric of submetrics.values()){let benchmarkIndex=0;for(const benchmark of submetric.values()){benchmark.hide=!hideValue?0:hideValue;const series=benchmark.x+'-'+benchmark.group;const mainColorIndex=metricIndex%tr.v.ui.COLORS.length;const subColorIndex=benchmarkIndex%tr.v.ui.COLORS[mainColorIndex].length;chart.getDataSeries(series).color=tr.v.ui.COLORS[mainColorIndex][subColorIndex];if(benchmarkIndex===(this.benchmarkCount_-1)){hideValue+=benchmark[series];}
+finalData.push(benchmark);benchmarkIndex++;}
+submetricIndex++;}
+return{data:finalData,hide:hideValue};},filterByPercentile_(){const startPercentile=this.$.start.value;const endPercentile=this.$.end.value;if(startPercentile===''||endPercentile==='')return;const length=this.data_.length/(this.benchmarkCount_+1);const startIndex=this.getPercentileIndex_(startPercentile,length);const endIndex=this.getPercentileIndex_(endPercentile,length);this.charts_.get(PAGE_BREAKDOWN_KEY).data=this.data_.slice(startIndex,endIndex);},getPercentileIndex_(percentile,arrayLength){const index=Math.ceil(arrayLength*(percentile/100.0));if(index===-1)return 0;if(index>=arrayLength)return arrayLength;return index*this.benchmarkCount_;},searchByPage_(){const criteria=this.$.search_page.value;if(criteria==='')return;const query=new RegExp(criteria);const filteredData=[...this.data_].filter(group=>{if(group.group)return group.group.match(query);return false;});if(filteredData.length<1){this.$.search_error.style.display='block';return;}
+const page=filteredData[0].group;const title=this.title_+' Breakdown: '+page;const metricToSubmetricMap=this.submetricsData_.get(page);let totalSubmetrics=0;for(const submetrics of metricToSubmetricMap.values()){for(const benchmark of submetrics.values()){totalSubmetrics+=benchmark.length;}}
+const width=tr.b.math.clamp(totalSubmetrics*150,300,700);const height=tr.b.math.clamp(totalSubmetrics*this.benchmarkCount_*30,300,700);const childChart=this.initializeChildChart_(title,height,width);const childData=[];let hide=0;let metricIndex=0;for(const submetrics of metricToSubmetricMap.values()){const submetricsData=this.processSubmetrics_(childChart,submetrics,hide,metricIndex);childData.push(...submetricsData.data);hide=submetricsData.hide;metricIndex++;}
+childChart.data=childData;},});});'use strict';Polymer({is:'tr-v-ui-raster-visualization',ready(){this.$.pageSelector.addEventListener('click',()=>{this.selectPage_();});this.$.search_page.addEventListener('keydown',(e)=>{if(e.key==='Enter')this.searchByPage_();});this.$.search_button.addEventListener('click',()=>{this.searchByPage_();});},build(chartData){this.data_=chartData;const aggregateChart=this.createChart_('Aggregate Data by Run');Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart);aggregateChart.enableToolTip=true;aggregateChart.toolTipCallBack=(rect)=>this.openBenchmarkChart_(rect);this.setChartColors_(aggregateChart,this.data_.get(tr.v.ui.AGGREGATE_KEY));aggregateChart.data=this.data_.get(tr.v.ui.AGGREGATE_KEY);this.setChartSize_(aggregateChart,this.data_.get(tr.v.ui.AGGREGATE_KEY).length);for(const page of this.data_.keys()){if(page===tr.v.ui.AGGREGATE_KEY)continue;const option=document.createElement('option');option.textContent=page;option.value=page;this.$.pageSelector.appendChild(option);}},setChartSize_(chart,pageCount,dataLength){chart.graphHeight=tr.b.math.clamp(pageCount*25,175,1000);chart.graphWidth=tr.b.math.clamp(pageCount*25,500,1000);},setChartColors_(chart,data){const metrics=new Map();let count=0;for(const thread of tr.v.ui.FRAME.values()){for(const metric of thread.keys()){metrics.set(metric,count);count++;}}
+for(let i=0;i<Math.floor(data.length/tr.v.ui.FRAME.length);++i){let j=0;for(const[threadName,thread]of tr.v.ui.FRAME.entries()){for(const metric of thread.keys()){let color='transparent';if(thread.get(metric)){const mainColorIndex=metrics.get(metric)%tr.v.ui.COLORS.length;const subColorIndex=i%tr.v.ui.COLORS[mainColorIndex].length;color=tr.v.ui.COLORS[mainColorIndex][subColorIndex];}
+const series=metric+'-'+data[i*2+j].x+'-'+threadName;chart.getDataSeries(series).color=color;chart.getDataSeries(series).title=!i?metric:'';}
+j++;}}},createChart_(title){const newChart=new tr.ui.b.NameBarChart();newChart.chartTitle=title;newChart.xAxisLabel='ms';newChart.hideLegend=false;newChart.showTitleInLegend=true;newChart.hideYAxis=true;newChart.isStacked=true;newChart.displayXInHover=true;newChart.isGrouped=true;return newChart;},openBenchmarkChart_(rect){const benchmarkIndex=Math.floor(rect.index/tr.v.ui.FRAME.length);const title=rect.datum.x;const div=document.createElement('div');Polymer.dom(this.$.pageContainer).insertBefore(div,this.$.pageContainer.firstChild);const chart=this.createChart_(title);div.appendChild(chart);const button=this.initializeCloseButton_(div,this.$.pageContainer);div.appendChild(button);const newDataSet=[];for(const page of this.data_.keys()){if(page===tr.v.ui.AGGREGATE_KEY)continue;for(let i=0;i<tr.v.ui.FRAME.length;i++){newDataSet.push(this.data_.get(page)[benchmarkIndex*tr.v.ui.FRAME.length+i]);}}
+this.setChartColors_(chart,newDataSet);chart.data=newDataSet;this.setChartSize_(chart,newDataSet.length);},selectPage_(){const div=document.createElement('div');const page=this.$.pageSelector.value;if(page==='')return;Polymer.dom(this.$.pageContainer).insertBefore(div,this.$.pageContainer.firstChild);const pageChart=this.createChart_(page);div.appendChild(pageChart);const button=this.initializeCloseButton_(div,this.$.pageContainer);div.appendChild(button);const pageData=this.data_.get(page);this.setChartColors_(pageChart,pageData);pageChart.data=pageData;this.setChartSize_(pageChart,pageData.length);},searchByPage_(){const criteria=this.$.search_page.value;if(criteria==='')return;const query=new RegExp(criteria);const filteredData=[...this.data_.keys()].filter(page=>page.match(query));if(filteredData.length<1){this.$.search_error.style.display='block';return;}
+const page=filteredData[0];const div=document.createElement('div');Polymer.dom(this.$.pageContainer).insertBefore(div,this.$.pageContainer.firstChild);const pageChart=this.createChart_(page);div.appendChild(pageChart);const button=this.initializeCloseButton_(div,this.$.pageContainer);div.appendChild(button);const pageData=this.data_.get(page);this.setChartColors_(pageChart,pageData);pageChart.data=pageData;this.setChartSize_(pageChart,pageData.length);},initializeCloseButton_(div,parent){const button=this.$.close.cloneNode(true);button.style.display='inline-block';button.addEventListener('click',()=>{Polymer.dom(parent).removeChild(div);});return button;},});'use strict';tr.exportTo('tr.v.ui',function(){const STATISTICS_KEY='statistics';const SUBMETRICS_KEY='submetrics';const AGGREGATE_KEY='aggregate';const RASTER_START_METRIC_KEY='pipeline:begin_frame_to_raster_start';const COLORS=[['#FFD740','#FFC400','#FFAB00','#E29800'],['#FF6E40','#FF3D00','#DD2C00','#A32000'],['#40C4FF','#00B0FF','#0091EA','#006DAF'],['#89C641','#54B503','#4AA510','#377A0D'],['#B388FF','#7C4DFF','#651FFF','#6200EA'],['#FF80AB','#FF4081','#F50057','#C51162'],['#FFAB40','#FF9100','#FF6D00','#D65C02'],['#8C9EFF','#536DFE','#3D5AFE','#304FFE']];const FRAME=[new Map([['pipeline:begin_frame_to_raster_start',false],['pipeline:begin_frame_to_raster_end',true]]),new Map([['pipeline:begin_frame_transport',true],['pipeline:begin_frame_to_frame_submission',true],['pipeline:frame_submission_to_display',true],['pipeline:draw',true]])];const METRICS=new Map([['Pipeline',['pipeline:begin_frame_transport','pipeline:begin_frame_to_frame_submission','pipeline:frame_submission_to_display','pipeline:draw']],['Thread',['thread_browser_cpu_time_per_frame','thread_display_compositor_cpu_time_per_frame','thread_GPU_cpu_time_per_frame','thread_IO_cpu_time_per_frame','thread_other_cpu_time_per_frame','thread_raster_cpu_time_per_frame','thread_renderer_compositor_cpu_time_per_frame','thread_renderer_main_cpu_time_per_frame']]]);function getValueFromMap(key,map){let retrievedValue=map.get(key);if(!retrievedValue){retrievedValue=new Map();map.set(key,retrievedValue);}
+return retrievedValue;}
+Polymer({is:'tr-v-ui-visualizations-data-container',created(){this.orderedBenchmarks_=[];this.groupedData_=new Map();},build(leafHistograms,histograms){if(!leafHistograms||leafHistograms.length<1||!histograms||histograms.length<1){this.$.data_error.style.display='block';return;}
+this.processHistograms_(this.groupHistograms_(histograms),this.groupHistograms_(leafHistograms));this.buildCharts_();},processHistograms_(histograms,leafHistograms){const benchmarkStartGrouping=tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START);const benchmarkToStartTime=new Map();for(const[metric,benchmarks]of histograms.entries()){for(const[benchmark,pages]of leafHistograms.get(metric).entries()){for(const[page,histograms]of pages.entries()){for(const histogram of histograms){const aggregateToBenchmarkMap=getValueFromMap(AGGREGATE_KEY,this.groupedData_);const benchmarkToMetricMap=getValueFromMap(benchmark,aggregateToBenchmarkMap);benchmarkToMetricMap.set(metric,new Map([[STATISTICS_KEY,histogram.running]]));}}}
+for(const[benchmark,pages]of benchmarks.entries()){for(const[page,histograms]of pages.entries()){for(const histogram of histograms){if(!benchmarkToStartTime.get(benchmark)){benchmarkToStartTime.set(benchmark,benchmarkStartGrouping.callback(histogram));}
+const pageToBenchmarkMap=getValueFromMap(page,this.groupedData_);const benchmarkToMetricMap=getValueFromMap(benchmark,pageToBenchmarkMap);const mergedSubmetrics=new tr.v.d.DiagnosticMap();for(const bin of histogram.allBins){for(const map of bin.diagnosticMaps){mergedSubmetrics.addDiagnostics(map);}}
+if(benchmarkToMetricMap.get(metric))continue;benchmarkToMetricMap.set(metric,new Map([[STATISTICS_KEY,histogram.running],[SUBMETRICS_KEY,mergedSubmetrics.get('breakdown')]]));}}}}
+this.orderedBenchmarks_=this.sortBenchmarks_(benchmarkToStartTime);},groupHistograms_(histograms){const groupings=[tr.v.HistogramGrouping.HISTOGRAM_NAME,tr.v.HistogramGrouping.DISPLAY_LABEL,tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)];return histograms.groupHistogramsRecursively(groupings);},sortBenchmarks_(benchmarks){return Array.from(benchmarks.keys()).sort((a,b)=>{Date.parse(benchmarks.get(a))-Date.parse(benchmarks.get(b));});},getSeriesKey_(metric,benchmark){return metric+'-'+benchmark;},buildCharts_(){const rasterDataToBePassed=this.buildRasterChart_();this.$.rasterVisualization.build(rasterDataToBePassed);for(const chartName of METRICS.keys()){const metricsDataToBePassed=this.buildMetricsData_(chartName);const newChart=this.$.metricsVisualization.cloneNode(true);newChart.style.display='block';Polymer.dom(this.$.metrics_container).appendChild(newChart);newChart.build(metricsDataToBePassed);}},buildRasterChart_(){const orderedPages=[...this.groupedData_.keys()].filter((page)=>this.filterPagesWithoutRasterMetric_(page)).sort((a,b)=>this.sortByRasterStart_(a,b));const allChartData=new Map();for(const page of orderedPages){const pageMap=this.groupedData_.get(page);let chartData=[];for(const benchmark of this.orderedBenchmarks_){if(!pageMap.has(benchmark))continue;const benchmarkMap=pageMap.get(benchmark);const benchmarkData=[];if(benchmarkMap.get(RASTER_START_METRIC_KEY)===undefined){continue;}
+for(const[threadName,thread]of FRAME.entries()){const data={x:benchmark,hide:0};if(page!==AGGREGATE_KEY)data.group=page;let rasterBegin=0;for(const metric of thread.keys()){const metricMap=benchmarkMap.get(metric);const key=this.getSeriesKey_(metric,data.x+'-'+threadName);const stats=metricMap.get(STATISTICS_KEY);const mean=stats?stats.mean:0;let roundedMean=Math.round(mean*100)/100;if(metric===RASTER_START_METRIC_KEY){rasterBegin=roundedMean;}else if(metric==='pipeline:begin_frame_to_raster_end'){roundedMean-=rasterBegin;}
+data[key]=roundedMean;}
+benchmarkData.push(data);}
+chartData=chartData.concat(benchmarkData);}
+allChartData.set(page,chartData);}
+return allChartData;},buildMetricsData_(chartName){const orderedPages=[...this.groupedData_.keys()].sort((a,b)=>this.sortByTotal_(a,b,chartName));const chartData=[];const aggregateChart=[];for(const page of orderedPages){const pageMap=this.groupedData_.get(page);for(const benchmark of this.orderedBenchmarks_){if(!pageMap.has(benchmark))continue;const data={x:benchmark,group:page};const benchmarkMap=pageMap.get(benchmark);for(const metric of METRICS.get(chartName)){const metricMap=benchmarkMap.get(metric);const key=this.getSeriesKey_(metric,benchmark);const stats=metricMap.get(STATISTICS_KEY);const mean=stats?stats.mean:0;data[key]=Math.round(mean*100)/100;}
+if(page===AGGREGATE_KEY){aggregateChart.push(data);}else{chartData.push(data);}}
 chartData.push({});}
-chart.data=chartData;},setChartSize_(pageCount,displayLabelCount,chart){chart.graphHeight=tr.b.math.clamp(pageCount*displayLabelCount*20,300,600);chart.graphWidth=tr.b.math.clamp(pageCount*displayLabelCount*25,500,1000);},setChartColors_(mainMetricNames,displayLabels,chart){for(let i=0;i<mainMetricNames.length;++i){const mainMetric=mainMetricNames[i];for(let j=0;j<displayLabels.length;++j){const mainColorIndex=i%COLORS.length;const subColorIndex=j%COLORS[mainColorIndex].length;const color=COLORS[mainColorIndex][subColorIndex];const displayLabel=displayLabels[j];const series=this.getSeriesKey_(mainMetric,displayLabel);chart.getDataSeries(series).color=color;}}},getSeriesKey_(metricName,displayLabel){return metricName+'-'+displayLabel;},initializeColumnChart(title){const newChart=new tr.ui.b.NameColumnChart();newChart.isStacked=true;newChart.yAxisLabel='ms';newChart.hideXAxis=true;newChart.displayXInHover=true;newChart.isGrouped=true;newChart.enableToolTip=true;newChart.showTitleInLegend=true;newChart.chartTitle=title;newChart.titleHeight='14pt';return newChart;},initializeChildChart_(title,height,width){const childChart=new tr.ui.b.NameBarChart();childChart.xAxisLabel='ms';childChart.chartTitle=title;childChart.graphHeight=height;childChart.graphWidth=width;childChart.titleHeight='14pt';childChart.isStacked=true;childChart.hideLegend=true;childChart.isGrouped=true;childChart.isWaterfall=true;return childChart;},initializeCloseButton_(div,parent){const button=this.$.close.cloneNode(true);button.style.display='inline-block';button.addEventListener('click',()=>{Polymer.dom(parent).removeChild(div);});return button;},initializeSelectors_(mainStep){const select=this.$.selectors.cloneNode(true);select.style.display='block';Polymer.dom(select).querySelector('#start').addEventListener('keydown',(e)=>{if(e.key==='Enter')this.filterByPercentile_(select,mainStep);});Polymer.dom(select).querySelector('#end').addEventListener('keydown',(e)=>{if(e.key==='Enter')this.filterByPercentile_(select,mainStep);});Polymer.dom(select).querySelector('#filter').addEventListener('click',()=>{this.filterByPercentile_(select,mainStep);});Polymer.dom(select).querySelector('#search_page').addEventListener('keydown',(e)=>{if(e.key==='Enter')this.searchByPage_(select,mainStep);});Polymer.dom(select).querySelector('#search').addEventListener('click',()=>{this.searchByPage_(select,mainStep);});return select;},openMetricsChart_(mainStep){const div=document.createElement('div');div.classList.add('child_container');Polymer.dom(this.$.container).appendChild(div);const selectors=this.initializeSelectors_(mainStep);div.appendChild(selectors);const childChart=this.initializeColumnChart(mainStep+' Breakdown');childChart.toolTipCallBack=(rect)=>this.openChildChart_(rect,mainStep);div.appendChild(childChart);const button=this.initializeCloseButton_(div,this.$.container);div.appendChild(button);this.populateChart_(this.histograms,mainStep,childChart,false);this.charts_.set(mainStep,childChart);},initializeChild_(title,height,width){const div=document.createElement('div');div.classList.add('child_container');Polymer.dom(this.$.children).insertBefore(div,this.$.children.firstChild);const childChart=this.initializeChildChart_(title,height,width);div.appendChild(childChart);const button=this.initializeCloseButton_(div,this.$.children);div.appendChild(button);return childChart;},openChildChart_(rect,metricName){let mainStep;let mainStepIndex;for(let i=0;i<METRICS.get(metricName).length;++i){if(rect.key.startsWith(METRICS.get(metricName)[i])){mainStep=METRICS.get(metricName)[i];mainStepIndex=i;break;}}
-const subSteps=this.subMetricNames_.get(mainStep);const width=tr.b.math.clamp(subSteps.length*150,300,700);const height=tr.b.math.clamp(subSteps.length*this.displayLabels_.get(metricName).length*50,300,700);const pageName=rect.datum.group;const title=mainStep+': '+pageName;const childChart=this.initializeChild_(title,height,width);const pageData=this.groups_.get(metricName).get(pageName);const subStepData=this.processSubStepData(childChart,mainStepIndex,mainStep,pageData,pageName,0);childChart.data=subStepData.childData;},processSubStepData(childChart,mainStepIndex,mainStep,pageData,pageName,total){const childData=[];const subSteps=this.subMetricNames_.get(mainStep);if(subSteps===undefined)return{childData,total};for(const subStep of subSteps){const currentTotal=total;let j=0;for(const[displayLabel,labelMap]of pageData){const data={x:subStep,hide:currentTotal,group:displayLabel};const metricMap=labelMap.get(mainStep);const series=this.getSeriesKey_(subStep,displayLabel);const mean=metricMap.get(subStep).mean;const roundedMean=Math.round(mean*100)/100;data[series]=roundedMean===undefined?0:roundedMean;childData.push(data);if(j===0)total+=data[series];else data.x='.';const subColorIndex=j%COLORS[mainStepIndex].length;const color=COLORS[mainStepIndex][subColorIndex];childChart.getDataSeries(series).color=color;j++;}
-childData.push({x:'.'});}
-return{childData,total};},filterByPercentile_(select,metricName){const startPercentile=Polymer.dom(select).querySelector('#start').value;const endPercentile=Polymer.dom(select).querySelector('#end').value;if(startPercentile===''||endPercentile==='')return;const length=this.sortedPages_.get(metricName).length;const startIndex=this.getPercentileIndex_(startPercentile,length);const endIndex=this.getPercentileIndex_(endPercentile,length);const slicedPages=this.sortedPages_.get(metricName).slice(startIndex,endIndex);this.setChartData_(this.groups_.get(metricName),slicedPages,this.displayLabels_.get(metricName),this.charts_.get(metricName));},getPercentileIndex_(percentile,arrayLength){const index=Math.ceil(arrayLength*(percentile/100.0));if(index===-1)return 0;if(index>=arrayLength)return arrayLength;return index;},searchByPage_(select,metricName){const criteria=Polymer.dom(select).querySelector('#search_page').value;if(criteria==='')return;const query=new RegExp(criteria);const filteredGroups=[...this.groups_.get(metricName)].filter(group=>group[0].match(query));if(filteredGroups.length<1){Polymer.dom(select).querySelector('#search_error').style.display='block';return;}
-Polymer.dom(select).querySelector('#search_error').style.display='none';const matchedPageName=filteredGroups[0][0];const match=this.groups_.get(metricName).get(matchedPageName);const title=metricName+' Breakdown: '+matchedPageName;let totalSteps=0;for(const[mainStep,subSteps]of this.subMetricNames_){totalSteps+=subSteps.length;}
-const width=tr.b.math.clamp(totalSteps*150,300,700);const height=tr.b.math.clamp(totalSteps*this.displayLabels_.get(metricName).length*30,300,700);const childChart=this.initializeChild_(title,height,width);const childData=[];let total=0;for(let i=0;i<METRICS.get(metricName).length;++i){const stepData=this.processSubStepData(childChart,i,METRICS.get(metricName)[i],match,matchedPageName,total);childData.push(...stepData.childData);total=stepData.total;}
-childChart.data=childData;},});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-histogram-set-view',listeners:{export:'onExport_',loadVisualization:'onLoadVisualization_'},created(){this.brushingStateController_=new tr.ui.NullBrushingStateController();this.viewState_=new tr.v.ui.HistogramSetViewState();this.metricsVisualizationLoaded_=false;},ready(){this.$.table.viewState=this.viewState;this.$.controls.viewState=this.viewState;},attached(){this.brushingStateController.parentController=tr.c.BrushingStateController.getControllerForElement(this.parentNode);},get brushingStateController(){return this.brushingStateController_;},get viewState(){return this.viewState_;},get histograms(){return this.$.table.histograms;},async build(histograms,opt_options){const options=opt_options||{};const progress=options.progress||(()=>Promise.resolve());if(options.helpHref)this.$.controls.helpHref=options.helpHref;if(options.feedbackHref){this.$.controls.feedbackHref=options.feedbackHref;}
+chartData.shift();return{title:chartName,aggregate:aggregateChart,page:chartData,submetrics:this.processSubmetricsData_(chartName)};},submetricsHelper_(submetric,value,benchmark,metricToSubmetricMap){let submetricToBenchmarkMap=metricToSubmetricMap.get(submetric);if(!submetricToBenchmarkMap){submetricToBenchmarkMap=[];metricToSubmetricMap.set(submetric,submetricToBenchmarkMap);}
+const data={x:submetric,hide:0,group:benchmark};const mean=value;const roundedMean=Math.round(mean*100)/100;if(!roundedMean)return;data[this.getSeriesKey_(submetric,benchmark)]=roundedMean;submetricToBenchmarkMap.push(data);},processSubmetricsData_(chartName){const submetrics=new Map();for(const[page,pageMap]of this.groupedData_.entries()){if(page===AGGREGATE_KEY)continue;const pageToMetricMap=getValueFromMap(page,submetrics);for(const benchmark of this.orderedBenchmarks_){const benchmarkMap=pageMap.get(benchmark);if(!benchmarkMap)continue;for(const metric of METRICS.get(chartName)){const metricMap=benchmarkMap.get(metric);const metricToSubmetricMap=getValueFromMap(metric,pageToMetricMap);const submetrics=metricMap.get(SUBMETRICS_KEY);if(!submetrics){this.submetricsHelper_(metric,metricMap.get(STATISTICS_KEY),benchmark,metricToSubmetricMap);continue;}
+for(const[submetric,value]of[...submetrics]){this.submetricsHelper_(submetric,value,benchmark,metricToSubmetricMap);}}}}
+return submetrics;},sortByTotal_(a,b,chartName){if(a===AGGREGATE_KEY)return-1;if(b===AGGREGATE_KEY)return 1;let aValue=0;const aMap=this.groupedData_.get(a);if(aMap.get(this.orderedBenchmarks_[0])!==undefined){for(const metric of METRICS.get(chartName)){const aMetricMap=aMap.get(this.orderedBenchmarks_[0]).get(metric);const aStats=aMetricMap.get(STATISTICS_KEY);aValue+=aStats?aStats.mean:0;}}
+let bValue=0;const bMap=this.groupedData_.get(b);if(bMap.get(this.orderedBenchmarks_[0])!==undefined){for(const metric of METRICS.get(chartName)){const bMetricMap=bMap.get(this.orderedBenchmarks_[0]).get(metric);const bStats=bMetricMap.get(STATISTICS_KEY);bValue+=bStats?bStats.mean:0;}}
+return aValue-bValue;},filterPagesWithoutRasterMetric_(page){const pageMap=this.groupedData_.get(page);for(const benchmark of this.orderedBenchmarks_){const pageMetricMap=pageMap.get(benchmark);if(!pageMetricMap)continue;const wantedMetric=pageMetricMap.get(RASTER_START_METRIC_KEY);if(wantedMetric!==undefined)return true;}
+return false;},sortByRasterStart_(a,b){if(a===AGGREGATE_KEY)return 1;if(b===AGGREGATE_KEY)return-1;let aValue=0;const aMap=this.groupedData_.get(a);if(aMap.get(this.orderedBenchmarks_[0])!==undefined){const aMetricMap=aMap.get(this.orderedBenchmarks_[0]).get(RASTER_START_METRIC_KEY);const aStats=aMetricMap.get(STATISTICS_KEY);aValue=aStats?aStats.mean:0;}
+let bValue=0;const bMap=this.groupedData_.get(b);if(bMap.get(this.orderedBenchmarks_[0])!==undefined){const bMetricMap=bMap.get(this.orderedBenchmarks_[0]).get(RASTER_START_METRIC_KEY);const bStats=bMetricMap.get(STATISTICS_KEY);bValue=bStats?bStats.mean:0;}
+return bValue-aValue;},});return{STATISTICS_KEY,SUBMETRICS_KEY,AGGREGATE_KEY,COLORS,FRAME,METRICS,getValueFromMap,};});'use strict';tr.exportTo('tr.v.ui',function(){Polymer({is:'tr-v-ui-histogram-set-view',listeners:{export:'onExport_',loadVisualization:'onLoadVisualization_'},created(){this.brushingStateController_=new tr.ui.NullBrushingStateController();this.viewState_=new tr.v.ui.HistogramSetViewState();this.visualizationLoaded_=false;},ready(){this.$.table.viewState=this.viewState;this.$.controls.viewState=this.viewState;},attached(){this.brushingStateController.parentController=tr.c.BrushingStateController.getControllerForElement(this.parentNode);},get brushingStateController(){return this.brushingStateController_;},get viewState(){return this.viewState_;},get histograms(){return this.$.table.histograms;},async build(histograms,opt_options){const options=opt_options||{};const progress=options.progress||(()=>Promise.resolve());if(options.helpHref)this.$.controls.helpHref=options.helpHref;if(options.feedbackHref){this.$.controls.feedbackHref=options.feedbackHref;}
 if(histograms===undefined||histograms.length===0){this.$.container.style.display='none';this.$.zero.style.display='block';this.style.display='block';return;}
 this.$.zero.style.display='none';this.$.container.style.display='block';this.$.container.style.maxHeight=(window.innerHeight-16)+'px';const buildMark=tr.b.Timing.mark('histogram-set-view','build');await progress('Finding important Histograms...');const sourceHistogramsMark=tr.b.Timing.mark('histogram-set-view','sourceHistograms');const sourceHistograms=histograms.sourceHistograms;sourceHistogramsMark.end();this.$.controls.showAllEnabled=(sourceHistograms.length!==histograms.length);await progress('Collecting parameters...');const collectParametersMark=tr.b.Timing.mark('histogram-set-view','collectParameters');const parameterCollector=new tr.v.HistogramParameterCollector();parameterCollector.process(histograms);this.$.controls.baseStatisticNames=parameterCollector.statisticNames;this.$.controls.possibleGroupings=parameterCollector.possibleGroupings;const displayLabels=parameterCollector.labels;this.$.controls.displayLabels=displayLabels;collectParametersMark.end();const hist=[...histograms][0];const benchmarks=hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS);let enable=false;if(benchmarks!==undefined&&benchmarks.length>0){for(const benchmark of benchmarks){if(benchmark.includes('rendering')){enable=true;break;}}}
 this.$.controls.enableVisualization=enable;await this.$.table.build(histograms,sourceHistograms,displayLabels,progress);buildMark.end();},onExport_(event){const mark=tr.b.Timing.mark('histogram-set-view','export'+
 (event.merged?'Merged':'Raw')+event.format.toUpperCase());const histograms=event.merged?this.$.table.leafHistograms:this.histograms;let blob;if(event.format==='csv'){const csv=new tr.v.CSVBuilder(histograms);csv.build();blob=new window.Blob([csv.toString()],{type:'text/csv'});}else if(event.format==='json'){blob=new window.Blob([JSON.stringify(histograms.asDicts())],{type:'text/json'});}else{throw new Error(`Unable to export format "${event.format}"`);}
-const path=window.location.pathname.split('/');const basename=path[path.length-1].split('.')[0]||'histograms';const anchor=document.createElement('a');anchor.download=`${basename}.${event.format}`;anchor.href=window.URL.createObjectURL(blob);anchor.click();mark.end();},onLoadVisualization_(event){if(!this.metricsVisualizationLoaded_){this.$.metrics.build(this.$.table.leafHistograms,this.histograms);this.metricsVisualizationLoaded_=true;}else if(this.$.metrics.style.display==='none'){this.$.metrics.style.display='block';}else{this.$.metrics.style.display='none';}},});return{};});'use strict';tr.exportTo('tr.ui',function(){Polymer({is:'tr-ui-sp-metrics-side-panel',behaviors:[tr.ui.behaviors.SidePanel],ready(){this.model_=undefined;this.rangeOfInterest_=undefined;this.metricLatenciesMs_=[];this.metrics_=[];tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach(function(m){if(m.constructor.name==='sampleMetric')return;this.metrics_.push({label:m.constructor.name,value:m.constructor.name});},this);this.metrics_.sort((x,y)=>x.label.localeCompare(y.label));this.settingsKey_='metrics-side-panel-metric-name';this.currentMetricName_='responsivenessMetric';const metricSelector=tr.ui.b.createSelector(this,'currentMetricName_',this.settingsKey_,this.currentMetricName_,this.metrics_);Polymer.dom(this.$.top_left_controls).appendChild(metricSelector);metricSelector.addEventListener('change',this.onMetricChange_.bind(this));this.currentMetricTypeInfo_=tr.metrics.MetricRegistry.findTypeInfoWithName(this.currentMetricName_);this.recomputeButton_=tr.ui.b.createButton('Recompute',this.onRecompute_,this);Polymer.dom(this.$.top_left_controls).appendChild(this.recomputeButton_);this.$.results.addEventListener('display-ready',()=>{this.$.results.style.display='';});},async build(model){this.model_=model;await this.updateContents_();},get metricLatencyMs(){return tr.b.math.Statistics.mean(this.metricLatenciesMs_);},onMetricChange_(){this.currentMetricTypeInfo_=tr.metrics.MetricRegistry.findTypeInfoWithName(this.currentMetricName_);this.metricLatenciesMs_=[];this.updateContents_();},onRecompute_(){this.updateContents_();},get textLabel(){return'Metrics';},supportsModel(m){if(!m){return{supported:false,reason:'No model available'};}
+const path=window.location.pathname.split('/');const basename=path[path.length-1].split('.')[0]||'histograms';const anchor=document.createElement('a');anchor.download=`${basename}.${event.format}`;anchor.href=window.URL.createObjectURL(blob);anchor.click();mark.end();},onLoadVisualization_(event){if(!this.visualizationLoaded_){this.$.visualizations.style.display='block';this.$.visualizations.build(this.$.table.leafHistograms,this.histograms);this.visualizationLoaded_=true;}else if(this.$.visualizations.style.display==='none'){this.$.visualizations.style.display='block';}else{this.$.visualizations.style.display='none';}},});return{};});'use strict';tr.exportTo('tr.ui',function(){Polymer({is:'tr-ui-sp-metrics-side-panel',behaviors:[tr.ui.behaviors.SidePanel],ready(){this.model_=undefined;this.rangeOfInterest_=undefined;this.metricLatenciesMs_=[];this.metrics_=[];tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach(function(m){if(m.constructor.name==='sampleMetric')return;this.metrics_.push({label:m.constructor.name,value:m.constructor.name});},this);this.metrics_.sort((x,y)=>x.label.localeCompare(y.label));this.settingsKey_='metrics-side-panel-metric-name';this.currentMetricName_='responsivenessMetric';const metricSelector=tr.ui.b.createSelector(this,'currentMetricName_',this.settingsKey_,this.currentMetricName_,this.metrics_);Polymer.dom(this.$.top_left_controls).appendChild(metricSelector);metricSelector.addEventListener('change',this.onMetricChange_.bind(this));this.currentMetricTypeInfo_=tr.metrics.MetricRegistry.findTypeInfoWithName(this.currentMetricName_);this.recomputeButton_=tr.ui.b.createButton('Recompute',this.onRecompute_,this);Polymer.dom(this.$.top_left_controls).appendChild(this.recomputeButton_);this.$.results.addEventListener('display-ready',()=>{this.$.results.style.display='';});},async build(model){this.model_=model;await this.updateContents_();},get metricLatencyMs(){return tr.b.math.Statistics.mean(this.metricLatenciesMs_);},onMetricChange_(){this.currentMetricTypeInfo_=tr.metrics.MetricRegistry.findTypeInfoWithName(this.currentMetricName_);this.metricLatenciesMs_=[];this.updateContents_();},onRecompute_(){this.updateContents_();},get textLabel(){return'Metrics';},supportsModel(m){if(!m){return{supported:false,reason:'No model available'};}
 return{supported:true};},get model(){return this.model_;},set model(model){this.build(model);},get selection(){},set selection(_){},get rangeOfInterest(){return this.rangeOfInterest_;},set rangeOfInterest(range){this.rangeOfInterest_=range;if(this.currentMetricTypeInfo_&&this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest){if((this.metricLatencyMs===undefined)||(this.metricLatencyMs<100)){this.updateContents_();}else{this.recomputeButton_.style.background='red';}}},async updateContents_(){Polymer.dom(this.$.error).textContent='';this.$.results.style.display='none';if(!this.model_){Polymer.dom(this.$.error).textContent='Missing model';return;}
 const options={metrics:[this.currentMetricName_]};if(this.currentMetricTypeInfo_&&this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest&&this.rangeOfInterest&&!this.rangeOfInterest.isEmpty){options.rangeOfInterest=this.rangeOfInterest;}
 const startDate=new Date();const addFailureCb=failure=>{Polymer.dom(this.$.error).textContent=failure.description;};const histograms=tr.metrics.runMetrics(this.model_,options,addFailureCb);this.metricLatenciesMs_.push(new Date()-startDate);while(this.metricLatenciesMs_.length>20){this.metricLatenciesMs_.shift();}
diff --git a/catapult/systrace/systrace/tracing_agents/atrace_agent.py b/catapult/systrace/systrace/tracing_agents/atrace_agent.py
index f699fc0..caf78f4 100644
--- a/catapult/systrace/systrace/tracing_agents/atrace_agent.py
+++ b/catapult/systrace/systrace/tracing_agents/atrace_agent.py
@@ -241,19 +241,28 @@
   def _stop_collect_trace(self):
     """Stops atrace.
 
-    Note that prior to Api 23, --async-stop may not actually stop tracing.
-    Thus, this uses a fallback method of running a zero-length synchronous
-    trace if tracing is still on."""
-    result = self._device_utils.RunShellCommand(
-        self._tracer_args + ['--async_stop'], raw_output=True,
-        large_output=True, check_return=True, timeout=ADB_LARGE_OUTPUT_TIMEOUT)
-    is_trace_enabled_file = '/sys/kernel/debug/tracing/tracing_on'
-
+    Note that prior to Api 23, --async-stop isn't working correctly. It
+    doesn't stop tracing and clears trace buffer before dumping it rendering
+    results unusable."""
     if self._device_sdk_version < version_codes.MARSHMALLOW:
-      if int(self._device_utils.ReadFile(is_trace_enabled_file)):
-        # tracing was incorrectly left on, disable it
-        self._device_utils.RunShellCommand(
-            self._tracer_args + ['-t 0'], check_return=True)
+      is_trace_enabled_file = '/sys/kernel/debug/tracing/tracing_on'
+      # Stop tracing first so new data won't arrive while dump is performed (it
+      # may take a non-trivial time and tracing buffer may overflow).
+      self._device_utils.WriteFile(is_trace_enabled_file, '0')
+      result = self._device_utils.RunShellCommand(
+          self._tracer_args + ['--async_dump'], raw_output=True,
+          large_output=True, check_return=True,
+          timeout=ADB_LARGE_OUTPUT_TIMEOUT)
+      # Run synchronous tracing for 0 seconds to stop tracing, clear buffers
+      # and other state.
+      self._device_utils.RunShellCommand(
+          self._tracer_args + ['-t 0'], check_return=True)
+    else:
+      # On M+ --async_stop does everything necessary
+      result = self._device_utils.RunShellCommand(
+          self._tracer_args + ['--async_stop'], raw_output=True,
+          large_output=True, check_return=True,
+          timeout=ADB_LARGE_OUTPUT_TIMEOUT)
 
     return result
 
diff --git a/catapult/systrace/systrace/tracing_agents/atrace_agent_unittest.py b/catapult/systrace/systrace/tracing_agents/atrace_agent_unittest.py
index 9ccc6e3..203e846 100755
--- a/catapult/systrace/systrace/tracing_agents/atrace_agent_unittest.py
+++ b/catapult/systrace/systrace/tracing_agents/atrace_agent_unittest.py
@@ -16,6 +16,7 @@
 
 from devil.android import device_utils
 from devil.android.sdk import intent
+from py_utils import tempfile_ext
 
 
 DEVICE_SERIAL = 'AG8404EC0444AGC'
@@ -50,9 +51,7 @@
     devices = device_utils.DeviceUtils.HealthyDevices()
     package_info = util.get_supported_browsers()['stable']
     device = devices[0]
-    output_file_name = util.generate_random_filename_for_test()
-
-    try:
+    with tempfile_ext.TemporaryFileName() as output_file_name:
       # Launch the browser before tracing.
       device.StartActivity(
           intent.Intent(activity=package_info.activity,
@@ -76,12 +75,7 @@
       # Verify results.
       with open(output_file_name, 'r') as f:
         full_trace = f.read()
-        self.assertTrue('CPU#'in full_trace)
-    except:
-      raise
-    finally:
-      if os.path.exists(output_file_name):
-        os.remove(output_file_name)
+      self.assertTrue('CPU#' in full_trace)
 
   @decorators.HostOnlyTest
   def test_construct_atrace_args(self):
diff --git a/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent.py b/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent.py
index 58ddb7a..4765e73 100644
--- a/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent.py
+++ b/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent.py
@@ -2,13 +2,12 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import hashlib
 import os
 import re
 import stat
 import subprocess
 import sys
-import urllib
+import urllib2
 
 import py_utils
 
@@ -22,9 +21,6 @@
 # Text that ADB sends, but does not need to be displayed to the user.
 ADB_IGNORE_REGEXP = r'^capturing trace\.\.\. done|^capturing trace\.\.\.'
 
-# Constants required for converting perfetto traces.
-LINUX_SHA1 = 'cd9dbc5c92ed0167245c4559bf1971bb21378928'
-MAC_SHA1 = 'aed4ad02da526a3f1e4f9df47d4989ae9305b30e'
 T2T_OUTPUT = 'trace.systrace'
 
 def try_create_agent(options):
@@ -42,34 +38,17 @@
     return False
 
 def convert_perfetto_trace(in_file):
-  t2t_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
-                                          '../trace_to_text'))
-  loaded_t2t = False
-  if sys.platform.startswith('linux'):
-    t2t_path += '-linux-' + LINUX_SHA1
-    loaded_t2t = load_trace_to_text(t2t_path, 'linux', LINUX_SHA1)
-  elif sys.platform.startswith('darwin'):
-    t2t_path += '-mac-' + MAC_SHA1
-    loaded_t2t = load_trace_to_text(t2t_path, 'mac', MAC_SHA1)
-  if loaded_t2t:
-    os.chmod(t2t_path, stat.S_IXUSR)
-    return subprocess.call([t2t_path, 'systrace', in_file, T2T_OUTPUT]) == 0
-  return False
-
-def load_trace_to_text(t2t_path, platform, sha_value):
-  if not os.path.exists(t2t_path):
-    with open(t2t_path, 'w') as t2t:
-      url = ('https://storage.googleapis.com/chromium-telemetry/binary_dependencies/trace_to_text-'
-             + platform + '-' + sha_value)
-      return urllib.urlretrieve(url, t2t_path)
-  os.chmod(t2t_path, stat.S_IRUSR)
-  with open(t2t_path, 'rb') as t2t:
-    existing_file_hash = hashlib.sha1(t2t.read()).hexdigest()
-    if existing_file_hash != sha_value:
-      print 'Hash of trace_to_text binary does not match.'
-      os.remove(t2t_path)
-      return False
-    return True
+  traceconv_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                          '../traceconv'))
+  try:
+    traceconv = urllib2.urlopen('https://get.perfetto.dev/traceconv')
+    with open(traceconv_path, 'w') as out:
+      out.write(traceconv.read())
+  except urllib2.URLError:
+    print 'Could not download traceconv to convert the Perfetto trace.'
+    sys.exit(1)
+  os.chmod(traceconv_path, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR)
+  return subprocess.call([traceconv_path, 'systrace', in_file, T2T_OUTPUT]) == 0
 
 def is_perfetto(from_file):
   # Starts with a preamble for field ID=1 (TracePacket)
diff --git a/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent_unittest.py b/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent_unittest.py
index 610b570..53e7943 100755
--- a/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent_unittest.py
+++ b/catapult/systrace/systrace/tracing_agents/atrace_from_file_agent_unittest.py
@@ -8,10 +8,11 @@
 import os
 import unittest
 
+from py_utils import tempfile_ext
 from systrace import decorators
 from systrace import run_systrace
 from systrace import update_systrace_trace_viewer
-from systrace import util
+
 
 TEST_DIR = os.path.join(os.path.dirname(__file__), '..', 'test_data')
 
@@ -20,32 +21,29 @@
                                         'decompressed_atrace_data.txt')
 NON_EXISTENT_DATA = os.path.join(TEST_DIR, 'THIS_FILE_DOES_NOT_EXIST.txt')
 
+
 class AtraceFromFileAgentTest(unittest.TestCase):
   @decorators.HostOnlyTest
   def test_from_file(self):
     update_systrace_trace_viewer.update(force_update=True)
     self.assertTrue(os.path.exists(
         update_systrace_trace_viewer.SYSTRACE_TRACE_VIEWER_HTML_FILE))
-    output_file_name = util.generate_random_filename_for_test()
     try:
-      # use from-file to create a specific expected output
-      run_systrace.main_impl(['./run_systrace.py',
-                              '--from-file',
-                              COMPRESSED_ATRACE_DATA,
-                              '-o',
-                              output_file_name])
-      # and verify file contents
-      with contextlib.nested(open(output_file_name, 'r'),
-                             open(DECOMPRESSED_ATRACE_DATA, 'r')) as (f1, f2):
-        full_trace = f1.read()
-        expected_contents = f2.read()
-        self.assertTrue(expected_contents in full_trace)
-    except:
-      raise
+      with tempfile_ext.TemporaryFileName() as output_file_name:
+        # use from-file to create a specific expected output
+        run_systrace.main_impl(['./run_systrace.py',
+                                '--from-file',
+                                COMPRESSED_ATRACE_DATA,
+                                '-o',
+                                output_file_name])
+        # and verify file contents
+        with contextlib.nested(open(output_file_name, 'r'),
+                               open(DECOMPRESSED_ATRACE_DATA, 'r')) as (f1, f2):
+          full_trace = f1.read()
+          expected_contents = f2.read()
+          self.assertTrue(expected_contents in full_trace)
     finally:
       os.remove(update_systrace_trace_viewer.SYSTRACE_TRACE_VIEWER_HTML_FILE)
-      if os.path.exists(output_file_name):
-        os.remove(output_file_name)
 
 
   @decorators.HostOnlyTest
diff --git a/catapult/systrace/systrace/tracing_controller.py b/catapult/systrace/systrace/tracing_controller.py
index a40222c..86f1310 100644
--- a/catapult/systrace/systrace/tracing_controller.py
+++ b/catapult/systrace/systrace/tracing_controller.py
@@ -281,7 +281,7 @@
 class TracingControllerConfig(tracing_agents.TracingConfig):
   def __init__(self, output_file, trace_time, write_json,
                link_assets, asset_dir, timeout, collection_timeout,
-               device_serial_number, target):
+               device_serial_number, target, trace_buf_size):
     tracing_agents.TracingConfig.__init__(self)
     self.output_file = output_file
     self.trace_time = trace_time
@@ -292,6 +292,7 @@
     self.collection_timeout = collection_timeout
     self.device_serial_number = device_serial_number
     self.target = target
+    self.trace_buf_size = trace_buf_size
 
 
 def GetControllerConfig(options):
@@ -299,9 +300,10 @@
                                  options.write_json,
                                  options.link_assets, options.asset_dir,
                                  options.timeout, options.collection_timeout,
-                                 options.device_serial_number, options.target)
+                                 options.device_serial_number, options.target,
+                                 options.trace_buf_size)
 
 def GetChromeStartupControllerConfig(options):
   return TracingControllerConfig(None, options.trace_time,
                                  options.write_json, None, None, None, None,
-                                 None, None)
+                                 None, None, options.trace_buf_size)
diff --git a/catapult/systrace/systrace/util.py b/catapult/systrace/systrace/util.py
index b13dd51..c9571b6 100644
--- a/catapult/systrace/systrace/util.py
+++ b/catapult/systrace/systrace/util.py
@@ -4,8 +4,6 @@
 
 import optparse
 import os
-import random
-import string
 import sys
 
 from devil.android.constants import chrome
@@ -95,18 +93,6 @@
   return version
 
 
-def generate_random_filename_for_test():
-  """Used for temporary files used in tests.
-
-  Files created from 'NamedTemporaryFile' have inconsistent reuse support across
-  platforms, so it's not guaranteed that they can be reopened. Since many tests
-  communicate files via path, we typically use this method, as well as
-  manual file removal."""
-  name = ''.join(random.choice(string.ascii_uppercase +
-              string.digits) for _ in range(10))
-  return os.path.abspath(name)
-
-
 def get_supported_browsers():
   """Returns the package names of all supported browsers."""
   # Add aliases for backwards compatibility.
diff --git a/catapult/tracing/tracing/trace_data/trace_data.py b/catapult/tracing/tracing/trace_data/trace_data.py
index b3b783c..ae658a2 100644
--- a/catapult/tracing/tracing/trace_data/trace_data.py
+++ b/catapult/tracing/tracing/trace_data/trace_data.py
@@ -2,7 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import copy
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+import collections
+import gzip
 import json
 import logging
 import os
@@ -10,22 +14,26 @@
 import subprocess
 import tempfile
 import time
+import six
+
+
+try:
+  StringTypes = six.string_types # pylint: disable=invalid-name
+except NameError:
+  StringTypes = str
 
 
 _TRACING_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                             os.path.pardir, os.path.pardir)
 _TRACE2HTML_PATH = os.path.join(_TRACING_DIR, 'bin', 'trace2html')
 
-
-class NonSerializableTraceData(Exception):
-  """Raised when raw trace data cannot be serialized to TraceData."""
-  pass
-
+MIB = 1024 * 1024
 
 class TraceDataPart(object):
-  """TraceData can have a variety of events.
+  """Trace data can come from a variety of tracing agents.
 
-  These are called "parts" and are accessed by the following fixed field names.
+  Data from each agent is collected into a trace "part" and accessed by the
+  following fixed field names.
   """
   def __init__(self, raw_field_name):
     self._raw_field_name = raw_field_name
@@ -49,7 +57,6 @@
 ATRACE_PROCESS_DUMP_PART = TraceDataPart('atraceProcessDump')
 CHROME_TRACE_PART = TraceDataPart('traceEvents')
 CPU_TRACE_DATA = TraceDataPart('cpuSnapshots')
-INSPECTOR_TRACE_PART = TraceDataPart('inspectorTimelineEvents')
 TELEMETRY_PART = TraceDataPart('telemetry')
 WALT_TRACE_PART = TraceDataPart('waltTraceEvents')
 
@@ -58,283 +65,232 @@
                    ATRACE_PROCESS_DUMP_PART,
                    CHROME_TRACE_PART,
                    CPU_TRACE_DATA,
-                   INSPECTOR_TRACE_PART,
                    TELEMETRY_PART}
 
-ALL_TRACE_PARTS_RAW_NAMES = set(k.raw_field_name for k in ALL_TRACE_PARTS)
 
-def _HasTraceFor(part, raw):
-  assert isinstance(part, TraceDataPart)
-  if part.raw_field_name not in raw:
-    return False
-  return len(raw[part.raw_field_name]) > 0
+class _TraceData(object):
+  """Provides read access to traces collected from multiple tracing agents.
 
-
-def _GetFilePathForTrace(trace, dir_path):
-  """ Return path to a file that contains |trace|.
-
-  Note: if |trace| is an instance of TraceFileHandle, this reuses the trace path
-  that the trace file handle holds. Otherwise, it creates a new trace file
-  in |dir_path| directory.
+  Instances are created by calling the AsData() method on a TraceDataWriter.
   """
-  if isinstance(trace, TraceFileHandle):
-    return trace.file_path
-  with tempfile.NamedTemporaryFile(mode='w', dir=dir_path, delete=False) as fp:
-    if isinstance(trace, basestring):
-      fp.write(trace)
-    elif isinstance(trace, dict) or isinstance(trace, list):
-      json.dump(trace, fp)
-    else:
-      raise TypeError('Trace is of unknown type.')
-    return fp.name
-
-
-class TraceData(object):
-  """ TraceData holds a collection of traces from multiple sources.
-
-  A TraceData can have multiple active parts. Each part represents traces
-  collected from a different trace agent.
-  """
-  def __init__(self):
-    """Creates TraceData from the given data."""
-    self._raw_data = {}
-    self._events_are_safely_mutable = False
-
-  def _SetFromBuilder(self, d):
-    self._raw_data = d
-    self._events_are_safely_mutable = True
-
-  @property
-  def events_are_safely_mutable(self):
-    """Returns true if the events in this value are completely sealed.
-
-    Some importers want to take complex fields out of the TraceData and add
-    them to the model, changing them subtly as they do so. If the TraceData
-    was constructed with data that is shared with something outside the trace
-    data, for instance a test harness, then this mutation is unexpected. But,
-    if the values are sealed, then mutating the events is a lot faster.
-
-    We know if events are sealed if the value came from a string, or if the
-    value came from a TraceDataBuilder.
-    """
-    return self._events_are_safely_mutable
-
-  @property
-  def active_parts(self):
-    return {p for p in ALL_TRACE_PARTS if p.raw_field_name in self._raw_data}
+  def __init__(self, raw_data):
+    self._raw_data = raw_data
 
   def HasTracesFor(self, part):
-    return _HasTraceFor(part, self._raw_data)
+    return bool(self.GetTracesFor(part))
 
   def GetTracesFor(self, part):
-    """ Return the list of traces for |part| in string or dictionary forms.
-
-    Note: since this API return the traces that can be directly accessed in
-    memory, it may require lots of memory usage as some of the trace can be
-    very big.
-    For references, we have cases where Telemetry is OOM'ed because the memory
-    required for processing the trace in Python is too big (crbug.com/672097).
-    """
-    assert isinstance(part, TraceDataPart)
-    if not self.HasTracesFor(part):
-      return []
-    traces_list = self._raw_data[part.raw_field_name]
-    # Since this API return the traces in memory form, and since the memory
-    # bottleneck of Telemetry is for keeping trace in memory, there is no uses
-    # in keeping the on-disk form of tracing beyond this point. Hence we convert
-    # all traces for part of form TraceFileHandle to the JSON form.
-    for i, data in enumerate(traces_list):
-      if isinstance(data, TraceFileHandle):
-        traces_list[i] = data.AsTraceData()
-    return traces_list
+    """Return the list of traces for |part| in string or dictionary forms."""
+    if not isinstance(part, TraceDataPart):
+      raise TypeError('part must be a TraceDataPart instance')
+    return self._raw_data.get(part.raw_field_name, [])
 
   def GetTraceFor(self, part):
-    assert isinstance(part, TraceDataPart)
     traces = self.GetTracesFor(part)
     assert len(traces) == 1
     return traces[0]
 
-  def CleanUpAllTraces(self):
-    """ Remove all the traces that this has handles to.
 
-    Those include traces stored in memory & on disk. After invoking this,
-    one can no longer uses this object for collecting the traces.
-    """
-    for traces_list in self._raw_data.itervalues():
-      for trace in traces_list:
-        if isinstance(trace, TraceFileHandle):
-          trace.Clean()
-    self._raw_data = {}
-
-  def Serialize(self, file_path, trace_title=''):
-    """Serializes the trace result to |file_path|.
-
-    """
-    if not self._raw_data:
-      logging.warning('No traces to convert to html.')
-      return
-    temp_dir = tempfile.mkdtemp()
-    trace_files = []
-    try:
-      trace_size_data = {}
-      for part, traces_list in self._raw_data.iteritems():
-        for trace in traces_list:
-          path = _GetFilePathForTrace(trace, temp_dir)
-          trace_size_data.setdefault(part, 0)
-          trace_size_data[part] += os.path.getsize(path)
-          trace_files.append(path)
-      logging.info('Trace sizes in bytes: %s', trace_size_data)
-
-      start_time = time.time()
-      cmd = (
-          ['python', _TRACE2HTML_PATH] + trace_files +
-          ['--output', file_path] + ['--title', trace_title])
-      subprocess.check_output(cmd)
-
-      elapsed_time = time.time() - start_time
-      logging.info('trace2html finished in %.02f seconds.', elapsed_time)
-    finally:
-      shutil.rmtree(temp_dir)
-
-
-class TraceFileHandle(object):
-  """A trace file handle object allows storing trace data on disk.
-
-  TraceFileHandle API allows one to collect traces from Chrome into disk instead
-  of keeping them in memory. This is important for keeping memory usage of
-  Telemetry low to avoid OOM (see:
-  https://github.com/catapult-project/catapult/issues/3119).
-
-  The fact that this uses a file underneath to store tracing data means the
-  callsite is repsonsible for discarding the file when they no longer need the
-  tracing data. Call TraceFileHandle.Clean when you done using this object.
-  """
-  def __init__(self):
-    self._backing_file = None
-    self._file_path = None
-    self._trace_data = None
-
-  def Open(self):
-    assert not self._backing_file and not self._file_path
-    self._backing_file = tempfile.NamedTemporaryFile(delete=False, mode='a')
-
-  def AppendTraceData(self, partial_trace_data):
-    assert isinstance(partial_trace_data, basestring)
-    self._backing_file.write(partial_trace_data)
-
-  @property
-  def file_path(self):
-    assert self._file_path, (
-        'Either the handle need to be closed first or this handle is cleaned')
-    return self._file_path
-
-  def Close(self):
-    assert self._backing_file
-    self._backing_file.close()
-    self._file_path = self._backing_file.name
-    self._backing_file = None
-
-  def AsTraceData(self):
-    """Get the object form of trace data that this handle manages.
-
-    *Warning: this can have large memory footprint if the trace data is big.
-
-    Since this requires the in-memory form of the trace, it is no longer useful
-    to still keep the backing file underneath, invoking this will also discard
-    the file to avoid the risk of leaking the backing trace file.
-    """
-    if self._trace_data:
-      return self._trace_data
-    assert self._file_path
-    with open(self._file_path) as f:
-      self._trace_data = json.load(f)
-    self.Clean()
-    return self._trace_data
-
-  def Clean(self):
-    """Remove the backing file used for storing trace on disk.
-
-    This should be called when and only when you no longer need to use
-    TraceFileHandle.
-    """
-    assert self._file_path
-    os.remove(self._file_path)
-    self._file_path = None
+_TraceItem = collections.namedtuple(
+    '_TraceItem', ['part_name', 'handle', 'compressed'])
 
 
 class TraceDataBuilder(object):
   """TraceDataBuilder helps build up a trace from multiple trace agents.
 
-  TraceData is supposed to be immutable, but it is useful during recording to
-  have a mutable version. That is TraceDataBuilder.
+  Note: the collected trace data is maintained in a set of temporary files to
+  be later processed e.g. by the Serialize() method. To ensure proper clean up
+  of such files clients must call the CleanUpTraceData() method or, even easier,
+  use the context manager API, e.g.:
+
+      with trace_data.TraceDataBuilder() as builder:
+        builder.AddTraceFor(trace_part, data)
+        builder.Serialize(output_file)
   """
   def __init__(self):
-    self._raw_data = {}
+    self._traces = []
+    self._frozen = False
+    self._temp_dir = tempfile.mkdtemp()
+
+  def __enter__(self):
+    return self
+
+  def __exit__(self, *args):
+    self.CleanUpTraceData()
+
+  def OpenTraceHandleFor(self, part, compressed=False):
+    """Open a file handle for writing trace data into it.
+
+    Args:
+      part: A TraceDataPart instance.
+      compressed: An optional Boolean, indicates whether the written data is
+        gzipped. Note, this information is currently only used by the AsData()
+        method in order to be able to open and read the written data.
+    """
+    if not isinstance(part, TraceDataPart):
+      raise TypeError('part must be a TraceDataPart instance')
+    if self._frozen:
+      raise RuntimeError('trace data builder is no longer open for writing')
+    trace = _TraceItem(
+        part_name=part.raw_field_name,
+        handle=tempfile.NamedTemporaryFile(delete=False, dir=self._temp_dir),
+        compressed=compressed)
+    self._traces.append(trace)
+    return trace.handle
+
+  def AddTraceFileFor(self, part, trace_file):
+    """Move a file with trace data into this builder.
+
+    This is useful for situations where a client might want to start collecting
+    trace data into a file, even before the TraceDataBuilder itself is created.
+
+    Args:
+      part: A TraceDataPart instance.
+      trace_file: A path to a file containing trace data. Note: for efficiency
+        the file is moved rather than copied into the builder. Therefore the
+        source file will no longer exist after calling this method; and the
+        lifetime of the trace data will thereafter be managed by this builder.
+    """
+    with self.OpenTraceHandleFor(part) as handle:
+      pass
+    if os.name == 'nt':
+      # On windows os.rename won't overwrite, so the destination path needs to
+      # be removed first.
+      os.remove(handle.name)
+    os.rename(trace_file, handle.name)
+
+  def AddTraceFor(self, part, data, allow_unstructured=False):
+    """Record new trace data into this builder.
+
+    Args:
+      part: A TraceDataPart instance.
+      data: The trace data to write: a json-serializable dict, or unstructured
+        text data as a string.
+      allow_unstructured: This must be set to True to allow passing
+        unstructured text data as input. Note: the use of this flag is
+        discouraged and only exists to support legacy clients; new tracing
+        agents should all produce structured trace data (e.g. proto or json).
+    """
+    if isinstance(data, StringTypes):
+      if not allow_unstructured:
+        raise ValueError('must pass allow_unstructured=True for text data')
+      do_write = lambda d, f: f.write(d)
+    elif isinstance(data, dict):
+      do_write = json.dump
+    else:
+      raise TypeError('invalid trace data type')
+    with self.OpenTraceHandleFor(part) as handle:
+      do_write(data, handle)
+
+  def Freeze(self):
+    """Do not allow writing any more data into this builder."""
+    self._frozen = True
+    return self
+
+  def CleanUpTraceData(self):
+    """Clean up resources used by the data builder."""
+    if self._traces is None:
+      return  # Already cleaned up.
+    self.Freeze()
+    for trace in self._traces:
+      # Make sure all trace handles are closed. It's fine if we close some
+      # of them multiple times.
+      trace.handle.close()
+    shutil.rmtree(self._temp_dir)
+    self._temp_dir = None
+    self._traces = None
+
+  def Serialize(self, file_path, trace_title=None):
+    """Serialize the trace data to a file in HTML format."""
+    self.Freeze()
+    assert self._traces, 'trace data has already been cleaned up'
+
+    trace_files = [trace.handle.name for trace in self._traces]
+    SerializeAsHtml(trace_files, file_path, trace_title)
 
   def AsData(self):
-    if self._raw_data is None:
-      raise Exception('Can only AsData once')
-    data = TraceData()
-    data._SetFromBuilder(self._raw_data)
-    self._raw_data = None
-    return data
+    """Allow in-memory access to read the collected JSON trace data.
 
-  def AddTraceFor(self, part, trace):
-    assert isinstance(part, TraceDataPart), part
-    if part == CHROME_TRACE_PART:
-      assert (isinstance(trace, dict) or
-              isinstance(trace, list) or
-              isinstance(trace, TraceFileHandle))
-    else:
-      assert (isinstance(trace, basestring) or
-              isinstance(trace, dict) or
-              isinstance(trace, list))
+    This method is only provided for writing tests which require read access
+    to the collected trace data (e.g. for tracing agents to test they correctly
+    write data), and to support legacy TBMv1 metric computation. Only traces
+    in JSON format are supported.
 
-    if self._raw_data is None:
-      raise Exception('Already called AsData() on this builder.')
+    Be careful: this may require a lot of memory if the traces to process are
+    very large. This has lead in the past to OOM errors (e.g. crbug/672097).
 
-    self._raw_data.setdefault(part.raw_field_name, [])
-    self._raw_data[part.raw_field_name].append(trace)
+    TODO(crbug/928278): Ideally, this method should be removed when it can be
+    entirely replaced by calls to an external trace processor.
+    """
+    self.Freeze()
+    assert self._traces, 'trace data has already been cleaned up'
 
-  def HasTracesFor(self, part):
-    return _HasTraceFor(part, self._raw_data)
+    raw_data = {}
+    for trace in self._traces:
+      traces_for_part = raw_data.setdefault(trace.part_name, [])
+      opener = gzip.open if trace.compressed else open
+      with opener(trace.handle.name, 'rb') as f:
+        traces_for_part.append(json.load(f))
+    return _TraceData(raw_data)
+
+  def IterTraceParts(self):
+    """Iterates over trace parts.
+
+    Return value: iterator over pairs (part_name, file_path).
+    """
+    for trace in self._traces:
+      yield trace.part_name, trace.handle.name
 
 
-def CreateTraceDataFromRawData(raw_data):
-  """Convenient method for creating a TraceData object from |raw_data|.
-     This is mostly used for testing.
+def CreateTestTrace(number=1):
+  """Convenient helper method to create trace data objects for testing.
 
-     Args:
-        raw_data can be:
-            + A dictionary that repsents multiple trace parts. Keys of the
-            dictionary must always contain 'traceEvents', as chrome trace
-            must always present.
-            + A list that represents Chrome trace events.
-            + JSON string of either above.
+  Objects are created via the usual trace data writing route, so clients are
+  also responsible for cleaning up trace data themselves.
 
+  Clients are meant to treat these test traces as opaque. No guarantees are
+  made about their contents, which they shouldn't try to read.
   """
-  raw_data = copy.deepcopy(raw_data)
-  if isinstance(raw_data, basestring):
-    json_data = json.loads(raw_data)
-  else:
-    json_data = raw_data
+  builder = TraceDataBuilder()
+  builder.AddTraceFor(CHROME_TRACE_PART, {'traceEvents': [{'test': number}]})
+  return builder.Freeze()
 
-  b = TraceDataBuilder()
-  if not json_data:
-    return b.AsData()
-  if isinstance(json_data, dict):
-    assert 'traceEvents' in json_data, 'Only raw chrome trace is supported'
-    trace_parts_keys = []
-    for k in json_data:
-      if k != 'traceEvents' and k in ALL_TRACE_PARTS_RAW_NAMES:
-        trace_parts_keys.append(k)
-        b.AddTraceFor(TraceDataPart(k), json_data[k])
-    # Delete the data for extra keys to form trace data for Chrome part only.
-    for k in trace_parts_keys:
-      del json_data[k]
-    b.AddTraceFor(CHROME_TRACE_PART, json_data)
-  elif isinstance(json_data, list):
-    b.AddTraceFor(CHROME_TRACE_PART, {'traceEvents': json_data})
-  else:
-    raise NonSerializableTraceData('Unrecognized data format.')
-  return b.AsData()
+
+def CreateFromRawChromeEvents(events):
+  """Convenient helper to create trace data objects from raw Chrome events.
+
+  This bypasses trace data writing, going directly to the in-memory json trace
+  representation, so there is no need for trace file cleanup.
+
+  This is used only for testing legacy clients that still read trace data.
+  """
+  assert isinstance(events, list)
+  return _TraceData({
+      CHROME_TRACE_PART.raw_field_name: [{'traceEvents': events}]})
+
+
+def SerializeAsHtml(trace_files, html_file, trace_title=None):
+  """Serialize a set of traces to a single file in HTML format.
+
+  Args:
+    trace_files: a list of file names, each containing a trace from
+        one of the tracing agents.
+    html_file: a name of the output file.
+    trace_title: optional. A title for the resulting trace.
+  """
+  if not trace_files:
+    raise ValueError('trace files list is empty')
+
+  input_size = sum(os.path.getsize(trace_file) for trace_file in trace_files)
+
+  cmd = ['python', _TRACE2HTML_PATH]
+  cmd.extend(trace_files)
+  cmd.extend(['--output', html_file])
+  if trace_title is not None:
+    cmd.extend(['--title', trace_title])
+
+  start_time = time.time()
+  subprocess.check_output(cmd)
+  elapsed_time = time.time() - start_time
+  logging.info('trace2html processed %.01f MiB of trace data in %.02f seconds.',
+               1.0 * input_size / MIB, elapsed_time)
diff --git a/catapult/tracing/tracing/trace_data/trace_data_unittest.py b/catapult/tracing/tracing/trace_data/trace_data_unittest.py
index dd91596..fdc48e2 100644
--- a/catapult/tracing/tracing/trace_data/trace_data_unittest.py
+++ b/catapult/tracing/tracing/trace_data/trace_data_unittest.py
@@ -2,98 +2,113 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import datetime
-import exceptions
+import base64
+import json
 import os
-import shutil
 import tempfile
 import unittest
 
+from py_utils import tempfile_ext
 from tracing.trace_data import trace_data
-from tracing_build import html2trace
 
 
 class TraceDataTest(unittest.TestCase):
-  def testSerialize(self):
-    test_dir = tempfile.mkdtemp()
-    trace_path = os.path.join(test_dir, 'test_trace.json')
-    try:
-      ri = trace_data.CreateTraceDataFromRawData({'traceEvents': [1, 2, 3]})
-      ri.Serialize(trace_path)
-      with open(trace_path) as f:
-        json_traces = html2trace.ReadTracesFromHTMLFile(f)
-      self.assertEqual(json_traces, [{'traceEvents': [1, 2, 3]}])
-    finally:
-      shutil.rmtree(test_dir)
-
-  def testEmptyArrayValue(self):
-    # We can import empty lists and empty string.
-    d = trace_data.CreateTraceDataFromRawData([])
-    self.assertFalse(d.HasTracesFor(trace_data.CHROME_TRACE_PART))
-
-  def testInvalidTrace(self):
-    with self.assertRaises(AssertionError):
-      trace_data.CreateTraceDataFromRawData({'hello': 1})
-
-  def testListForm(self):
-    d = trace_data.CreateTraceDataFromRawData([{'ph': 'B'}])
+  def testHasTracesForChrome(self):
+    d = trace_data.CreateFromRawChromeEvents([{'ph': 'B'}])
     self.assertTrue(d.HasTracesFor(trace_data.CHROME_TRACE_PART))
-    events = d.GetTracesFor(trace_data.CHROME_TRACE_PART)[0].get(
-        'traceEvents', [])
-    self.assertEquals(1, len(events))
 
-  def testStringForm(self):
-    d = trace_data.CreateTraceDataFromRawData('[{"ph": "B"}]')
-    self.assertTrue(d.HasTracesFor(trace_data.CHROME_TRACE_PART))
-    events = d.GetTracesFor(trace_data.CHROME_TRACE_PART)[0].get(
-        'traceEvents', [])
-    self.assertEquals(1, len(events))
+  def testHasNotTracesForCpu(self):
+    d = trace_data.CreateFromRawChromeEvents([{'ph': 'B'}])
+    self.assertFalse(d.HasTracesFor(trace_data.CPU_TRACE_DATA))
+
+  def testGetTracesForChrome(self):
+    d = trace_data.CreateFromRawChromeEvents([{'ph': 'B'}])
+    ts = d.GetTracesFor(trace_data.CHROME_TRACE_PART)
+    self.assertEqual(len(ts), 1)
+    self.assertEqual(ts[0], {'traceEvents': [{'ph': 'B'}]})
+
+  def testGetNoTracesForCpu(self):
+    d = trace_data.CreateFromRawChromeEvents([{'ph': 'B'}])
+    ts = d.GetTracesFor(trace_data.CPU_TRACE_DATA)
+    self.assertEqual(ts, [])
 
 
 class TraceDataBuilderTest(unittest.TestCase):
-  def testBasicChrome(self):
-    builder = trace_data.TraceDataBuilder()
-    builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
-                        {'traceEvents': [1, 2, 3]})
+  def testAddTraceDataAndSerialize(self):
+    with tempfile_ext.TemporaryFileName() as trace_path:
+      with trace_data.TraceDataBuilder() as builder:
+        builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+                            {'traceEvents': [1, 2, 3]})
+        builder.Serialize(trace_path)
+        self.assertTrue(os.path.exists(trace_path))
+        self.assertGreater(os.stat(trace_path).st_size, 0)  # File not empty.
 
-    d = builder.AsData()
-    self.assertTrue(d.HasTracesFor(trace_data.CHROME_TRACE_PART))
+  def testAddTraceForRaisesWithInvalidPart(self):
+    with trace_data.TraceDataBuilder() as builder:
+      with self.assertRaises(TypeError):
+        builder.AddTraceFor('not_a_trace_part', {})
 
-    self.assertRaises(Exception, builder.AsData)
+  def testAddTraceWithUnstructuredData(self):
+    with trace_data.TraceDataBuilder() as builder:
+      builder.AddTraceFor(trace_data.TELEMETRY_PART, 'unstructured trace',
+                          allow_unstructured=True)
 
-  def testSetTraceFor(self):
-    telemetry_trace = {
-        'traceEvents': [1, 2, 3],
-        'metadata': {
-            'field1': 'value1'
-        }
-    }
+  def testAddTraceRaisesWithImplicitUnstructuredData(self):
+    with trace_data.TraceDataBuilder() as builder:
+      with self.assertRaises(ValueError):
+        builder.AddTraceFor(trace_data.TELEMETRY_PART, 'unstructured trace')
 
-    builder = trace_data.TraceDataBuilder()
-    builder.AddTraceFor(trace_data.TELEMETRY_PART, telemetry_trace)
-    d = builder.AsData()
+  def testAddTraceFileFor(self):
+    original_data = {'msg': 'The answer is 42'}
+    with tempfile.NamedTemporaryFile(delete=False) as source:
+      json.dump(original_data, source)
+    with trace_data.TraceDataBuilder() as builder:
+      builder.AddTraceFileFor(trace_data.CHROME_TRACE_PART, source.name)
+      self.assertFalse(os.path.exists(source.name))
+      out_data = builder.AsData().GetTraceFor(trace_data.CHROME_TRACE_PART)
 
-    self.assertEqual(d.GetTracesFor(trace_data.TELEMETRY_PART),
-                     [telemetry_trace])
+    self.assertEqual(original_data, out_data)
 
-  def testSetTraceForRaisesWithInvalidPart(self):
-    builder = trace_data.TraceDataBuilder()
+  def testOpenTraceHandleFor(self):
+    original_data = {'msg': 'The answer is 42'}
+    with trace_data.TraceDataBuilder() as builder:
+      with builder.OpenTraceHandleFor(trace_data.CHROME_TRACE_PART) as handle:
+        handle.write(json.dumps(original_data))
+      out_data = builder.AsData().GetTraceFor(trace_data.CHROME_TRACE_PART)
 
-    self.assertRaises(exceptions.AssertionError,
-                      lambda: builder.AddTraceFor('not_a_trace_part', {}))
+    # Trace handle should be cleaned up.
+    self.assertFalse(os.path.exists(handle.name))
+    self.assertEqual(original_data, out_data)
 
-  def testSetTraceForRaisesWithInvalidTrace(self):
-    builder = trace_data.TraceDataBuilder()
+  def testOpenTraceHandleForCompressedData(self):
+    original_data = {'msg': 'The answer is 42'}
+    # gzip.compress() does not work in python 2, so hardcode the encoded data.
+    compressed_data = base64.b64decode(
+        'H4sIAIDMblwAA6tWyi1OV7JSUArJSFVIzCsuTy1SyCxWMDFSquUCAA4QMtscAAAA')
+    with trace_data.TraceDataBuilder() as builder:
+      with builder.OpenTraceHandleFor(
+          trace_data.CHROME_TRACE_PART, compressed=True) as handle:
+        handle.write(compressed_data)
+      out_data = builder.AsData().GetTraceFor(trace_data.CHROME_TRACE_PART)
 
-    self.assertRaises(
-        exceptions.AssertionError,
-        lambda: builder.AddTraceFor(trace_data.TELEMETRY_PART,
-                                    datetime.time.min))
+    # Trace handle should be cleaned up.
+    self.assertFalse(os.path.exists(handle.name))
+    self.assertEqual(original_data, out_data)
 
-  def testSetTraceForRaisesAfterAsData(self):
-    builder = trace_data.TraceDataBuilder()
-    builder.AsData()
+  def testCantWriteAfterCleanup(self):
+    with trace_data.TraceDataBuilder() as builder:
+      builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+                          {'traceEvents': [1, 2, 3]})
+      builder.CleanUpTraceData()
+      with self.assertRaises(RuntimeError):
+        builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+                            {'traceEvents': [1, 2, 3]})
 
-    self.assertRaises(
-        exceptions.Exception,
-        lambda: builder.AddTraceFor(trace_data.TELEMETRY_PART, {}))
+  def testCantWriteAfterFreeze(self):
+    with trace_data.TraceDataBuilder() as builder:
+      builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+                          {'traceEvents': [1, 2, 3]})
+      builder.Freeze()
+      with self.assertRaises(RuntimeError):
+        builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+                            {'traceEvents': [1, 2, 3]})
diff --git a/catapult/tracing/tracing_project.py b/catapult/tracing/tracing_project.py
index 633b98b..b4475b9 100644
--- a/catapult/tracing/tracing_project.py
+++ b/catapult/tracing/tracing_project.py
@@ -83,6 +83,7 @@
 
   jszip_path = os.path.join(tracing_third_party_path, 'jszip')
   pako_path = os.path.join(tracing_third_party_path, 'pako')
+  jpegjs_path = os.path.join(tracing_third_party_path, 'jpeg-js')
 
   glmatrix_path = os.path.join(
       tracing_third_party_path, 'gl-matrix', 'dist')
@@ -120,6 +121,7 @@
     self.source_paths.append(self.mre_path)
     self.source_paths.append(self.jszip_path)
     self.source_paths.append(self.pako_path)
+    self.source_paths.append(self.jpegjs_path)
     self.source_paths.append(self.glmatrix_path)
     self.source_paths.append(self.mannwhitneyu_path)
     self.source_paths.append(self.d3_path)